TariffLens

Docs

Getting Started

Guides

API Reference

TariffLens

Docs

Guide

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.

Status Lifecycle

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 (by product_id) under one or more schedules.
bash — create + classify
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:

json — 202 Accepted
{
  "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:

bash — classify existing product
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 ElapsedPoll Interval
0 – 30 secondsEvery 5 seconds
30 – 120 secondsEvery 10 seconds
After 120 secondsEvery 30 seconds (timeout after 5 minutes)

Polling with cURL

bash
# 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
done

If 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

typescript
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

json — 200 OK
{
  "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:

bash
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:

bash
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:

Headers
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
json — POST body
{
  "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

typescript — Express
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:

AttemptDelay
1st retry1 minute
2nd retry5 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

FactorPollingWebhooks
ComplexitySimple — no server neededModerate — requires a public HTTPS endpoint
LatencyDepends on poll interval (5-30s delay)Near real-time
VolumeGood for low volumeBetter for high volume
ReliabilityClient-driven, always availableServer pushes with 3 retries
Best forScripts, CLIs, testingProduction apps, ERP integrations
Tip

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.

bash
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:

bash
curl https://api.tarifflens.ai/api/v1/classifications/bulk/bat_xyz789 \
  -H "Authorization: Bearer tlk_live_xxxxx"
json — 200 OK
{
  "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.

Batch Timing

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:

json — Error Response
{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "Product name is required",
    "request_id": "req_abc123",
    "details": [
      { "field": "product.name", "message": "Product name is required" }
    ]
  }
}
CodeHTTP StatusDescription
INVALID_REQUEST400Malformed request body or missing required fields
UNAUTHORIZED401Invalid or missing API key
NOT_FOUND404Classification or resource not found
RATE_LIMIT_EXCEEDED429Too many requests — check Retry-After header
INTERNAL_ERROR500Server error — safe to retry with backoff

Classification Failures

If a classification fails, the status becomes failed with error details in the response:

json
{
  "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.

Next Steps