API Documentation
Integrate HS code classification into your applications with the Customs8 REST API.
https://customs8.ai/api
Authentication
All API requests (except /health) require authentication via an API key.
Generate API keys in your dashboard.
Pass your API key via the Authorization header using the Bearer scheme:
curl -H "Authorization: Bearer c8_your_api_key" \
https://customs8.ai/api/v1/me
import requests
response = requests.get(
"https://customs8.ai/api/v1/me",
headers={"Authorization": "Bearer c8_your_api_key"}
)
print(response.json())
const response = await fetch('https://customs8.ai/api/v1/me', {
headers: { 'Authorization': 'Bearer c8_your_api_key' }
});
const data = await response.json();
Usage Tracking
All classification endpoints include a usage object in the response, so you can track your remaining quota without a separate API call.
"usage": {
"classifications_remaining": 47,
"classifications_used": 3,
"classifications_limit": 50,
"overage_allowed": false
}
| Field | Type | Description |
|---|---|---|
| classifications_remaining | integer | Number of classifications left in the current billing period |
| classifications_used | integer | Number of classifications used this billing period |
| classifications_limit | integer | Total classifications included in your plan |
| overage_allowed | boolean | Whether your plan allows exceeding the limit (billed as overage) |
/v1/classify
Submit a product for HS code classification. The classification is processed asynchronously -- you receive a 202 Accepted response with a classification ID. Poll for results or configure a webhook.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| image_url | string | optional | URL of a product image (up to 5 images supported) |
| image_base64 | string | optional | Base64-encoded product image |
| description | string | optional | Product text description |
| origin_country | string | optional | 2-letter ISO country code of product origin |
| destination_country | string | optional | 2-letter ISO country code (default: NL) |
Note: At least one of image_url, image_base64, or description is required.
Example Request
curl -X POST https://customs8.ai/api/v1/classify \
-H "Authorization: Bearer c8_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"description": "Wireless bluetooth over-ear headphones with noise cancellation",
"image_url": "https://example.com/product.jpg",
"origin_country": "CN",
"destination_country": "NL"
}'
import requests
response = requests.post(
"https://customs8.ai/api/v1/classify",
headers={"Authorization": "Bearer c8_your_api_key"},
json={
"description": "Wireless bluetooth over-ear headphones with noise cancellation",
"image_url": "https://example.com/product.jpg",
"origin_country": "CN",
"destination_country": "NL"
}
)
data = response.json()
classification_id = data["data"]["id"]
print(f"Classification queued: {classification_id}")
const response = await fetch('https://customs8.ai/api/v1/classify', {
method: 'POST',
headers: {
'Authorization': 'Bearer c8_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
description: 'Wireless bluetooth over-ear headphones with noise cancellation',
image_url: 'https://example.com/product.jpg',
origin_country: 'CN',
destination_country: 'NL',
}),
});
const { data } = await response.json();
console.log('Classification queued:', data.id);
Response 202 Accepted
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"message": "Classification queued. Poll GET /v1/classifications/{id} for results, or configure a webhook to receive notifications."
},
"usage": {
"classifications_remaining": 47,
"classifications_used": 3,
"classifications_limit": 50,
"overage_allowed": false
}
}
/v1/classifications/{id}
Retrieve a classification result by its UUID. Poll this endpoint after submitting a classification request until the status changes from pending to completed.
Example Request
curl https://customs8.ai/api/v1/classifications/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer c8_your_api_key"
import requests
import time
classification_id = "550e8400-e29b-41d4-a716-446655440000"
# Poll until completed (typically 25-35 seconds)
while True:
response = requests.get(
f"https://customs8.ai/api/v1/classifications/{classification_id}",
headers={"Authorization": "Bearer c8_your_api_key"}
)
data = response.json()["data"]
if data["status"] == "completed":
print(f"HS Code: {data['hs_code']['code_8']}")
print(f"Confidence: {data['confidence']}%")
break
elif data["status"] == "failed":
print("Classification failed")
break
time.sleep(5)
const classificationId = '550e8400-e29b-41d4-a716-446655440000';
// Poll until completed
async function pollResult() {
const response = await fetch(
`https://customs8.ai/api/v1/classifications/${classificationId}`,
{ headers: { 'Authorization': 'Bearer c8_your_api_key' } }
);
const { data } = await response.json();
if (data.status === 'completed') return data;
if (data.status === 'failed') throw new Error('Classification failed');
await new Promise(r => setTimeout(r, 5000));
return pollResult();
}
Response 200 OK
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"source": "api",
"created_at": "2026-03-03T12:00:00+00:00",
"hs_code": {
"code_6": "8518.30",
"code_8": "8518.30.00",
"description": "Headphones and earphones",
"chapter": "85"
},
"confidence": 92,
"reasoning": "The product is wireless bluetooth headphones, which fall under HS heading 8518 for microphones, loudspeakers, and headphones.",
"decision_tree": [
{"level": "section", "code": "XVI", "description": "Machinery and mechanical appliances"},
{"level": "chapter", "code": "85", "description": "Electrical machinery and equipment"},
{"level": "heading", "code": "8518", "description": "Microphones, loudspeakers, headphones"}
],
"alternative_codes": [
{
"code": "8518.10.00",
"description": "Microphones",
"reasoning": "Would apply if the product includes a built-in microphone as primary function"
}
],
"product_summary": "Wireless bluetooth over-ear headphones with noise cancellation",
"ai_usage": {
"input_tokens": 1234,
"output_tokens": 567,
"cost_usd": 0.00408
},
"completed_at": "2026-03-03T12:00:30+00:00"
},
"usage": {
"classifications_remaining": 47,
"classifications_used": 3,
"classifications_limit": 50,
"overage_allowed": false
}
}
/v1/classifications
List all classifications for your organization, sorted by newest first. Results are paginated.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| per_page | integer | 20 | Number of results per page (max 100) |
Response 200 OK
{
"success": true,
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"product_summary": "Wireless bluetooth over-ear headphones",
"hs_code": "8518.30.00",
"confidence": 92,
"created_at": "2026-03-03T12:00:00+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 42,
"last_page": 3
},
"usage": {
"classifications_remaining": 47,
"classifications_used": 3,
"classifications_limit": 50,
"overage_allowed": false
}
}
/v1/classify/batch
Submit up to 100 products for classification in a single request. Each item is processed individually and asynchronously. Returns a batch ID to track overall progress.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| items | array | required | Array of classification requests (max 100). Each item has the same fields as POST /v1/classify. |
Example Request
curl -X POST https://customs8.ai/api/v1/classify/batch \
-H "Authorization: Bearer c8_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"items": [
{"description": "Wireless bluetooth headphones", "origin_country": "CN"},
{"description": "Stainless steel water bottle", "origin_country": "CN"},
{"description": "Cotton t-shirt, men, crew neck", "origin_country": "BD"}
]
}'
response = requests.post(
"https://customs8.ai/api/v1/classify/batch",
headers={"Authorization": "Bearer c8_your_api_key"},
json={
"items": [
{"description": "Wireless bluetooth headphones", "origin_country": "CN"},
{"description": "Stainless steel water bottle", "origin_country": "CN"},
{"description": "Cotton t-shirt, men, crew neck", "origin_country": "BD"}
]
}
)
batch_id = response.json()["data"]["batch_id"]
const response = await fetch('https://customs8.ai/api/v1/classify/batch', {
method: 'POST',
headers: {
'Authorization': 'Bearer c8_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: [
{ description: 'Wireless bluetooth headphones', origin_country: 'CN' },
{ description: 'Stainless steel water bottle', origin_country: 'CN' },
{ description: 'Cotton t-shirt, men, crew neck', origin_country: 'BD' },
]
}),
});
Response 202 Accepted
{
"success": true,
"data": {
"batch_id": "batch_abc123",
"total": 3,
"classification_ids": [
"550e8400-e29b-41d4-a716-446655440001",
"550e8400-e29b-41d4-a716-446655440002",
"550e8400-e29b-41d4-a716-446655440003"
]
},
"usage": {
"classifications_remaining": 47,
"classifications_used": 3,
"classifications_limit": 50,
"overage_allowed": false
}
}
/v1/classify/batch/{batch_id}
Check the status of a batch classification request. Returns the status of each individual classification in the batch.
Response 200 OK
{
"success": true,
"data": {
"batch_id": "batch_abc123",
"total": 3,
"completed": 2,
"pending": 1,
"failed": 0,
"classifications": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"status": "completed",
"hs_code": "8518.30.00"
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"status": "completed",
"hs_code": "7323.93.00"
},
{
"id": "550e8400-e29b-41d4-a716-446655440003",
"status": "pending",
"hs_code": null
}
]
},
"usage": {
"classifications_remaining": 44,
"classifications_used": 6,
"classifications_limit": 50,
"overage_allowed": false
}
}
/v1/classifications/{id}/feedback
Submit feedback on a classification result. Confirm the classification was correct or provide the correct HS code. This helps improve the system over time.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| status | string | required | confirmed or corrected |
| corrected_code | string | if corrected | The correct HS code (required when status is corrected) |
Example: Confirm
curl -X POST https://customs8.ai/api/v1/classifications/550e8400-.../feedback \
-H "Authorization: Bearer c8_your_api_key" \
-H "Content-Type: application/json" \
-d '{"status": "confirmed"}'
Example: Correct
curl -X POST https://customs8.ai/api/v1/classifications/550e8400-.../feedback \
-H "Authorization: Bearer c8_your_api_key" \
-H "Content-Type: application/json" \
-d '{"status": "corrected", "corrected_code": "8518.10.00"}'
/v1/usage
Get current usage statistics for your organization in the current billing period.
Response 200 OK
{
"success": true,
"data": {
"plan": {
"name": "Starter",
"display_name": "Starter"
},
"classifications": {
"current": 42,
"limit": 500,
"remaining": 458,
"percentage": 8
},
"period": {
"year": 2026,
"month": 3
}
}
}
/v1/me
Get account information for the authenticated API key, including organization details and current plan.
Response 200 OK
{
"success": true,
"data": {
"organization": {
"name": "Acme Trading Co.",
"email": "api@acmetrading.com"
},
"plan": {
"name": "Starter",
"display_name": "Starter",
"classifications_limit": 500
}
}
}
/health
Check the API status. This endpoint does not require authentication.
curl https://customs8.ai/api/health
# Response:
{
"success": true,
"data": {
"status": "operational"
}
}
/v1/duty-rates/{hs_code}
Retrieve duty rates for a 10-digit HS code. Returns MFN duties, preferential duties, anti-dumping duties, and VAT rate.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| origin | string | 2-letter ISO country code to filter preferential duties |
| destination | string | 2-letter ISO country code for VAT calculation (default: NL) |
Example
curl "https://customs8.ai/api/v1/duty-rates/8518300090?origin=CN&destination=NL" \
-H "Authorization: Bearer c8_your_api_key"
Response 200 OK
{
"success": true,
"data": {
"hs_code": "8518300090",
"description": "Headphones and earphones",
"mfn_duty": {
"rate": "2.00%",
"expression": "2.000 %"
},
"preferential_duties": [
{
"scheme": "GSP",
"rate": "0.00%",
"countries": ["CN", "VN", "BD"]
}
],
"anti_dumping": [],
"vat_rate": "21.00%"
}
}
/v1/landed-cost
Calculate total landed cost for importing a product. Combines CIF value, duties, anti-dumping duties, and VAT.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| hs_code | string | required | 10-digit HS commodity code |
| origin_country | string | required | 2-letter ISO country code |
| destination_country | string | optional | Default: NL |
| value | number | required | Product value (min: 0.01) |
| currency | string | optional | Default: EUR |
| shipping | number | optional | Shipping costs (default: 0) |
| insurance | number | optional | Insurance costs (default: 0) |
Response 200 OK
{
"success": true,
"data": {
"cif_value": 31.50,
"duty": {
"rate": "0.00%",
"amount": 0.00,
"type": "preferential"
},
"anti_dumping": {
"rate": "0.00%",
"amount": 0.00
},
"vat": {
"rate": "21.00%",
"amount": 6.62
},
"total_landed_cost": 38.12,
"currency": "EUR"
}
}
/v1/webhook
Configure a webhook URL to receive push notifications when classifications complete or fail. A signing secret is generated automatically.
Note: The secret is only returned when the webhook is created. Store it securely -- you will not be able to retrieve it again.
Example
curl -X PUT https://customs8.ai/api/v1/webhook \
-H "Authorization: Bearer c8_your_api_key" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/webhook"}'
Response 200 OK
{
"success": true,
"data": {
"url": "https://example.com/webhook",
"secret": "whsec_a1b2c3d4e5f6g7h8i9j0..."
}
}
/v1/webhook
Remove the configured webhook. You will stop receiving notifications.
curl -X DELETE https://customs8.ai/api/v1/webhook \
-H "Authorization: Bearer c8_your_api_key"
# Response:
{
"success": true,
"message": "Webhook removed."
}
Webhook Events
When a classification finishes processing, Customs8 sends a POST request to your webhook URL with the event type in the X-Customs8-Event header.
| Event | Description |
|---|---|
| classification.completed | Classification finished successfully |
| classification.failed | Classification failed due to an error |
Headers
| Header | Description |
|---|---|
| X-Customs8-Event | Event type (e.g. classification.completed) |
| X-Customs8-Signature | HMAC-SHA256 signature: sha256={hex_digest} |
| User-Agent | Customs8-Webhook/1.0 |
Example Payload
{
"event": "classification.completed",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"source": "api",
"hs_code": {
"code_6": "851830",
"code_8": "85183000",
"code_10": "8518300090",
"description": "Headphones and earphones",
"chapter": "85"
},
"confidence": 95,
"reasoning": "Wireless bluetooth headphones with noise cancellation...",
"product_summary": "Over-ear wireless bluetooth headphones",
"created_at": "2026-03-04T12:00:00+00:00",
"completed_at": "2026-03-04T12:00:30+00:00"
}
}
Webhook Verification
Verify the X-Customs8-Signature header to ensure requests are from Customs8. The signature is an HMAC-SHA256 hex digest of the raw request body, using your webhook secret as the key, prefixed with sha256=.
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
const crypto = require("crypto");
function verifyWebhook(payload, signature, secret) {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(payload).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
function verifyWebhook(string $payload, string $signature, string $secret): bool
{
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
return hash_equals($expected, $signature);
}
Rate Limits
Rate limits are applied per API key and depend on your plan. Every API response includes rate limit headers:
| Header | Description |
|---|---|
| X-RateLimit-Limit | Max requests per window |
| X-RateLimit-Remaining | Remaining requests in current window |
| X-RateLimit-Reset | Unix timestamp when window resets |
When exceeded, the API returns 429 Too Many Requests with a Retry-After header.
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
{
"success": false,
"message": "Rate limit exceeded. Please retry after 30 seconds."
}
Error Codes
The API uses standard HTTP status codes:
| Status | Meaning |
|---|---|
| 200 | Success |
| 202 | Accepted -- classification queued |
| 401 | Invalid or missing API key |
| 404 | Resource not found |
| 422 | Validation error -- check request body |
| 429 | Rate limit or usage limit exceeded |
| 500 | Server error |
Error Response Format
{
"success": false,
"message": "Description of the error"
}