TarriAPI

API Reference · v1

Tarri API

Send a product's name, listing copy and photo; get back the 10-digit HTS line, the full landed-duty stack as CBP collects it this month, the CBP rulings behind the pick, and a confidence score that says when to trust it. One REST call, or a batch of 200.

Introduction

The API is JSON over HTTPS. Two endpoints are metered (POST /classify and POST /classify/batch: one credit per product). Everything else is free and only rate-limited: HTS line lookups with the current program rates for an origin, duty math for a code you already hold, CBP CROSS rulings, and your own usage.

Base URLhttps://app.tarri.ai/api/v1
AuthAuthorization: Bearer <key>
Content typeapplication/json

The duty stack it returns is the one in force since 24 July 2026: MFN, China-list Section 301 (Lists 1–4A and the four-year review, with the 178 live exclusions flagged), Section 232 at 25% (Annex I-B) or 50% (Annex I-A) of full FOB on steel, aluminum and copper articles, the forced-labor Section 301 tier by origin (12.5% or 10%, skipped on 232 articles and Annex II exemptions), and MPF and HMF. Section 122 expired and defaults to 0.

Authentication & keys

Every request except GET /plans and GET /openapi.json carries your secret key as a bearer token:

Authorization: Bearer tarri_sk_live_…
  • Create keys in the app under Settings → API. A workspace holds up to 10 live keys; name one per integration ("Shopify sync", "PIM import") so you can rotate them independently.
  • Shown once. Only a SHA-256 hash is stored. Copy the key when it appears; if it is lost, revoke it and create another.
  • Revoke a key and requests with it fail with 401 invalid_api_key immediately. Revoked keys stay listed for your records.
  • Server-side only. Never ship a key in a browser, a mobile app, or a public repository. Store it as TARRI_API_KEY.
Treat keys like passwords. Anyone holding a key can spend your credits and, on plans with overage, your money up to the spend cap.

Plans & credits

One credit is one classification, with or without a photo. Lookups, duty math and rulings never cost credits. Unused credits roll over up to 3× your monthly allowance, so an onboarding month can draw on the quiet months around it.

PlanPriceCredits / monthOverageFits
Starter $49/mo 500 $0.12 per classification Small importers and marketplace sellers classifying a catalog and its monthly additions.
Growth $199/mo 2,000 $0.10 per classification Brands and sourcing platforms with thousands of SKUs and steady new-product flow.
Scale $699/mo 8,000 $0.09 per classification Platforms, marketplaces, 3PLs and brokers classifying for many clients.
Enterprise Custom 50,000+ Negotiated Volume contracts from 50,000 classifications a month with an SLA and invoicing.

When the balance is spent, plans with overage keep answering at the per-credit price, capped by the spend cap you set in Settings → API (a cap of $0 stops at the allowance). Overage is invoiced with your next cycle. Every classify response and GET /usage report credits_remaining, so your integration can warn before it hits 402.

The app's Team plan includes 500 classifications a month through the API without an API plan (Enterprise app plans are not metered); add an API plan for rollover, overage, batch and webhooks.

Quickstart

Set TARRI_API_KEY in your environment and classify one product:

curl -X POST https://app.tarri.ai/api/v1/classify \
  -H "Authorization: Bearer $TARRI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sku-48213" \
  -d '{
    "name": "Hybrid Steel Fry Pan",
    "description": "Stainless steel fry pan with ceramic nonstick interior, 10 inch",
    "origin": "CN",
    "fob": 55650,
    "units": 5000
  }'
const res = await fetch("https://app.tarri.ai/api/v1/classify", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.TARRI_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "sku-48213",
  },
  body: JSON.stringify({
    name: "Hybrid Steel Fry Pan",
    description: "Stainless steel fry pan with ceramic nonstick interior, 10 inch",
    origin: "CN",
    fob: 55650,
    units: 5000,
  }),
});

