Async Classification & Webhooks
TariffLens classifications are AI-powered and run asynchronously. Learn how to retrieve results using polling or webhooks, and best practices for production integrations.
How Classification Works
When you submit a product for classification, TariffLens runs a multi-step AI analysis to determine the correct HTS code. This process takes approximately 60 seconds on average.
The API is designed around an asynchronous pattern: you submit a classification request, receive an immediate acknowledgment (HTTP 202), and then retrieve the result once processing completes.
Every classification goes through these states:
pending → processing → completed
↘ failed- pending — Queued, waiting to be picked up
- processing — AI analysis in progress
- completed — Result available with HTS code and reasoning
- failed — Classification could not be completed (see error details)
Submitting a Classification
There are two entry points depending on whether the product already exists in TariffLens:
POST /api/v1/products/classify— creates the product and queues classification in one call.POST /api/v1/classifications— classifies an existing product (byproduct_id) under one or more schedules.
curl -X POST https://api.tarifflens.ai/api/v1/products/classify \
-H "Authorization: Bearer tlk_live_xxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-unique-key-123" \
-d '{
"product": {
"name": "Industrial Servo Motor",
"description": "Brushless AC servo motor, 2.5kW, 3000 RPM, with integrated encoder",
"identifier": "SKU-12345",
"country_of_origin": "DE"
},
"schedules": ["US-HTS"]
}'The API responds immediately with the product ID and one classification stub per requested schedule:
{
"product_id": "product_abc123xyz",
"product": {
"id": "product_abc123xyz",
"name": "Industrial Servo Motor",
"identifier": "SKU-12345",
"country_of_origin": "DE",
"created_at": "2026-01-08T10:30:00Z"
},
"classifications": [
{
"id": "clf_def456",
"status": "pending",
"schedule": "US-HTS",
"product_identifier": "SKU-12345",
"poll_url": "/api/v1/classifications/clf_def456"
}
],
"estimated_completion_seconds": 45
}For an existing product, send the product_id instead:
curl -X POST https://api.tarifflens.ai/api/v1/classifications \
-H "Authorization: Bearer tlk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"product_id": "product_abc123xyz",
"schedules": ["EU-CN"]
}'Retrieving Results
There are two strategies for getting classification results: polling and webhooks. Choose based on your integration needs.
Strategy 1: Polling
Poll the classification status endpoint until the status is completed or failed. This is the simplest approach and works well for scripts, CLIs, and low-volume integrations.
Recommended Polling Intervals
| Time Elapsed | Poll Interval |
|---|---|
| 0 – 30 seconds | Every 5 seconds |
| 30 – 120 seconds | Every 10 seconds |
| After 120 seconds | Every 30 seconds (timeout after 5 minutes) |
Polling with cURL
# Poll until status is "completed" or "failed"
while true; do
RESULT=$(curl -s https://api.tarifflens.ai/api/v1/classifications/clf_def456 \
-H "Authorization: Bearer tlk_live_xxxxx")
STATUS=$(echo "$RESULT" | jq -r '.status')
echo "Status: $STATUS"
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
echo "$RESULT" | jq .
break
fi
sleep 5
doneIf you classified against several schedules at once, you can also poll the product to get the status of every classification in one round-trip: GET /api/v1/products/{product_id}.
Polling with Node.js
async function pollClassification(id: string, apiKey: string) {
const baseUrl = "https://api.tarifflens.ai";
const maxWait = 5 * 60 * 1000; // 5 minute timeout
const start = Date.now();
while (Date.now() - start < maxWait) {
const res = await fetch(`${baseUrl}/api/v1/classifications/${id}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const data = await res.json();
if (data.status === "completed" || data.status === "failed") {
return data;
}
// Adaptive interval: 5s for first 30s, then 10s
const elapsed = Date.now() - start;
const delay = elapsed < 30_000 ? 5_000 : 10_000;
await new Promise((r) => setTimeout(r, delay));
}
throw new Error("Classification timed out");
}Completed Response
{
"id": "clf_def456",
"status": "completed",
"schedule": "US-HTS",
"created_at": "2026-01-08T10:30:00Z",
"completed_at": "2026-01-08T10:31:02Z",
"product": {
"name": "Industrial Servo Motor",
"description": "Brushless AC servo motor, 2.5kW, 3000 RPM, with integrated encoder",
"identifier": "SKU-12345",
"country_of_origin": "DE"
},
"result": {
"hts_code": "8501524000",
"hts_description": "AC motors, multi-phase: exceeding 750 W but not exceeding 75 kW",
"supporting_rulings": ["NY N123456", "HQ H987654"],
"reasoning": "Classified as AC servo motor exceeding 750W based on product specifications and CBP ruling precedent."
},
"usage": {
"credits_consumed": 1,
"processing_time_ms": 62000
}
}Strategy 2: Webhooks
Configure a webhook endpoint to receive a POST request when classifications complete. This is the recommended approach for production integrations — no polling required.
Option A: Organization-Wide Webhook
Configure a webhook endpoint that receives events for all classifications in your organization:
curl -X PUT https://api.tarifflens.ai/api/v1/webhooks \
-H "Authorization: Bearer tlk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/tarifflens",
"secret": "whsec_your_signing_secret",
"event_types": ["classification.completed"],
"enabled": true
}'Option B: Per-Request Webhook
Pass a webhook_url in the classification request to receive a callback for that specific classification. One classification.completed event fires per (product, schedule) pair:
curl -X POST https://api.tarifflens.ai/api/v1/products/classify \
-H "Authorization: Bearer tlk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"product": {
"name": "Industrial Servo Motor",
"description": "Brushless AC servo motor, 2.5kW"
},
"schedules": ["US-HTS"],
"options": {
"webhook_url": "https://your-app.com/webhooks/tarifflens"
}
}'Webhook Payload
When a classification completes, TariffLens sends a POST request to your endpoint with the following headers and body:
Content-Type: application/json
X-TariffLens-Event: classification.completed
X-TariffLens-Delivery-ID: dlv_abc123 # Unique per delivery (use for idempotency)
X-Webhook-Secret: whsec_your_signing_secret # If configured{
"event": "classification.completed",
"timestamp": "2026-01-08T10:31:02Z",
"data": {
"id": "clf_def456",
"status": "completed",
"schedule": "US-HTS",
"product": {
"name": "Industrial Servo Motor",
"identifier": "SKU-12345"
},
"result": {
"hts_code": "8501524000",
"hts_description": "AC motors, multi-phase: exceeding 750 W but not exceeding 75 kW",
"supporting_rulings": ["NY N123456", "HQ H987654"],
"reasoning": "Classified as AC servo motor exceeding 750W based on product specifications and CBP ruling precedent."
}
}
}The schedule field identifies which tariff schedule this classification ran under — relevant when you submit the same product against multiple schedules in one call.
Webhook Handler Example
import express from "express";
const app = express();
app.use(express.json());
app.post("/webhooks/tarifflens", (req, res) => {
const event = req.headers["x-tarifflens-event"];
const deliveryId = req.headers["x-tarifflens-delivery-id"];
const secret = req.headers["x-webhook-secret"];
// Verify the secret matches your configured value
if (secret !== process.env.WEBHOOK_SECRET) {
return res.status(401).send("Invalid secret");
}
// Deduplicate using delivery ID
// (store processed delivery IDs to handle retries)
const { data } = req.body;
if (event === "classification.completed") {
console.log(`Classification ${data.id} completed:`);
console.log(` HTS: ${data.result.hts_code}`);
console.log(` Reasoning: ${data.result.reasoning}`);
console.log(` Product: ${data.product.identifier}`);
// Update your database, notify users, etc.
}
// Respond quickly with 2xx — process asynchronously if needed
res.status(200).send("OK");
});Retry Policy
If your endpoint returns a non-2xx status or is unreachable, TariffLens retries delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry (final) | 30 minutes |
After 3 failed attempts, the delivery is marked as failed. You can use the GET /api/v1/classifications/{id} endpoint as a fallback to retrieve results.
Polling vs. Webhooks
| Factor | Polling | Webhooks |
|---|---|---|
| Complexity | Simple — no server needed | Moderate — requires a public HTTPS endpoint |
| Latency | Depends on poll interval (5-30s delay) | Near real-time |
| Volume | Good for low volume | Better for high volume |
| Reliability | Client-driven, always available | Server pushes with 3 retries |
| Best for | Scripts, CLIs, testing | Production apps, ERP integrations |
You can use both strategies together. Configure webhooks as your primary notification mechanism, and fall back to polling if a webhook delivery fails or for ad-hoc lookups.
Batch Classifications
Submit up to 100 products in a single request using the POST /api/v1/classifications/bulk endpoint. The same schedules list is applied to every product — total classifications run is products.length × schedules.length, and credits consumed match that total.
curl -X POST https://api.tarifflens.ai/api/v1/classifications/bulk \
-H "Authorization: Bearer tlk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"products": [
{ "name": "Servo Motor", "identifier": "SKU-001" },
{ "name": "Steel Pipe", "identifier": "SKU-002" },
{ "name": "Cotton T-Shirt", "identifier": "SKU-003" }
],
"schedules": ["US-HTS"],
"options": {
"webhook_url": "https://your-app.com/webhooks/tarifflens"
}
}'Monitoring Batch Progress
Poll the batch status endpoint to see progress across all products and schedules:
curl https://api.tarifflens.ai/api/v1/classifications/bulk/bat_xyz789 \
-H "Authorization: Bearer tlk_live_xxxxx"{
"batch_id": "bat_xyz789",
"status": "processing",
"summary": {
"total": 3,
"completed": 1,
"failed": 0,
"pending": 2
},
"classifications": [
{
"id": "clf_001",
"status": "completed",
"schedule": "US-HTS",
"product_identifier": "SKU-001",
"result": { "hts_code": "8501524000", "hts_description": "AC motors, multi-phase: exceeding 750 W but not exceeding 75 kW", "supporting_rulings": ["NY N123456"], "reasoning": "Classified as AC servo motor exceeding 750W." }
},
{
"id": "clf_002",
"status": "processing",
"schedule": "US-HTS",
"product_identifier": "SKU-002"
},
{
"id": "clf_003",
"status": "pending",
"schedule": "US-HTS",
"product_identifier": "SKU-003"
}
]
}When every classification in the batch finishes, a batch.completed webhook event is sent if you configured a webhook URL. Each item in the payload carries its own schedule.
Each classification takes ~60 seconds. Classifications in a batch are processed with some parallelism, so a 10-product × 1-schedule batch typically completes faster than 10 minutes. Use estimated_completion_seconds in the response for guidance.
Best Practices
Use idempotency keys
Include an Idempotency-Key header on classification requests. If a request is retried (e.g., due to a network error), the same key ensures you don't create duplicate classifications.
Include product identifiers
Set product.identifier to your internal SKU or part number. This value is returned in webhook payloads and status responses, making it easy to match results back to your records.
Provide detailed descriptions
The more detail you provide in product.description, the higher the classification accuracy. Include materials, dimensions, use case, and technical specifications when available.
Respect rate limits
The default rate limit is 100 requests per minute. Check the X-RateLimit-Remaining header and back off when approaching the limit. If you receive a 429 response, wait for the number of seconds specified in the Retry-After header before retrying.
Handle webhooks quickly
Your webhook endpoint should respond with a 2xx status within a few seconds. If you need to do heavy processing (e.g., updating a database, triggering downstream workflows), acknowledge the webhook first and process asynchronously.
Store classification IDs
Persist the id returned when you create a classification. You can always retrieve the full result later via GET /api/v1/classifications/{id}, even after webhook delivery.
Error Handling
API Errors
All errors follow a consistent format with a machine-readable code and human-readable message:
{
"error": {
"code": "INVALID_REQUEST",
"message": "Product name is required",
"request_id": "req_abc123",
"details": [
{ "field": "product.name", "message": "Product name is required" }
]
}
}| Code | HTTP Status | Description |
|---|---|---|
INVALID_REQUEST | 400 | Malformed request body or missing required fields |
UNAUTHORIZED | 401 | Invalid or missing API key |
NOT_FOUND | 404 | Classification or resource not found |
RATE_LIMIT_EXCEEDED | 429 | Too many requests — check Retry-After header |
INTERNAL_ERROR | 500 | Server error — safe to retry with backoff |
Classification Failures
If a classification fails, the status becomes failed with error details in the response:
{
"id": "clf_def456",
"status": "failed",
"schedule": "US-HTS",
"created_at": "2026-01-08T10:30:00Z",
"product": {
"name": "Unknown Object"
},
"error": {
"code": "CLASSIFICATION_ERROR",
"message": "Unable to classify product with sufficient confidence"
}
}Classification failures consume credits. If the failure was due to insufficient product information, try resubmitting with a more detailed description.