Tarifflo
  • Pricing
  • Refunds
  • IEEPA Refunds
Sign In

API version

v2Currentv1Deprecated

Get started

  • Overview
  • Authentication
  • Errors
  • Rate limits

Build

  • Webhooks
  • MCP connector

Help

  • Request a feature

Build

Webhooks

When asynchronous work finishes, Tarifflo POSTs the result to your organization's webhook endpoint.

Where results go

Set one endpoint per environment in Developers → Webhooks. Events from work a tflo_test_ key started go to the test endpoint, and events from a tflo_live_ key will go to the live endpoint once live keys are issued. To send one request's events elsewhere, set the Tarifflo-Webhook-Url header, or the webhookUrl body field where an endpoint takes one. The URL must be public HTTPS. Private and internal addresses are refused.

Without an endpoint, poll the job's GET endpoint, and every poll counts as a request. CBP answers have no GET: they are sent only by webhook. A request that sends to CBP with no endpoint for its key's environment is refused with 412.

Events

EventSent when
classification.completedA classification request finished
classification_batch.completedA bulk classification batch finished
tariff_rate.completedA tariff-rate lookup finished
pre_audit.completedAn audit intake run finished
export_control.completedAn export-control (ECCN) determination finished
origin.completedA rules-of-origin determination finished
entry.dispositionCBP answered an entry filed with POST /api/v2/entries
cbp_query.answeredCBP answered a query you sent (entry summary, importer, bond, exchange rate)
cbp_query.failedCBP rejected a query you sent, or never answered it
webhook.testSent when you save or test an endpoint

Payload

Every delivery has the same envelope. data is the same object the job's GET endpoint returns once it is complete. A CBP answer (entry.disposition, cbp_query.answered, cbp_query.failed) to a request an API key sent also carries livemode: false for a test key, which sends to CBP certification, and true for a live key (production CBP, not issued yet).

Saving a new endpoint (or pressing Send test) in the console sends it one signed webhook.test event whose data is { "environment": "test", "message": "Tarifflo webhook test" }. Answer it with a 2xx within 10 seconds; the console shows whether it did. It is not retried.

Envelope
json
{
  "event": "tariff_rate.completed",
  "timestamp": "2026-10-23T14:23:45.123Z",
  "data": { "results": [ ... ] }
}
entry.disposition data
json
{
  "efid": "0193c1f2-...",
  "entryNumber": 1234567,
  "status": "APPROVED",
  "numActionItems": 0
}

Headers

X-Tarifflo-Event names the event. X-Tarifflo-Signature carries a timestamp and an HMAC-SHA256 of <timestamp>.<raw body>, keyed with your organization's webhook secret. Find the secret in the dashboard under Developers → Webhooks.

Request headers
http
Content-Type: application/json
X-Tarifflo-Event: tariff_rate.completed
X-Tarifflo-Signature: t=1761229425,sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Verify the signature

Recompute the HMAC over the timestamp, a dot and the raw body, compare it in constant time, and reject timestamps older than five minutes so a captured delivery cannot be replayed.

Node.js
javascript
import crypto from "node:crypto";

// rawBody must be the exact bytes received, not re-serialized JSON.
export function verifyTarifflo(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
    const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
    const timestamp = Number(parts.t);
    if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

    const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
    const a = Buffer.from(expected);
    const b = Buffer.from(parts.sha256 ?? "");
    return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python
python
import hashlib, hmac, time

def verify_tarifflo(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    ts = parts.get("t", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
        return False
    signed = ts.encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("sha256", ""))

Retries

Answer with any 2xx within 10 seconds. A timeout, a network error, a 5xx, a 408 or a 429 is retried; any other 4xx is treated as a refusal and not retried. Deliveries can arrive more than once, so make your handler idempotent.

Tarifflo

AI-driven trade automation for modern importers and brokers.

Get started free

Platform

  • Audit
  • ABI Filing
  • PGA
  • Suppliers

Customs

  • HTS Classification
  • Schedule B
  • ECCN
  • Tariff Rates
  • Customs Clearance

Developers

  • Docs
  • API reference
  • Webhooks
  • MCP Connector

Product

  • Pricing
  • Refunds
  • IEEPA Refunds
  • Explore data

Company

  • FAQ
  • Contact Us
  • Book a Demo

© 2026 Tarifflo, Inc. All rights reserved.

Privacy PolicyTerms & Conditions