if (res.status === 429) { /* back off for res.headers.get("Retry-After") seconds */ }
if (!res.ok) throw new Error((await res.json()).detail.message);
const result = await res.json();
console.log(result.best.htsNumber, result.best.confidence, result.duty.totalKnownDuty);
import os, requests

res = requests.post(
    "https://app.tarri.ai/api/v1/classify",
    headers={
        "Authorization": f"Bearer {os.environ['TARRI_API_KEY']}",
        "Idempotency-Key": "sku-48213",
    },
    json={
        "name": "Hybrid Steel Fry Pan",
        "description": "Stainless steel fry pan with ceramic nonstick interior, 10 inch",
        "origin": "CN",
        "fob": 55650,
        "units": 5000,
    },
    timeout=60,
)
res.raise_for_status()
result = res.json()
print(result["best"]["htsNumber"], result["best"]["confidence"], result["duty"]["totalKnownDuty"])

Products the deterministic rules recognize answer in about 3 seconds; products that need the vision and reranking models take 10–30 seconds. Set client timeouts to 60 seconds, or use the batch endpoint for catalogs.

Idempotency & retries

Network calls fail and jobs get re-run. Send Idempotency-Key on POST /classify (your SKU id works well) and Tarri returns the original response for 24 hours without consuming another credit, marked with the header Idempotent-Replayed: true. Reusing a key with a different body returns 409 idempotency_key_reused. The key is claimed before any credit moves, so two concurrent requests with the same key never both classify: the second receives 409 idempotency_in_progress until the first finishes.

Safe retry rule: on 429, 502 and 503, retry with exponential backoff and the same Idempotency-Key. 502 classification_failed never consumes a credit, and a failed request releases its Idempotency-Key so the retry goes through.

Send X-Request-Id with your own correlation id; it is echoed on every response (and generated when absent). Quote it when you write to support.

Rate limits

The same limits apply to every plan, per key: 60 single classifications per minute, 6 batch requests per minute (up to 200 items each, worked by our queue at its own pace), and 600 free requests per minute. A 429 rate_limited carries Retry-After in seconds. For a catalog, batch is the high-throughput path: one request queues up to 200 products.

Errors

Every error shares one shape. error is the machine-readable code; detail.message is safe to surface to an operator; detail.doc_url links here.

{
  "error": "spend_cap_reached",
  "credits_remaining": 0,
  "detail": {
    "code": "spend_cap_reached",
    "message": "Your overage spend cap for this period is reached. Raise it in Settings → API or wait for the next cycle.",
    "doc_url": "https://tarri.ai/docs/errors#spend_cap_reached"
  },
  "request_id": "6c1f…"
}
StatusCodeMeaning
400invalid_hts_code / invalid_batch_item / validationThe body or path failed validation. Batch: the failing index is included and nothing was queued.
400webhook_secret_requiredThe batch named a webhook_url but the workspace has no signing secret yet. Generate one in Settings → API.
401missing_api_key / invalid_api_keyNo bearer header, or the key is unknown or revoked.
402api_plan_requiredThe workspace has no active API plan.
402credits_exhaustedNo credits left and the plan has no overage.
402spend_cap_reachedThe overage spend cap for this period is reached; raise it in Settings → API.
402payment_past_dueIncluded credits are spent and the last invoice is unpaid. Granted credits still work; overage resumes once the payment method is updated.
404hts_not_found / ruling_not_found / batch_not_foundNothing matches.
409idempotency_key_reusedThe same Idempotency-Key arrived with a different body.
409idempotency_in_progressThe original request with this Idempotency-Key is still running. Retry in a few seconds to receive its result.
429rate_limitedPer-key limit exceeded. Honor Retry-After.
502classification_failedThe engine failed. The credit was not consumed; retry with backoff.
503database_unavailable / batch_unavailableTemporary outage; retry with backoff. A batch that could not be queued consumed no credits.

Versioning

The path carries the major version (/api/v1). Additive changes (new fields, new endpoints, new duty line kinds) ship without notice; read objects by field name and ignore what you do not know. Breaking changes get a new major version with at least 90 days of overlap. Tariff data changes (new HTS revisions, new Chapter 99 programs) are not API changes; they flow through the same fields.

