{"openapi":"3.1.0","info":{"title":"Tarri API","version":"1","summary":"HTS classification and U.S. landed-duty math for product catalogs.","description":"Classify a product from its name, listing copy and photo to a 10-digit HTS line, with the complete September 2026 duty stack: MFN, China-list Section 301, Section 232 (25% / 50% of full FOB by annex), the forced-labor Section 301 tier by origin, MPF and HMF, plus the CBP CROSS rulings that support the pick.\n\n## Authentication\nEvery request (except `/plans` and this document) sends `Authorization: Bearer tarri_sk_live_…`. Create and revoke keys in the app under Settings → API. A workspace holds up to 10 live keys; name one per integration so you can rotate them independently.\n\n## Credits and plans\nOne credit is one classification, with or without a photo. HTS lookups, duty math, rulings, usage and account endpoints are free. Unused credits roll over up to 3× the monthly allowance. Once the balance is spent, plans with overage keep working at the per-credit price up to the spend cap you set; the overage lands on your next Stripe invoice.\n\n| Plan | Price | Credits / month | Overage |\n|---|---|---|---|\n| Starter | $49/mo | 500 | $0.12/credit |\n| Growth | $199/mo | 2,000 | $0.10/credit |\n| Scale | $699/mo | 8,000 | $0.09/credit |\n| Enterprise | custom | 50,000+ | negotiated |\n\n## Limits\n60 classifications per minute per key, 6 batch requests per minute per key (up to 200 items each, worked by a queue), 600 free requests per minute per key. A 429 carries `Retry-After`.\n\n## Idempotency\nSend `Idempotency-Key: <unique string>` on `POST /classify` to make retries safe: for 24 hours the same key returns the original response (header `Idempotent-Replayed: true`) without consuming another credit. The key is claimed before any credit moves, so two concurrent requests with the same key never both classify; the second gets `409 idempotency_in_progress` until the first finishes. Reusing a key with a different body returns 409. A request that failed (402, 502) releases the key so the retry can proceed.\n\n## Errors\nAll errors share one JSON shape: `{ \"error\": \"<code>\", \"detail\": { \"code\", \"message\", \"doc_url\" }, \"request_id\" }`. Quote `request_id` when you contact support.\n\n- `missing_api_key` (401): No `Authorization: Bearer` header.\n- `invalid_api_key` (401): The key is unknown or has been revoked.\n- `api_plan_required` (402): The workspace has no active API plan.\n- `credits_exhausted` (402): No credits left and the plan has no overage.\n- `spend_cap_reached` (402): The overage spend cap for this period is reached.\n- `payment_past_due` (402): Included credits are spent and the last invoice is unpaid; no overage until the payment method is updated.\n- `invalid_hts_code` (400): The HTS code did not parse to 4 to 10 digits.\n- `invalid_batch_item` (400): One item in a batch failed validation; the whole batch is rejected.\n- `webhook_secret_required` (400): A batch named a webhook_url but the workspace has no signing secret yet (Settings → API).\n- `hts_not_found` (404): No active HTS line matches the code.\n- `ruling_not_found` (404): No CBP ruling with that number.\n- `batch_not_found` (404): No batch with that id in this workspace.\n- `idempotency_key_reused` (409): The Idempotency-Key was already used with a different body.\n- `idempotency_in_progress` (409): A request with this Idempotency-Key is still running; retry shortly for its result.\n- `rate_limited` (429): Per-key request limit exceeded; honor `Retry-After`.\n- `classification_failed` (502): The engine failed; no credit was consumed. Retry with backoff.\n- `batch_unavailable` (503): The batch could not be queued; no credits were consumed. Retry with backoff.\n- `database_unavailable` (503): Temporary outage; retry with backoff.\n\n## Webhooks\nBatch requests may carry `webhook_url`; generate the workspace signing secret in Settings → API first, or the request is refused with `webhook_secret_required`. When the batch finishes, Tarri POSTs `{ \"type\": \"batch.completed\", \"batch_id\", \"url\", \"sent_at\" }` with a `Tarri-Signature: t=<unix>,v1=<hex>` header, where v1 is HMAC-SHA256 of `\"<t>.<body>\"` using that secret.\n\n## Versioning\nThe path carries the major version (`/api/v1`). Additive changes (new fields, new endpoints) ship without notice; breaking changes get a new major version and at least 90 days of overlap.","contact":{"name":"Tarri support","email":"support@tarri.ai","url":"https://tarri.ai/docs"}},"servers":[{"url":"https://app.tarri.ai/api/v1"}],"security":[{"apiKey":[]}],"tags":[{"name":"Classify","description":"Metered: one credit per product."},{"name":"Lookups","description":"Free: HTS lines, duty math, CBP rulings."},{"name":"Account","description":"Free: plan, keys, usage."}],"paths":{"/classify":{"post":{"tags":["Classify"],"operationId":"classify","summary":"Classify one product","description":"Returns the best HTS line, ranked alternates, the duty breakdown when `fob` is supplied, and supporting CBP rulings. Consumes one credit. Typical latency is 3 s for products the deterministic rules recognize and 10–30 s when the vision and reranking models run; set client timeouts to 60 s or use the batch endpoint.","parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string","maxLength":200},"description":"Makes retries safe; see Idempotency."},{"name":"X-Request-Id","in":"header","schema":{"type":"string","maxLength":80},"description":"Your correlation id; echoed back. Generated when absent."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClassifyRequest"},"example":{"name":"Hybrid Steel Fry Pan","description":"Stainless steel fry pan with ceramic nonstick interior, 10 inch","origin":"CN","fob":55650,"units":5000}}}},"responses":{"200":{"description":"Classified (or abstained with candidates).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClassifyResponse"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"No plan, no credits, or spend cap reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Idempotency-Key reused with a different body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Engine failure; credit not consumed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/classify/batch":{"post":{"tags":["Classify"],"operationId":"classifyBatch","summary":"Classify up to 200 products asynchronously","description":"Validates every item, consumes one credit per item up front, queues the work and returns a batch id to poll. Failed items are refunded when the batch settles, and overage for a batch is billed only for the items that succeeded. Add `webhook_url` to be called when the batch completes (needs a signing secret).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":200,"items":{"allOf":[{"$ref":"#/components/schemas/ClassifyRequest"},{"type":"object","properties":{"id":{"type":"string","maxLength":200,"description":"Your identifier for the item; echoed in results."}}}]}},"webhook_url":{"type":"string","format":"uri"}}}}}},"responses":{"202":{"description":"Queued.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"item_count":{"type":"integer"},"credits":{"type":"object"},"poll_url":{"type":"string"}}}}}},"400":{"description":"An item failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Not enough credits for the whole batch.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/batches/{id}":{"get":{"tags":["Classify"],"operationId":"getBatch","summary":"Poll a batch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Batch status and per-item results.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","completed","failed"]},"item_count":{"type":"integer"},"completed_count":{"type":"integer"},"failed_count":{"type":"integer"},"items":{"type":"array","items":{"type":"object","properties":{"position":{"type":"integer"},"id":{"type":["string","null"]},"status":{"type":"string"},"result":{"oneOf":[{"$ref":"#/components/schemas/ClassifyResponse"},{"type":"null"}]},"error":{"type":["string","null"]}}}}}}}}},"404":{"description":"Unknown batch.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/hts/{code}":{"get":{"tags":["Lookups"],"operationId":"getHtsLine","summary":"Look up an HTS line and its program rates for an origin","description":"Free. Returns the line plus `section301EffectiveRate`, `section232Rate`, `section232Annex`, `forcedLaborRate` and `section122Rate` for the origin. Use this to re-price a catalog after a tariff change without re-classifying.","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string"},"example":"7323.93.00.45"},{"name":"origin","in":"query","schema":{"type":"string","default":"CN"}}],"responses":{"200":{"description":"The line.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HtsLine"}}}},"404":{"description":"Unknown code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/duty":{"post":{"tags":["Lookups"],"operationId":"calculateDuty","summary":"Duty breakdown for a known HTS code","description":"Free. The same engine the classifier uses, for a code you already hold.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["htsDigits","fob"],"properties":{"htsDigits":{"type":"string"},"fob":{"type":"number"},"origin":{"type":"string","default":"CN"},"units":{"type":"number"},"netWeightKg":{"type":"number"},"section232Applies":{"type":"boolean"},"oceanShipment":{"type":"boolean","default":true},"settings":{"type":"object"}}}}}},"responses":{"200":{"description":"Line and breakdown.","content":{"application/json":{"schema":{"type":"object","properties":{"line":{"$ref":"#/components/schemas/HtsLine"},"duty":{"$ref":"#/components/schemas/Duty"}}}}}},"404":{"description":"Unknown code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/rulings":{"get":{"tags":["Lookups"],"operationId":"searchRulings","summary":"Search CBP CROSS rulings","parameters":[{"name":"code","in":"query","schema":{"type":"string"},"description":"HTS code (4 to 10 digits) to match rulings against."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Free-text search."},{"name":"limit","in":"query","schema":{"type":"integer","default":24}}],"responses":{"200":{"description":"Rulings.","content":{"application/json":{"schema":{"type":"object","properties":{"rulings":{"type":"array","items":{"$ref":"#/components/schemas/Ruling"}},"totalHits":{"type":"integer"},"matchedBy":{"type":"string"}}}}}}}}},"/rulings/{number}":{"get":{"tags":["Lookups"],"operationId":"getRuling","summary":"One ruling with its text","parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"string"},"example":"N320523"}],"responses":{"200":{"description":"The ruling."},"404":{"description":"Unknown ruling.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/me":{"get":{"tags":["Account"],"operationId":"getAccount","summary":"Plan, live keys and limits for this workspace","responses":{"200":{"description":"Account."}}}},"/usage":{"get":{"tags":["Account"],"operationId":"getUsage","summary":"Credits remaining, this period, and the last 30 days","responses":{"200":{"description":"Usage.","content":{"application/json":{"schema":{"type":"object","properties":{"credits_remaining":{"type":"integer"},"period_start":{"type":["string","null"]},"period_end":{"type":["string","null"]},"used_this_period":{"type":"integer"},"overage_this_period":{"type":"integer"},"overage_cents_this_period":{"type":"integer"},"spend_cap_cents":{"type":["integer","null"]},"last_30_days":{"type":"array","items":{"type":"object","properties":{"day":{"type":"string"},"classifications":{"type":"integer"},"overage":{"type":"integer"}}}}}}}}}}}},"/plans":{"get":{"tags":["Account"],"operationId":"listPlans","summary":"Public plan catalog and limits","security":[],"responses":{"200":{"description":"Plans."}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"tarri_sk_live_…"}},"schemas":{"ClassifyRequest":{"type":"object","description":"At least one of name, description or imageUrl is required.","properties":{"name":{"type":"string","maxLength":500,"description":"Short product name."},"description":{"type":"string","maxLength":4000,"description":"What it is, what it is made of, how it is used. The more material and function detail, the better the line."},"imageUrl":{"type":"string","description":"A `data:image/...;base64,` URL (recommended, up to ~10 MB) or an https URL on an allow-listed host. The photo is described by a vision model and used to confirm material and form."},"origin":{"type":"string","maxLength":2,"default":"CN","description":"ISO 3166-1 alpha-2 country of origin. Drives China-list 301 (CN/HK), Section 232 and the forced-labor tier (60 economies)."},"fob":{"type":"number","minimum":0,"description":"Customs value in USD. Needed for the dollar breakdown and for value-banded HTS lines (\"valued not over $3 each\")."},"units":{"type":"number","minimum":0,"description":"Pieces in the shipment or set; with fob, yields the per-unit value that picks value bands."},"netWeightKg":{"type":"number","minimum":0,"description":"For per-kilogram specific rates."},"section232Applies":{"type":"boolean","description":"Force Section 232 on or off. Omit to follow the HTS row: Annex I-A pays 50% and Annex I-B 25% of full FOB."},"oceanShipment":{"type":"boolean","default":true,"description":"Adds the Harbor Maintenance Fee."},"settings":{"type":"object","description":"Override duty-engine parameters (percent values).","properties":{"section232PrimaryRate":{"type":"number","default":50},"section232DerivativeRate":{"type":"number","default":25},"forcedLaborRate":{"type":"number","default":12.5},"forcedLaborReducedRate":{"type":"number","default":10},"section122Rate":{"type":"number","default":0,"description":"Expired 24 July 2026; leave at 0 unless reauthorized."},"mpfRate":{"type":"number","default":0.3464},"mpfMin":{"type":"number","default":33.58},"mpfMax":{"type":"number","default":651.5},"hmfRate":{"type":"number","default":0.125}}}}},"ClassifyResponse":{"type":"object","properties":{"best":{"oneOf":[{"$ref":"#/components/schemas/Candidate"},{"type":"null"}],"description":"null when Tarri abstains."},"candidates":{"type":"array","items":{"$ref":"#/components/schemas/Candidate"},"description":"Ranked; includes best. Alternates below 0.55 that share no heading with the pick are retrieval filler."},"abstained":{"type":"boolean","description":"True when no candidate cleared the confidence threshold. Review candidates rather than auto-accepting."},"abstainReason":{"type":["string","null"]},"query":{"type":"string"},"imageDescription":{"type":["string","null"],"description":"What the vision model saw, when a photo was supplied."},"perUnitValue":{"type":["number","null"]},"duty":{"oneOf":[{"$ref":"#/components/schemas/Duty"},{"type":"null"}],"description":"null when no fob was supplied."},"rulings":{"type":"array","items":{"$ref":"#/components/schemas/Ruling"},"description":"CBP CROSS rulings that support the pick."},"pipeline":{"type":"object","properties":{"directCodeMatch":{"type":"boolean"},"headings":{"type":"array","items":{"type":"string"}},"embeddingUsed":{"type":"boolean"},"aiRerankUsed":{"type":"boolean"},"rulingsUsed":{"type":"boolean"}}},"credits":{"type":"object","properties":{"consumed":{"type":"integer"},"overage":{"type":"boolean"},"remaining":{"type":"integer"}}},"request_id":{"type":"string"}}},"Candidate":{"type":"object","properties":{"htsNumber":{"type":"string","example":"7323.93.00.45"},"htsDigits":{"type":"string","example":"7323930045"},"item":{"type":"string","description":"Full HTS line description, heading to statistical suffix, joined by \" > \"."},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"0.80 and up is high; 0.62 is the abstention threshold."},"rationale":{"type":"string"},"generalDuty":{"type":"string","example":"2%"},"generalDutyRate":{"type":["number","null"]},"section301Duty":{"type":"string","description":"China-list Section 301 (Lists 1–4A, four-year review). Not the forced-labor 12.5%."},"section301Rate":{"type":["number","null"]},"section301ExclusionScope":{"type":"string","enum":["","full","partial"],"description":"A live 9903.88.69/.70 exclusion covers the line (through 9 November 2026)."},"section232Annex":{"type":"string","enum":["none","I-A","I-B"]},"section232Category":{"type":["string","null"],"example":"steel-derivative"},"forcedLaborExempt":{"type":"boolean","description":"On Annex II Part A of the forced-labor action: no forced-labor duty for any origin."},"forcedLaborExemptScope":{"type":"string","description":"Conditional exemption scope (\"Ex\", \"Aircraft\", \"Pharma\") or empty."},"retrievalScore":{"type":"number"}},"required":["htsNumber","htsDigits","item","confidence"]},"Duty":{"type":"object","description":"Landed-duty breakdown. Section 232 (25% Annex I-B / 50% Annex I-A) and the forced-labor Section 301 line never appear together: the forced-labor line is 0 with a note on any 232 article.","properties":{"fob":{"type":"number"},"lines":{"type":"array","items":{"$ref":"#/components/schemas/DutyLine"}},"manualReview":{"type":"boolean","description":"True when any line needs a human (specific/compound rates, net-of-MFN with a specific MFN)."},"missingInputs":{"type":"array","items":{"type":"string"}},"totalKnownDuty":{"type":"number","description":"USD sum of computable lines."},"landedCostKnown":{"type":"number","description":"FOB plus total known duty."},"effectiveKnownRate":{"type":"number","description":"totalKnownDuty / fob as a fraction (0.27 = 27%)."}}},"DutyLine":{"type":"object","properties":{"kind":{"type":"string","enum":["standard","section301","section232","forcedLabor","section122","mpf","hmf"]},"label":{"type":"string"},"basis":{"type":"string","description":"\"FOB\", \"Full FOB\", or \"Per customs entry\"."},"rate":{"type":["number","null"],"description":"Percent of the basis; null when the rate is specific or compound and needs manual review."},"amount":{"type":["number","null"],"description":"USD; null when it cannot be computed from the inputs."},"reviewRequired":{"type":"boolean"},"note":{"type":"string"},"missing":{"type":"array","items":{"type":"string"},"description":"Inputs that would let the line compute (e.g. \"quantity (units)\")."}},"required":["kind","label","basis","rate","amount","reviewRequired"]},"HtsLine":{"type":"object","properties":{"htsNumber":{"type":"string"},"htsDigits":{"type":"string"},"heading":{"type":"string"},"item":{"type":"string"},"generalDuty":{"type":"string"},"generalDutyRate":{"type":["number","null"]},"section301Duty":{"type":"string"},"section301Rate":{"type":["number","null"]},"section301EffectiveRate":{"type":["number","null"],"description":"After the origin gate and any full-line exclusion."},"section301ExclusionScope":{"type":"string"},"section232Annex":{"type":"string","enum":["none","I-A","I-B"]},"section232Category":{"type":["string","null"]},"section232Rate":{"type":"number"},"forcedLaborExempt":{"type":"boolean"},"forcedLaborExemptScope":{"type":"string"},"forcedLaborEconomyExemptions":{"type":"array","items":{"type":"string"}},"forcedLaborRate":{"type":["number","null"]},"section122Rate":{"type":"number"},"origin":{"type":"string"}}},"Ruling":{"type":"object","properties":{"rulingNumber":{"type":"string"},"subject":{"type":"string"},"rulingDate":{"type":["string","null"]},"tariffs":{"type":"array","items":{"type":"string"}},"revoked":{"type":"boolean"},"url":{"type":"string"}}},"Error":{"type":"object","properties":{"error":{"type":"string","description":"Machine-readable code (also in detail.code)."},"detail":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"doc_url":{"type":"string"}}},"request_id":{"type":"string"},"credits_remaining":{"type":"integer"}}}}}}