API Documentation

Integrate HS code classification into your applications with the Customs8 REST API.

Base URL 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)
POST

/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
  }
}
GET

/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
  }
}
GET

/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
  }
}
POST

/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
  }
}
GET

/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
  }
}
POST

/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"}'
GET

/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
    }
  }
}
GET

/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
    }
  }
}
GET

/health

Check the API status. This endpoint does not require authentication.

curl https://customs8.ai/api/health

# Response:
{
  "success": true,
  "data": {
    "status": "operational"
  }
}
GET

/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%"
  }
}
POST

/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"
  }
}
PUT

/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..."
  }
}
DELETE

/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"
}
Customs8

Welcome back

Log in to your account

Forgot password?

Don't have an account?

Customs8

Create your account

Start with 50 free classifications

Already have an account?