POST /classify

Classify one product. Consumes one credit. Returns the pick, ranked alternates, the duty breakdown when fob is sent, and supporting rulings.

FieldTypeRequiredDescription
namestringConditionalShort product name, e.g. Hybrid Steel Fry Pan. At least one of name, description, or imageUrl is required.
descriptionstringConditionalWhat it is, what it is made of, how it is used. Material words matter: "enameled cast iron" and "stainless" land on different lines.
imageUrlstringConditionalA data:image/…;base64, URL (recommended, up to ~10 MB) or an https URL on an allow-listed host. A vision model describes the photo and the description confirms material and form.
originstringOptionalISO 3166-1 alpha-2 country of origin. Defaults to CN. Drives China-list Section 301 (CN/HK only), Section 232, and the forced-labor Section 301 tier for the 60 investigated economies.
fobnumberOptionalCustoms value in USD. Needed for the dollar duty breakdown and for value-banded HTS lines ("valued not over $3 each").
unitsnumberOptionalPieces in the shipment or set. With fob, gives the per-unit value that picks value bands.
netWeightKgnumberOptionalNet weight in kilograms, for per-kilogram specific rates.
section232AppliesbooleanOptionalForce Section 232 on or off. Omit to follow the HTS row: Annex I-A pays 50% and Annex I-B 25% of full FOB.
oceanShipmentbooleanOptionalAdds the Harbor Maintenance Fee. Defaults to true.
settingsobjectOptionalOverride engine rates: section232PrimaryRate, section232DerivativeRate, forcedLaborRate, forcedLaborReducedRate, section122Rate, mpfRate, mpfMin, mpfMax, hmfRate.
Send the photo. With a photo, the material and form are confirmed by what is in frame: an "enamel Dutch oven" that is visibly cast iron moves from 7323.94 to 7323.92. Send fob and units too; glassware and several other headings split on per-unit value, and without it Tarri assumes the lowest band at reduced confidence.

The response object

FieldTypeDescription
bestCandidate | nullThe pick, or null when Tarri abstains.
candidatesCandidate[]Ranked; includes best. Alternates that share the pick's heading are material or value-band variants worth a look; alternates at 0.5 are retrieval filler.
abstainedbooleantrue when no candidate cleared the confidence threshold (0.62). Review candidates; do not auto-accept.
abstainReasonstring | nullWhy Tarri abstained.
imageDescriptionstring | nullWhat the vision model saw, when you sent a photo.
dutyDuty | nullThe landed-duty breakdown for best. null without fob.
rulingsRuling[]CBP CROSS rulings that support the pick.
perUnitValuenumber | nullfob ÷ units when both were sent.
pipelineobjectDiagnostics: directCodeMatch, headings[], embeddingUsed, aiRerankUsed, rulingsUsed.
creditsobject{ consumed, overage, remaining } for this call.
request_idstringEcho of X-Request-Id, or a generated id. Quote it to support.

Example (abridged):

