API Reference · v1
Tarri Classification API
Send a product description or photo; get back the HTS code, the real stacked landed duty, the CBP rulings behind it, and a calibrated confidence score — the same engine that powers the Tarri app, over a single REST call.
Introduction
The Tarri API is a JSON-over-HTTPS endpoint. There is one method — classify a product — and it returns everything the app shows: the ranked HTS classification, a full landed-duty breakdown (Section 301 / 122 / 232 / MPF / HMF, gated by country of origin), and an honest confidence score that tells you when to trust the answer and when to review it.
https://app.tarri.aiAuthorization: Bearer <key>application/jsonAuthentication
Every request must include your secret API key as a bearer token:
Authorization: Bearer tarri_sk_live_…- Get your key in the app under Settings → API access (available on the Team plan). Click Generate API key.
- One key per workspace. Generating again rotates the key — the previous one stops working immediately.
- Shown once. For your security only a hash is stored; copy the key when it's shown. If you lose it, regenerate.
- Keep it secret. Use it server-side only — never ship it in a browser, mobile app, or public repo.
TARRI_API_KEY), not in source.Quickstart
Classify your first product in one call. Set TARRI_API_KEY in your environment, then:
curl -X POST https://app.tarri.ai/api/v1/classify \
-H "Authorization: Bearer $TARRI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "1500W stainless steel convection toaster oven",
"origin": "CN",
"fob": 42,
"units": 1
}'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",
},
body: JSON.stringify({
description: "1500W stainless steel convection toaster oven",
origin: "CN",
fob: 42,
units: 1,
}),
});
if (!res.ok) throw new Error(`Tarri API error ${res.status}`);
const result = await res.json();
console.log(result.best.htsNumber, result.best.confidence);import os, requests
res = requests.post(
"https://app.tarri.ai/api/v1/classify",
headers={"Authorization": f"Bearer {os.environ['TARRI_API_KEY']}"},
json={
"description": "1500W stainless steel convection toaster oven",
"origin": "CN",
"fob": 42,
"units": 1,
},
timeout=60,
)
res.raise_for_status()
result = res.json()
print(result["best"]["htsNumber"], result["best"]["confidence"])A classification typically takes a few seconds (it runs retrieval, an AI rerank, and a CBP-ruling lookup). Use a request timeout of at least 60 seconds.
Rate limits & quota
API calls draw from the same monthly classification quota as the app — there is no separate API meter.
| Plan | Classifications / month | API access |
|---|---|---|
| Free | 10 | — |
| Pro | 1,000 | — |
| Team | 5,000 | ✓ |
| Enterprise | Unlimited | ✓ |
When the quota is exhausted the endpoint returns 402 quota_exceeded with plan, used, and limit. Quota resets at the start of each calendar month (UTC). Only successful (2xx) classifications count.
POST /api/v1/classify
Classify a single product. Returns the ranked HTS shortlist, the landed-duty breakdown, and a confidence score.
POST https://app.tarri.ai/api/v1/classifyRequest parameters
A JSON body. Supply at least one of description, name, or imageUrl; everything else is optional but improves accuracy and unlocks the dollar duty breakdown.
| Field | Type | Required | Description |
|---|---|---|---|
description | string | Conditional | Plain-language description of the product — what it is, material, and use. e.g. "1500W stainless steel convection toaster oven". At least one of description, name, or imageUrl is required. |
name | string | Conditional | Short product name. Combined with description as the classification query. |
imageUrl | string | Conditional | A public image URL or a base64 data URL of the product photo. Tarri describes the image, then classifies. Max ~10 MB. |
origin | string | Optional | ISO 3166-1 alpha-2 country of origin, e.g. CN, IN, US. Drives origin-gated tariffs (Section 301 applies to CN/HK; Section 122 is zeroed for US). Defaults to no origin gating. |
fob | number | Optional | FOB / customs value in USD. Required to compute the dollar landed-duty breakdown and per-unit value bands. Omit for a code-only classification. |
units | number | Optional | Pieces per set. With fob, yields the per-unit value used to resolve "valued … each" HTS value bands. |
netWeightKg | number | Optional | Net weight in kilograms. Needed for specific/compound rates expressed per kg. |
containsSteel | boolean | Optional | Whether the article contains steel (triggers a Section 232 line on the steel portion). Defaults to false. |
steelPercent | number | Optional | Percent (0–100) of FOB attributable to steel content. Used with containsSteel. |
oceanShipment | boolean | Optional | Whether the shipment arrives by ocean (adds the Harbor Maintenance Fee). Defaults to true. |
settings | object | Optional | Override the duty-engine parameters: section122Rate, steel232Rate, mpfRate, mpfMin, mpfMax, hmfRate (all numbers). Defaults to the current published rates. |
The response object
A 200 returns the full classification result:
| Field | Type | Description |
|---|---|---|
best | Candidate | null | The single best HTS classification, or null when Tarri abstains (no confident match). |
candidates | Candidate[] | Ranked alternatives (highest confidence first), including best. Present even when abstained, so you can review the closest options. |
abstained | boolean | When true, no candidate cleared the confidence threshold — review candidates manually rather than auto-accepting. |
abstainReason | string | null | Why Tarri abstained, when it did. |
query | string | The normalized text query that was classified. |
imageDescription | string | null | If an image was supplied, the literal product description Tarri derived from it. |
perUnitValue | number | null | FOB ÷ units, when both were provided. |
duty | Duty | null | The landed-duty breakdown for best. null when no fob was supplied. |
pipeline | object | Diagnostics: directCodeMatch, headings[], embeddingUsed, aiRerankUsed, rulingsUsed. |
Example response (abridged):
{
"best": {
"htsNumber": "8516.60.40.70",
"htsDigits": "8516604070",
"item": "Electrothermic ovens … Cooking stoves, ranges and ovens",
"confidence": 0.94,
"generalDutyRate": 2.7,
"section301Rate": 25,
"generalDuty": "2.7%",
"section301Duty": "25%",
"rationale": "Consumer electric toaster oven — domestic electrothermic oven under 8516.60.",
"retrievalScore": 0.81
},
"candidates": [ /* … ranked alternatives … */ ],
"abstained": false,
"abstainReason": null,
"perUnitValue": 42,
"duty": {
"fob": 42,
"totalKnownDuty": 16.04,
"effectiveKnownRate": 38.2,
"landedCostKnown": 58.04,
"lines": [ /* Standard, Sec 301, Sec 122, MPF, HMF … */ ],
"manualReview": false,
"missingInputs": []
},
"pipeline": { "aiRerankUsed": true, "rulingsUsed": true, "headings": ["8516"] }
}The candidate object
Each entry in best and candidates:
| Field | Type | Description |
|---|---|---|
htsNumber | string | Formatted 10-digit HTS code, e.g. "8516.60.40.70". |
htsDigits | string | Unformatted digits, e.g. "8516604070". |
item | string | The full HTS line description (breadcrumb of headings → statistical suffix). |
confidence | number | Calibrated confidence in [0, 1]. ≥ 0.80 high, 0.60–0.79 medium, < 0.60 low. |
generalDutyRate | number | null | Standard ad-valorem rate as a percent (e.g. 2.7), or null for specific/compound rates. |
section301Rate | number | null | Section 301 rate as a percent, when applicable to the origin. |
generalDuty | string | Raw HTSUS general duty expression (e.g. "2.7%", "5.3¢/kg + 1.4%"). |
section301Duty | string | Raw Section 301 duty expression. |
rationale | string | Short explanation of why this line was chosen, including any CBP-ruling support. |
retrievalScore | number | Internal hybrid-retrieval score (debugging aid). |
The duty object
Present when you pass a positive fob. It computes the full landed-duty stack by country of origin.
| Field | Type | Description |
|---|---|---|
fob | number | The FOB value the breakdown was computed on. |
lines | DutyLine[] | Each duty component: { kind, label, basis, rate, amount, reviewRequired, note?, missing? } (Standard, Section 301, Section 122, Section 232, MPF, HMF). |
totalKnownDuty | number | Sum of all computable duty lines, in USD. |
landedCostKnown | number | FOB + total known duty. |
effectiveKnownRate | number | Total known duty ÷ FOB, as a percent. |
manualReview | boolean | True if any line needs manual review (e.g. an un-parsable specific rate). |
missingInputs | string[] | Inputs needed to finish the calc (e.g. ["net weight"] for a per-kg rate). |
Confidence & abstention
Tarri is calibrated — it tells you how sure it is, and refuses to bluff. Read these two fields before you trust a result programmatically:
best.confidence— a score in[0, 1]. As a rule of thumb: ≥ 0.80 high (safe to auto-accept), 0.60–0.79 medium (spot-check the value band), < 0.60 low.abstained— whentrue, no candidate was a confident fit.bestisnull; route the item to a human and reviewcandidates(still returned) instead of auto-accepting.
!abstained && best.confidence >= 0.8; otherwise queue for review. This keeps a documented reasonable-care trail.Errors
Errors return a non-2xx status with a JSON body of the form { "error": "…" }.
| Status | error | Meaning |
|---|---|---|
400 | Bad Request | The body failed validation — missing all of description/name/imageUrl, or a field has the wrong type. |
401 | missing_api_key | No Authorization: Bearer header was sent. |
401 | invalid_api_key | The key does not match any workspace (revoked, mistyped, or regenerated). |
402 | quota_exceeded | The workspace hit its monthly classification limit. Response includes plan, used, limit. |
403 | api_access_requires_team | The key is valid but its workspace is no longer on a plan that includes API access (Team or Enterprise). |
5xx | server_error | A transient server/database error. Safe to retry with backoff. |
Full examples
The same request in three languages:
curl -X POST https://app.tarri.ai/api/v1/classify \
-H "Authorization: Bearer $TARRI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "1500W stainless steel convection toaster oven",
"origin": "CN",
"fob": 42,
"units": 1
}'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",
},
body: JSON.stringify({
description: "1500W stainless steel convection toaster oven",
origin: "CN",
fob: 42,
units: 1,
}),
});
if (!res.ok) throw new Error(`Tarri API error ${res.status}`);
const result = await res.json();
console.log(result.best.htsNumber, result.best.confidence);import os, requests
res = requests.post(
"https://app.tarri.ai/api/v1/classify",
headers={"Authorization": f"Bearer {os.environ['TARRI_API_KEY']}"},
json={
"description": "1500W stainless steel convection toaster oven",
"origin": "CN",
"fob": 42,
"units": 1,
},
timeout=60,
)
res.raise_for_status()
result = res.json()
print(result["best"]["htsNumber"], result["best"]["confidence"])Support
Questions, higher limits, or a bulk/Enterprise plan? Email hello@tarri.ai or manage your key any time in Settings → API access.
Classifications are decision support, not customs or legal advice. Review before filing.