Classification + Tariff Calculation API API reference
Get HTS codes and duties from one call.
Two POST endpoints, one product or 120 per batch by default, with every field, error code and curl example.
On this page
What does the Classification + Tariff Calculation API return?
The Classification + Tariff Calculation API is GingerControl's REST API for U.S. HTS classification and duty lookup. You POST a plain product description and its country of origin; the API returns the HTS code, the general and special rates, and each Chapter 99 duty layer with its code and rate. Use the single-product endpoint for one item or the batch endpoint for 120 items per call by default (sized to your throughput on request). Keys are issued offline, and the results feed entry prep that a licensed customs broker reviews.
- Base URL:
https://api.gingercontrol.com - Endpoints:
POST /openapi/v1/tarifffor one product,POST /openapi/v1/tariff/batchfor up to 120 - Authentication: an API key in the
X-Api-Keyheader, issued offline by GingerControl - Format: JSON request bodies and JSON responses
How do I get access and integrate?
Read the contract below, book a call for an API key sized to your traffic, then wire the results into your own systems.
Read the API contract
Read the endpoint sections below in full before you start. They cover request patterns, input and output structures, error semantics and rate limits, which cuts integration and debugging time.
Ask for an API key
Book a call with us. We ask about your calling pattern, IP allowlist, peak QPS and latency expectations, then deliver your key through an offline channel, such as email.
Integrate
Most teams finish the basic integration quickly, then spend more of their time wiring the returned data into their own systems.
Keep your production key secure. At this stage, the API key is the sole basis for billing.
Single-product endpoint
Send one product description and its country of origin; get back its HTS code and tariff data.
POST/openapi/v1/tariffhttps://api.gingercontrol.com
Request headers
| Header | Required | Meaning | Purpose |
|---|---|---|---|
Content-Type | Yes | Request body format, set by the caller | Must be application/json |
X-Api-Key | Yes | Caller credential, issued offline by GingerControl | Authentication, rate limiting and quota control |
X-Request-Id | No | Request trace ID, best generated by your system | Troubleshooting, log tracing and investigation; the server generates one if you leave it out |
Response headers
| Header | Description | Purpose |
|---|---|---|
Content-Type | Always application/json | The response body format |
X-Request-Id | Echoes your value, or a server-generated one | Log it for troubleshooting |
Retry-After | Returned only with 429 Too Many Requests | How many seconds to wait before retrying |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
description | string | Yes | Product description. Must not be empty; up to 10,000 characters |
country_of_origin | string | Yes | ISO 3166-1 alpha-2 country code (code list). EU (European Union) and UK (United Kingdom) are also accepted; GB is processed as UK |
extra | object | No | Additional input. Only steel_pour_country and aluminum_pour_country are read; other fields are ignored |
extra.steel_pour_country | string | No | Steel pour country, same rules as country_of_origin |
extra.aluminum_pour_country | string | No | Aluminum pour country, same rules as country_of_origin |
{
"description": "Cotton knit short sleeve T-shirt",
"country_of_origin": "DE",
"extra": {
"steel_pour_country": "IT",
"aluminum_pour_country": "FR"
}
}Successful response
| Field | Type | Notes |
|---|---|---|
hts_code | string | The HTS code for the product. Normally 10 digits; a split-code product may return an 8-digit parent code such as 9101.11.40 |
tariffs | object or null | Tariff data for the product. May be null for split-code products |
tariffs.general_rate | string or null | General rate, which can be read as MFN |
tariffs.special_rate | string or null | Special rate, lightly normalized (a trailing parenthetical note is removed, for example) |
tariffs.<module_key> | array | Chapter 99 entries for that module, each {code, rate} |
tariffs.<module_key>[].code | string | Chapter 99 code, for example 9903.85.08 |
tariffs.<module_key>[].rate | string | Rate as a percentage string, such as 25% |
components | array | Components of a split-code product; omitted for ordinary products |
components[].hts_code | string | Component HTS code, always 10 digits |
components[].tariffs | object or null | Tariff data for the component |
{
"hts_code": "6109.10.0012",
"tariffs": {
"general_rate": "16.5%",
"special_rate": "Free",
"Section 301": [],
"Section 232 - Metals": []
}
}Each module key in tariffs, here Section 301 and Section 232 - Metals, holds a list of {code, rate} entries. An empty list means no Chapter 99 entry from that module applies to this product.
Error response
| Field | Type | Notes |
|---|---|---|
detail | object | Error object |
detail.code | string | Error code, for programmatic handling |
detail.message | string | Error message, for logging and troubleshooting |
{
"detail": {
"code": "invalid_request",
"message": "Invalid request body."
}
}Error codes
| HTTP status | Code | Meaning |
|---|---|---|
401 Unauthorized | missing_api_key | X-Api-Key was not provided in the request headers |
401 Unauthorized | invalid_api_key | API key validation failed |
403 Forbidden | api_key_revoked | The API key has been revoked |
403 Forbidden | client_disabled | The caller account has been disabled |
422 Unprocessable Entity | invalid_request | The request body is malformed, fields are missing, field types are wrong, or values do not satisfy the contract |
429 Too Many Requests | request_rate_limited | Request-level protection was triggered |
429 Too Many Requests | item_rate_limited | Item-level quota protection was triggered |
500 Internal Server Error | classification_failed | HTS identification failed |
500 Internal Server Error | calculator_failed | Tariff calculation failed |
500 Internal Server Error | internal_error | An unclassified internal error occurred |
Try it with curl
Replace YOUR_API_KEY with the key we issued you offline, then run the command as is.
curl -X POST 'https://api.gingercontrol.com/openapi/v1/tariff' \
-H 'Content-Type: application/json' \
-H 'X-Api-Key: YOUR_API_KEY' \
-H 'X-Request-Id: manual-single-001' \
-d '{
"description": "Cotton knit short sleeve T-shirt",
"country_of_origin": "DE",
"extra": {
"steel_pour_country": "IT",
"aluminum_pour_country": "FR"
}
}'Batch endpoint
Send up to 120 products in one call, each with your own item_id; results come back in the order you sent them, each with its own status.
POST/openapi/v1/tariff/batchhttps://api.gingercontrol.com
Headers
The batch endpoint takes the same request headers and returns the same response headers as the single-product endpoint: Content-Type, X-Api-Key and the optional X-Request-Id in; Content-Type, X-Request-Id and, with a 429, Retry-After out.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
items | array | Yes | Products to process; up to 120 items in the current version |
items[].item_id | string | Yes | Your own identifier, to map each result back to your record. Must be unique within the request |
items[].description | string | Yes | Product description. Must not be empty; up to 10,000 characters |
items[].country_of_origin | string | Yes | ISO 3166-1 alpha-2 country code. EU and UK are also accepted; GB is processed as UK |
items[].extra | object | No | Only steel_pour_country and aluminum_pour_country are read; other fields are ignored |
items[].extra.steel_pour_country | string | No | Steel pour country, same rules as country_of_origin |
items[].extra.aluminum_pour_country | string | No | Aluminum pour country, same rules as country_of_origin |
{
"items": [
{
"item_id": "SKU-DE-001",
"description": "Cotton knit short sleeve T-shirt",
"country_of_origin": "DE",
"extra": {
"steel_pour_country": "IT"
}
},
{
"item_id": "SKU-FR-002",
"description": "Cotton crew neck T-shirt",
"country_of_origin": "FR",
"extra": {}
}
]
}Successful response
| Field | Type | Notes |
|---|---|---|
items | array | Results, in the same order as the request |
items[].item_id | string | Your identifier, for reconciliation |
items[].status | string | ok or failed |
items[].hts_code | string | HTS code of a successful item. Normally 10 digits; a split-code product may return an 8-digit parent code. Only when status is ok |
items[].tariffs | object or null | Tariff data of a successful item; may be null for split-code products. Only when status is ok |
items[].tariffs.general_rate | string or null | General rate, which can be read as MFN |
items[].tariffs.special_rate | string or null | Special rate, lightly normalized |
items[].tariffs.<module_key> | array | Module entries, each {code, rate} |
items[].tariffs.<module_key>[].code | string | Chapter 99 code |
items[].tariffs.<module_key>[].rate | string | Rate |
items[].components | array | Components of a split-code product; omitted for ordinary products. Only when status is ok |
items[].components[].hts_code | string | Component HTS code, always 10 digits |
items[].components[].tariffs | object or null | Tariff data for the component |
items[].code | string | Failure code of a failed item. Only when status is failed |
summary | object | Summary of the batch request |
summary.total | integer | Items in this request |
summary.succeeded | integer | Items that succeeded |
summary.failed | integer | Items that failed |
{
"items": [
{
"item_id": "SKU-DE-001",
"status": "ok",
"hts_code": "6109.10.0012",
"tariffs": {
"general_rate": "16.5%",
"special_rate": "Free",
"Section 301": [],
"Section 232 - Metals": []
}
},
{
"item_id": "SKU-FR-002",
"status": "ok",
"hts_code": "6109.10.0004",
"tariffs": {
"general_rate": "16.5%",
"special_rate": "Free",
"Section 301": [],
"Section 232 - Metals": []
}
}
],
"summary": {
"total": 2,
"succeeded": 2,
"failed": 0
}
}Error response
Batch-level errors use the same detail object as the single-product endpoint, with detail.code and detail.message.
{
"detail": {
"code": "item_rate_limited",
"message": "Item quota exceeded."
}
}Error codes
A batch-level error fails the whole request:
| HTTP status | Code | Meaning |
|---|---|---|
401 Unauthorized | missing_api_key | X-Api-Key was not provided in the request headers |
401 Unauthorized | invalid_api_key | API key validation failed |
403 Forbidden | api_key_revoked | The API key has been revoked |
403 Forbidden | client_disabled | The caller account has been disabled |
422 Unprocessable Entity | invalid_request | The top-level structure is invalid, item_id values are duplicated, fields are missing, field types are wrong, or values do not satisfy the contract |
429 Too Many Requests | request_rate_limited | Request-level protection was triggered |
429 Too Many Requests | item_rate_limited | Item-level quota protection was triggered |
500 Internal Server Error | internal_error | An unclassified batch-level internal error occurred |
An item-level failure comes back inside a successful response, as status: failed with a code on that item:
| Code | Meaning |
|---|---|
classification_failed | HTS identification failed for that item |
calculator_failed | Tariff calculation failed for that item |
internal_error | An unclassified internal error occurred for that item |
Try it with curl
Replace YOUR_API_KEY with the key we issued you offline, then run the command as is.
curl -X POST 'https://api.gingercontrol.com/openapi/v1/tariff/batch' \
-H 'Content-Type: application/json' \
-H 'X-Api-Key: YOUR_API_KEY' \
-H 'X-Request-Id: manual-batch-001' \
-d '{
"items": [
{
"item_id": "SKU-DE-001",
"description": "Cotton knit short sleeve T-shirt",
"country_of_origin": "DE",
"extra": {
"steel_pour_country": "IT"
}
},
{
"item_id": "SKU-FR-002",
"description": "Cotton crew neck T-shirt",
"country_of_origin": "FR",
"extra": {}
}
]
}'How do rate limits and quotas work?
Every call passes request-level protection, and every product processed draws from an item-level quota tied to your API key.
- When requests come too fast, the API returns
429 Too Many Requests. - Quota is consumed per product: the single-product endpoint uses 1, the batch endpoint uses the number of entries in
items. - Both endpoints draw from the same item quota under the same API key.
- Each key is configured to that customer's traffic model, so quotas differ from customer to customer.
How should I handle errors and retries?
Check the request body on a 422, wait the number of seconds in Retry-After on a 429, and log the X-Request-Id of every call.
- 422 Unprocessable Entity: the body structure, field types or field values usually do not match the contract. Check the request body first.
- 429 Too Many Requests: request-level protection or the item quota was triggered. Retry using the
Retry-Afterresponse header. - Request IDs: keep request logs in your own system and record the
X-Request-Idof each call; it cuts troubleshooting time during integration and operations. - Timeouts and alerts: set reasonable timeouts, retries and failure alerts so network fluctuations and upstream jitter are handled cleanly.
How do I keep my API key safe?
Keep the key on your servers, out of anything users can see, and tell us right away if a key may have leaked.
- Never embed your API key in frontend code, browser code or anything exposed to end users.
- Store keys in a secure configuration system, such as a secret manager or tightly controlled environment variables.
- If a key is lost, may have leaked, or needs rotating, contact us offline as soon as possible.
Who do I contact for API support?
Email chen@gingercontrol.com for help with integration, rollout or day-to-day operations.