{
  "best": {
    "htsNumber": "7323.93.00.45",
    "htsDigits": "7323930045",
    "item": "Table, kitchen or other household articles … > Of stainless steel > Cooking ware: > Other",
    "confidence": 0.86,
    "rationale": "Deterministic named-cookware match: … (stainless steel is 7323.93).",
    "generalDuty": "2%", "generalDutyRate": 2,
    "section301Duty": "0.00%", "section301Rate": 0,
    "section301ExclusionScope": "",
    "section232Annex": "I-B", "section232Category": "steel-derivative",
    "forcedLaborExempt": false
  },
  "candidates": [ /* best plus 7323.99.70.00 (carbon steel) and 7323.94.00.26 (enameled) */ ],
  "abstained": false,
  "duty": {
    "fob": 55650,
    "lines": [
      { "kind": "standard",    "rate": 2,    "amount": 1113,     "basis": "FOB" },
      { "kind": "section301",  "rate": 0,    "amount": 0 },
      { "kind": "section232",  "rate": 25,   "amount": 13912.5,  "basis": "Full FOB", "note": "Annex I-B (steel derivative), Proclamation 11032." },
      { "kind": "forcedLabor", "rate": 0,    "amount": 0,        "note": "Skipped: Section 232 articles are exempt (U.S. note 52(f))." },
      { "kind": "mpf",         "rate": 0.3464, "amount": 192.772 },
      { "kind": "hmf",         "rate": 0.125,  "amount": 69.563 }
    ],
    "totalKnownDuty": 15287.835,
    "landedCostKnown": 70937.835,
    "effectiveKnownRate": 0.2747,
    "manualReview": false
  },
  "rulings": [ { "rulingNumber": "N326219", "subject": "…", "url": "https://rulings.cbp.gov/ruling/N326219" } ],
  "credits": { "consumed": 1, "overage": false, "remaining": 499 },
  "request_id": "6c1f…"
}

Reading confidence

Treat 0.80 and up as ready to file. 0.62 to 0.79 is right at the heading with a suffix or fact to confirm; the rationale names it (unstated metal, assumed value band, a photo that disagrees with the title). Below 0.62 Tarri abstains: best is null, candidates holds the closest lines, and a person should choose. Retail sets with no single essential character (a picture-hanging kit, a hardware assortment) are the usual abstentions.

The candidate object

FieldTypeDescription
htsNumberstringFormatted 10-digit code, e.g. "7323.93.00.45".
htsDigitsstringDigits only, e.g. "7323930045".
itemstringFull HTS line description, heading to statistical suffix, joined by " > ".
confidencenumberCalibrated, 0 to 1. 0.80 and up is high; 0.62 is the abstention threshold; 0.72 with a note means "confirm one fact" (unstated material, assumed value band).
rationalestringWhy this line: the rule or the reranker's reasoning, with CBP ruling numbers when they apply.
generalDuty / generalDutyRatestring / number | nullColumn 1 MFN rate. null rate means specific or compound (needs review).
section301Duty / section301Ratestring / number | nullChina-list Section 301 (Lists 1–4A, four-year review). Applies to CN/HK origin only. This is not the forced-labor 12.5%.
section301ExclusionScope"" | "full" | "partial"A live 9903.88.69/.70 exclusion covers the line (through 9 November 2026). full zeroes the list rate when claimed on entry; partial covers only goods matching the note description.
section232Annex"none" | "I-A" | "I-B"I-A pays 50%, I-B 25% of full FOB. section232Category says steel, aluminum or copper.
forcedLaborExemptbooleanOn Annex II Part A of the forced-labor action: no forced-labor duty for any origin. forcedLaborExemptScope flags conditional scopes (Ex, Aircraft, Pharma).

The duty object

Present when you send a positive fob. Each line carries its percent rate and its dollar amount, so you can show the stack or just the total.

FieldTypeDescription
linesDutyLine[]{ kind, label, basis, rate, amount, reviewRequired, note?, missing? }. Kinds: standard, section301, section232 (only when the row is on an annex), forcedLabor (0 with a note on any 232 article), section122 (only when you set a non-zero rate), mpf, hmf.
totalKnownDutynumberUSD sum of computable lines.
landedCostKnownnumberFOB plus total known duty.
effectiveKnownRatenumberTotal known duty ÷ FOB as a fraction (0.275 = 27.5%).
manualReviewbooleanA line needs a human: a specific or compound rate, or a net-of-MFN forced-labor tier with a specific MFN.
missingInputsstring[]What would let a line compute, e.g. ["quantity (units)"].

POST /classify/batch

Up to 200 products in one request. Every item is validated first (a bad item rejects the whole batch with its index), credits are consumed up front, and the batch is queued. When it settles, credits for failed items come back, and any overage is billed only for the items that succeeded. Poll GET /batches/{id} or pass webhook_url to be called when it completes. Items run in parallel on our side, so a 200-item catalog finishes in minutes, not hours.

curl -X POST https://app.tarri.ai/api/v1/classify/batch \
  -H "Authorization: Bearer $TARRI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://example.com/hooks/tarri",
    "items": [
      { "id": "SKU-1", "name": "Copper Moscow Mule Mugs", "description": "16oz solid copper mugs", "origin": "CN", "fob": 8.2, "units": 1 },
      { "id": "SKU-2", "name": "Digital Meat Thermometer", "description": "Fold-out probe, bright display", "origin": "CN" }
    ]
  }'

# → 202 { "id": "…", "status": "queued", "item_count": 2, "poll_url": "/api/v1/batches/…" }
curl https://app.tarri.ai/api/v1/batches/$BATCH_ID -H "Authorization: Bearer $TARRI_API_KEY"

Each item accepts the same fields as /classify plus an optional id that is echoed back so you can join results to your rows. Batch status is queued, processing, completed or failed (every item failed); per-item status is queued, completed or failed with an error string.

GET /hts/{code}

Free. Look up an HTS line by 4 to 10 digits (dots optional) and get its program rates for an origin: section301EffectiveRate, section232Rate and annex, forcedLaborRate, section122Rate. This is how you re-price a catalog after a tariff change without re-classifying it: codes do not change when rates do.

curl "https://app.tarri.ai/api/v1/hts/7323.93.00.45?origin=CN" -H "Authorization: Bearer $TARRI_API_KEY"

# → { "htsNumber": "7323.93.00.45", "generalDutyRate": 2, "section301Rate": 0,
#     "section232Annex": "I-B", "section232Rate": 25, "forcedLaborRate": 0, "section122Rate": 0, … }

POST /duty

Free. The duty breakdown for a code you already hold: { "htsDigits", "fob", "origin", "units", "netWeightKg", "section232Applies", "oceanShipment", "settings" }. Returns { line, duty } with the same duty object as classify.

GET /rulings

Free. CBP CROSS rulings by code (an HTS prefix) or free-text q, up to limit (default 24). GET /rulings/{number} returns one ruling with its text. Rulings are fact-specific precedent, useful when a broker asks "why this line".

GET /me · /usage · /plans

/me returns the workspace's plan, live keys and limits. /usage returns credits_remaining, the current period, overage so far and a 30-day daily series, which is what you want on an internal dashboard. /plans is public and returns the plan catalog above as JSON.

Webhooks

Add webhook_url to a batch request and Tarri POSTs { "type": "batch.completed", "batch_id", "url", "sent_at" } when the batch finishes. Requests carry Tarri-Signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with the signing secret from Settings → API (generate it first; a batch naming a webhook_url without one is refused with webhook_secret_required). Verify the signature and reject timestamps older than five minutes:

import crypto from "node:crypto";

export function verifyTarriSignature(header, rawBody, secret) {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Delivery is attempted once with a 10-second timeout. Your handler should fetch the batch by id rather than trust the payload, so a missed delivery costs nothing more than a poll.

Integration recipes

  • Catalog onboarding. Send products in batches of 200 with the SKU as id and Idempotency-Key per item on retries. Store htsDigits, confidence, rationale and the top ruling number. Route anything under 0.80 to a review queue.
  • New products. Classify on creation, with the photo. Show the rationale and the rulings next to the code so the person approving it sees the reasoning.
  • Tariff changes. Do not re-classify. Call GET /hts/{code}?origin= for each stored code and rebuild landed cost from the returned rates; it is free.
  • Landed-cost quotes. POST /duty with the stored code, the quote's FOB and units, and the buyer's origin. The response lines map directly to a quote's line items.
  • Spend control. Set a spend cap, read credits.remaining on every response, and alert your team at 10% left.

Interactive reference

The full OpenAPI 3.1 description with every schema, rendered below. Import the spec into Postman, Insomnia, or a client generator.

Support

Email support@tarri.ai with the request_id and we will look at the exact run. Enterprise plans (50,000+ classifications a month, SLA, invoicing) start with the same address.