Build production trade
infrastructure.
Authentication, endpoints, webhooks, errors, SDKs, and changelog. Everything required to wire TariffOS into ERPs, quoting engines, and freight platforms.
The TariffOS API
The TariffOS API is a JSON over HTTPS interface for tariff intelligence, landed-cost computation, and trade compliance. It is the same engine that powers our dashboards, AI assistant, and enterprise integrations — exposed as primitives you can wire into any quoting engine, ERP, or freight platform.
All requests are made against https://api.tariffos.com and require a Bearer token. Responses are JSON, latencies are sub-100ms p95, and the surface area is versioned via the URL.
Run a sample request right here
The console below executes against an in-browser sandbox using mocked responses modeled on the production schema. Swap the placeholder API key for a real sk_test_… sandbox key to hit sandbox.api.tariffos.com directly.
https://sandbox.api.tariffos.com/v2/tariff/lookup?hs=8703.80&origin=CN&destination=USMemory only. Cleared on reload — safest for shared machines.
Build your TOS market stream subscription
Pick symbols, an emit interval, and a payload mode. The endpoint is GET /api/public/market/stream over Server-Sent Events. Use comma-separated symbols with optional * wildcards.
GET /api/public/market/stream?symbols=TOS.HS8703%2CTOS.FX.CNH%2CTOS.BDI&interval=1000&format=full Accept: text/event-stream
curl
curl -N "https://tos.sagolik.com/api/public/market/stream?symbols=TOS.HS8703%2CTOS.FX.CNH%2CTOS.BDI&interval=1000&format=full"
* wildcards (e.g. TOS.HS*) and the global *.hs, fx, idx. Combined with symbols via AND.interval 250–10000 ms. format=delta emits only changed ticks.From zero to landed cost in 60 seconds
- Request a key from enterprise sales or hit our self-serve sandbox.
- Export it as
TARIFFOS_KEY. - Call
POST /v2/landed-costwith an HS code and shipment value. - Subscribe to
tariff.changedwebhooks for ongoing intelligence.
curl https://api.tariffos.com/v2/landed-cost \
-H "Authorization: Bearer $TARIFFOS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: shp_4kJ29fA" \
-d '{
"hs_code": "8703.80",
"origin": "CN",
"destination": "US",
"value_usd": 48000,
"incoterm": "FOB"
}'Sandbox and production
| Environment | Base URL | Data | Rate limit |
|---|---|---|---|
| Sandbox | https://sandbox.api.tariffos.com | Frozen Q1 dataset | 60 r/min |
| Production | https://api.tariffos.com | Live, multi-region | By plan |
| EU residency | https://eu.api.tariffos.com | Live, EU-only egress | By plan |
URL-versioned, additive by default
Major versions live in the URL (/v2/…). Field additions and new enum values are non-breaking. Removals or rename ship as /v3 with a 12-month overlap and deprecation headers.
Sunset and Deprecation response headers per RFC 8594.Authentication overview
TariffOS supports three auth modes. Use API keys for server-side integrations, OAuth 2.0 for third-party apps acting on behalf of a tenant, and request signing when calling from untrusted edges.
| Mode | Use case | Header |
|---|---|---|
| API key | Server → API | Authorization: Bearer sk_live_… |
| OAuth 2.0 | Third-party app | Authorization: Bearer <access_token> |
| HMAC signing | Edge / browser | X-TariffOS-Signature: t=…,v1=… |
API keys
Keys are scoped to an environment and a workspace. Live keys are prefixed sk_live_; sandbox keys are sk_test_. Treat keys as secrets — store them in your platform's secret manager, never in source control.
GET /v2/tariff/lookup?hs=8703.80&origin=CN&destination=US HTTP/1.1
Host: api.tariffos.com
Authorization: Bearer sk_live_REPLACE_ME
Accept: application/json401 key_revoked within 5 seconds globally.OAuth 2.0 (Authorization Code + PKCE)
For partner apps acting on behalf of a TariffOS tenant. Standard OAuth 2.1 with PKCE; access tokens expire in 1 hour, refresh tokens in 60 days.
| Endpoint | Path |
|---|---|
| Authorize | https://auth.tariffos.com/oauth/authorize |
| Token | https://auth.tariffos.com/oauth/token |
| Revoke | https://auth.tariffos.com/oauth/revoke |
| Discovery | /.well-known/openid-configuration |
| Scope | Grants |
|---|---|
| tariff:read | Read tariff lookups + landed cost |
| shipments:write | Create shipment intents and quotes |
| alerts:manage | Subscribe and manage webhook endpoints |
| admin | Full workspace administration |
Request signing (HMAC-SHA256)
For environments where a key cannot be embedded (browser SDK, partner edge), sign each request with a short-lived signing secret derived server-side.
{timestamp}.{method}.{path}.{sha256(body)}
X-TariffOS-Signature: t=1714694400,v1=4f2e…c0a1/v2/tariff/lookupTariff lookup
Resolve the live applicable duty rate for a given HS code, origin, and destination — including special programs (Section 232/301, IEEPA, reciprocal tariffs).
| Field | Type | Req | Description |
|---|---|---|---|
| hs | string | required | HS code, 6–10 digits. |
| origin | string | required | ISO 3166-1 alpha-2 origin country. |
| destination | string | required | ISO 3166-1 alpha-2 destination country. |
| as_of | string | optional | ISO date — historical lookup. |
GET /v2/tariff/lookup?hs=8703.80&origin=CN&destination=US{
"hs_code": "8703.80",
"origin": "CN",
"destination": "US",
"applied_rate_pct": 44.5,
"components": [
{ "kind": "mfn", "rate_pct": 2.5 },
{ "kind": "section_301", "rate_pct": 25.0 },
{ "kind": "reciprocal", "rate_pct": 17.0 }
],
"effective": "2025-04-01",
"_meta": { "latency_ms": 38, "version": "v2.4.1" }
}/v2/landed-costLanded cost
Full landed-cost decomposition: duty, fees (MPF/HMF/VAT), freight, brokerage, and FTA-eligible savings. Returns alternative-sourcing recommendations when material savings exist.
| Field | Type | Req | Description |
|---|---|---|---|
| hs_code | string | required | HS code. |
| origin | string | required | ISO origin country. |
| destination | string | required | ISO destination country. |
| value_usd | number | required | Commercial invoice value. |
| incoterm | string | optional | FOB, CIF, DDP, EXW, … |
| weight_kg | number | optional | Used for freight modeling. |
POST /v2/landed-cost
Content-Type: application/json
Idempotency-Key: shp_4kJ29fA
{
"hs_code": "8703.80",
"origin": "CN",
"destination": "US",
"value_usd": 48000,
"incoterm": "FOB"
}{
"shipment_id": "shp_4kJ29fA",
"currency": "USD",
"value": 48000.00,
"duty": {
"mfn": 1200.00,
"section_301": 12000.00,
"reciprocal": 8160.00,
"total": 21360.00
},
"fees": { "mpf": 538.40, "hmf": 60.00 },
"freight_estimate": 2840.00,
"brokerage": 425.00,
"total_landed": 73223.40,
"fta_eligible": [],
"recommendations": [
{ "action": "shift_origin", "to": "VN", "savings_usd": 8420.00 }
]
}/v2/classifyAI classification
Map a free-text product description (and optional attributes) to the most likely HS code, with confidence and citations to the explanatory notes.
| Field | Type | Req | Description |
|---|---|---|---|
| description | string | required | Plain-text product description. |
| attributes | object | optional | Material, intended use, weight, etc. |
| destination | string | optional | Tunes classification to local nomenclature. |
POST /v2/classify
{
"description": "Cordless impact driver, lithium-ion, 18V, with carry case",
"attributes": { "powered_by": "battery" },
"destination": "US"
}{
"candidates": [
{ "hs_code": "8467.21", "confidence": 0.92, "title": "Drills, electromechanical, hand-held, …" },
{ "hs_code": "8467.29", "confidence": 0.05, "title": "Other tools, with self-contained electric motor" }
],
"citations": ["WCO Explanatory Note 84.67 (II)(B)"],
"_meta": { "model": "tariff-classifier-v3", "latency_ms": 612 }
}/v2/fta/eligibilityFTA eligibility
Check whether a shipment qualifies for preferential treatment under any active free trade agreement between the origin and destination.
| Field | Type | Req | Description |
|---|---|---|---|
| hs | string | required | HS code. |
| origin | string | required | Origin ISO. |
| destination | string | required | Destination ISO. |
GET /v2/fta/eligibility?hs=6109.10&origin=VN&destination=US{
"eligible_agreements": [],
"near_misses": [
{
"agreement": "GSP",
"blocked_by": "expired_2020-12-31",
"would_save_pct": 16.5
}
]
}/v2/origin/rulesRules of origin
Returns the substantial-transformation tests and regional value content thresholds for an HS code under a given agreement.
| Field | Type | Req | Description |
|---|---|---|---|
| hs | string | required | HS code. |
| agreement | string | required | USMCA, CPTPP, EU-VN, … |
GET /v2/origin/rules?hs=8703.80&agreement=USMCA{
"agreement": "USMCA",
"tests": [
{ "kind": "tariff_shift", "from": "8708", "to": "8703" },
{ "kind": "rvc", "method": "net_cost", "threshold_pct": 75 },
{ "kind": "labor_value", "threshold_pct": 40, "wage_floor_usd": 16 }
]
}/v2/scenarios/simulateScenario simulate
Run a what-if across one or many shipments with hypothetical tariff schedules, sourcing changes, or FX shocks. Returns deltas and worst/best case ranges.
| Field | Type | Req | Description |
|---|---|---|---|
| shipments | array | required | Array of shipment intents. |
| overrides | object | required | Tariff, FX, or sourcing overrides. |
POST /v2/scenarios/simulate
{
"shipments": [{ "hs_code": "8703.80", "origin": "CN", "destination": "US", "value_usd": 48000 }],
"overrides": { "reciprocal_tariff_pct": { "CN": 30 } }
}{
"baseline_total_landed": 73223.40,
"scenario_total_landed": 79463.40,
"delta_usd": 6240.00,
"delta_pct": 8.52
}/v2/changes/sinceTariff changes
Pull all schedule changes that affect your subscribed HS codes / lanes since a given timestamp. Designed for nightly batch reconciliation.
| Field | Type | Req | Description |
|---|---|---|---|
| since | string | required | ISO 8601 timestamp. |
| lanes | string | optional | Comma-separated origin>destination pairs. |
GET /v2/changes/since?since=2025-04-01T00:00:00Z&lanes=CN>US,VN>US{
"changes": [
{
"id": "chg_91kfA2",
"kind": "rate_change",
"hs": "8703.80",
"lane": "CN>US",
"from_pct": 27.5,
"to_pct": 44.5,
"effective": "2025-04-09",
"source": "USTR Federal Register 90 FR 12345"
}
],
"next_cursor": "cur_…"
}Webhooks
Subscribe an HTTPS endpoint to receive event notifications. Deliveries are signed with HMAC-SHA256 over the raw request body and a per-endpoint signing secret. Retries follow exponential backoff (1m → 24h) for up to 72 hours.
curl https://api.tariffos.com/v2/alerts/subscribe \
-H "Authorization: Bearer $TARIFFOS_KEY" \
-d '{
"url": "https://hooks.acme.com/tariffos",
"events": ["tariff.changed", "shipment.recommendation"],
"filters": { "lanes": ["CN>US", "VN>US"] }
}'import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(req: { headers: Record<string,string>, rawBody: string }, secret: string) {
const sig = req.headers["x-tariffos-signature"]; // t=...,v1=...
const parts = Object.fromEntries(sig.split(",").map(p => p.split("=")));
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${req.rawBody}`)
.digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Event reference
| Event | Fires when |
|---|---|
| tariff.changed | A duty rate on a subscribed lane is updated. |
| policy.announced | A policy memo is published before becoming effective. |
| fta.changed | An agreement enters/exits force or rules update. |
| shipment.recommendation | An optimization is found for a recurring lane. |
| alert.exposure_breach | Aggregate exposure crosses a configured threshold. |
Streaming API (SSE)
For low-latency dashboards, the TariffOS market and tariff feed is exposed as a long-lived Server-Sent Events connection. A single HTTP/1.1 or HTTP/2 GET opens a stream that emits typed events until the client or server closes it. The protocol is the W3C text/event-stream wire format — no client library is required, just EventSource in the browser or any framing-aware HTTP client on the server.
Endpoint
GET /api/public/market/stream?symbols=TOS.HS*,TOS.FX.CNH&interval=1000&format=delta HTTP/1.1
Host: api.tariffos.com
Accept: text/event-stream
Authorization: Bearer sk_live_REPLACE_MESandbox base: https://sandbox.api.tariffos.com. The same path is mirrored on the platform domain at /api/public/market/stream for unauthenticated public market data.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
| symbols | string | * | Comma-separated symbols or wildcard patterns. Examples: TOS.HS8703, TOS.HS*, TOS.FX.*, *. |
| categories | string | (all) | Filter by category — one or more of hs, fx, idx. Combined with symbols using AND semantics. |
| interval | number (ms) | 1000 | Tick cadence in milliseconds. Clamped to the inclusive range [250, 10000]. |
| format | enum | full | full = every tick contains all subscribed symbols. delta = only symbols whose value changed since the previous tick. |
400 no_symbols_matched and a JSON body listing the available symbols and their categories. Open the stream once with ?symbols=* during development to discover the full inventory.Response headers
| Header | Value | Purpose |
|---|---|---|
| Content-Type | text/event-stream; charset=utf-8 | SSE wire format. |
| Cache-Control | no-cache, no-transform | Disables intermediary caching. |
| Connection | keep-alive | Long-lived TCP connection. |
| X-Accel-Buffering | no | Disables nginx response buffering. |
| X-TOS-Symbols | TOS.HS8703,TOS.FX.CNH | Resolved symbol set after wildcard expansion. |
| X-TOS-Interval-Ms | 1000 | Effective (clamped) tick cadence. |
Event types
Every frame is a named SSE event with a JSON data payload. Listen with es.addEventListener("<event>", …); the default onmessage handler will not fire because every event is named.
| Event | Cadence | Purpose |
|---|---|---|
| ready | Once, on connect | Resolved subscription metadata. Use to render UI before the first tick. |
| snapshot | Once, after ready | Initial value for every subscribed symbol. |
| tick | Every interval ms | Updated values. In delta mode, only changed symbols. |
| heartbeat | Every 5 s | Liveness signal. Carries seq + serverTime; no market data. |
event: ready
data: {"subscription":{"patterns":["TOS.HS*"],"categories":[],"intervalMs":1000,"format":"delta","symbols":[{"sym":"TOS.HS8703","label":"EV · CN→US","category":"hs","unit":"pct"}]},"serverTime":"2026-05-06T14:22:01.004Z"}
event: snapshot
data: {"source":"live","ts":"2026-05-06T14:22:01.040Z","ticks":[{"sym":"TOS.HS8703","val":27.52,"open":27.5,"unit":"pct","decimals":2,"ts":"2026-05-06T14:22:01.040Z"}]}
event: tick
data: {"source":"live","ts":"2026-05-06T14:22:02.041Z","ticks":[{"sym":"TOS.HS8703","val":27.61,"ts":"2026-05-06T14:22:02.041Z"}]}
: hb 1746540126000
event: heartbeat
data: {"seq":1,"serverTime":"2026-05-06T14:22:06.001Z"}Heartbeat & liveness
The server emits two synchronized signals every 5000 ms:
- A raw SSE comment frame
: hb <unix-ms>. Comments are required by the spec to be ignored by EventSource consumers, but they keep proxies (nginx, Cloudflare, corporate gateways) from idling out the TCP connection during quiet markets. - A named
heartbeatevent with{ seq: number, serverTime: string }. The sequence number is monotonically increasing per connection and resets to1on reconnect. Use it both to detect dropped frames and to compute server clock skew.
const STALE_MS = 12_000; // 2.4× heartbeat cadence
const OFFLINE_MS = 30_000; // 6× heartbeat cadence — force reconnect
let lastBeat = Date.now();
let status: "connecting" | "live" | "stale" | "offline" = "connecting";
const es = new EventSource("/api/public/market/stream?symbols=TOS.HS*");
es.addEventListener("ready", () => { lastBeat = Date.now(); status = "live"; });
es.addEventListener("snapshot", () => { lastBeat = Date.now(); });
es.addEventListener("tick", () => { lastBeat = Date.now(); });
es.addEventListener("heartbeat", (e) => {
const { seq, serverTime } = JSON.parse(e.data);
lastBeat = Date.now();
console.debug("hb", seq, "skew", Date.now() - Date.parse(serverTime), "ms");
});
setInterval(() => {
const silent = Date.now() - lastBeat;
if (silent >= OFFLINE_MS) { status = "offline"; es.close(); /* reconnect */ }
else if (silent >= STALE_MS) { status = "stale"; }
}, 1000);Connection status semantics
| Status | Trigger | Recommended UI |
|---|---|---|
| connecting | EventSource constructed; ready event not yet received. | Neutral spinner. Disable write-back actions that depend on live values. |
| live | Last frame (any kind) received < 12 s ago. | Green ● LIVE indicator. Render values normally. |
| stale | No frame for 12–30 s. Connection still open; data may be lagging. | Amber ● STALE indicator. Keep last value, badge it as suspect. |
| offline | No frame for ≥ 30 s, or readyState === CLOSED. | Red ● OFFLINE indicator. Close socket, schedule reconnect with backoff. |
Reconnect policy
Browsers retry EventSource automatically with a 3 s default backoff. For server-side consumers we recommend exponential backoff capped at 30 s with full jitter, plus a hard cap of 1 reconnect / second on a single process to avoid thundering-herd against the edge.
Last-Event-ID header — every reconnect issues a fresh ready + snapshot pair so clients always start from a known state. Treat snapshots as authoritative and discard any prior cached values.Error responses
| Status | Code | Meaning |
|---|---|---|
| 400 | no_symbols_matched | Filter combination resolved to zero symbols. |
| 401 | unauthorized | Missing or invalid bearer token (authenticated tier). |
| 408 | client_timeout | Edge closed an idle TCP connection. Reconnect. |
| 429 | rate_limited | Too many concurrent streams from this key. |
| 503 | upstream_unavailable | Upstream feed degraded. Heartbeats continue. |
Errors
Errors return a stable shape. The code field is the contract — do not branch on message, which is for humans.
{
"error": {
"code": "tariff_not_found",
"message": "No tariff schedule for HS 9999.99 on lane XX>US.",
"request_id": "req_01HZ…",
"doc_url": "https://docs.tariffos.com/errors#tariff_not_found"
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed input or missing required field. |
| 401 | unauthorized | Missing or invalid API key. |
| 403 | scope_required | Token lacks the required OAuth scope. |
| 404 | tariff_not_found | No schedule for the requested HS / lane. |
| 409 | idempotency_conflict | Same Idempotency-Key, different payload. |
| 422 | validation_failed | Schema valid, business rule violated. |
| 429 | rate_limited | Throttled — see Retry-After header. |
| 500 | internal_error | Surface as retryable; we are paged. |
| 503 | upstream_unavailable | A government data source is degraded. |
Rate limits
| Plan | Sustained | Burst | Concurrency |
|---|---|---|---|
| Sandbox | 60 r/min | 120 r/min | 4 |
| Growth | 1,200 r/min | 3,000 r/min | 32 |
| Enterprise | Custom | Custom | Custom |
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. On 429, respect Retry-After.
Idempotency
All POST endpoints accept an Idempotency-Key header (UUID v4 recommended). Replays within 24 hours return the original response, including status code. Different payloads under the same key return 409 idempotency_conflict.
Pagination
List endpoints use cursor pagination. Pass ?limit=100&cursor=… — responses include next_cursor when more pages exist. Maximum page size is 500.
SDKs & libraries
| Language | Package | Status |
|---|---|---|
| TypeScript / Node | @tariffos/sdk | GA |
| Python | tariffos | GA |
| Go | github.com/tariffos/tariffos-go | Beta |
| Java | com.tariffos:sdk | Beta |
| .NET | TariffOS.Sdk | Preview |
| Ruby | tariffos | Preview |
Streaming SDK (typed)
A zero-dependency TypeScript client generated from the OpenAPI spec at /api/public/market/openapi. Every SSE frame — ready, snapshot, tick, heartbeat — is a fully typed payload. The client also exposes a derived status stream (connecting | live | stale | offline | closed) backed by heartbeat liveness thresholds, plus exponential-backoff reconnect.
# Single file, no runtime deps — copy into your app:
src/lib/tos-market-sdk.tsimport { createMarketStream } from "@/lib/tos-market-sdk";
const client = createMarketStream({
baseUrl: "https://tos.sagolik.com",
symbols: ["TOS.HS*", "TOS.FX.CNH"],
intervalMs: 1000,
format: "delta",
});
client.on("ready", (e) => {
// e.subscription: Subscription (patterns, categories, intervalMs, format, symbols)
console.log("subscribed to", e.subscription.symbols.map(s => s.sym));
});
client.on("snapshot", (e) => {
// e.ticks: Tick[] { sym, label, category, unit, decimals, open, val, ts }
hydrate(e.ticks);
});
client.on("tick", (e) => {
// delta or full depending on options
for (const t of e.ticks) update(t.sym, t.val);
});
client.on("heartbeat", (e) => {
// e.seq: number, e.serverTime: ISO string
setLastBeat(e.serverTime);
});
client.on("status", (s) => {
// "connecting" | "live" | "stale" | "offline" | "closed"
setBadge(s);
});
client.connect();export type Category = "hs" | "fx" | "idx";
export type Unit = "pct" | "px";
export type Format = "full" | "delta";
export type Source = "live" | "synthetic";
export interface SymbolDef { sym: string; label: string; category: Category; unit: Unit }
export interface Tick { sym: string; label: string; category: Category; unit: Unit;
decimals: number; open: number; val: number; ts: string }
export interface Subscription { patterns: string[]; categories: Category[];
intervalMs: number; format: Format; symbols: SymbolDef[] }
export interface ReadyEvent { subscription: Subscription; serverTime: string }
export interface SnapshotEvent { source: Source; ts: string; ticks: Tick[] }
export interface TickEvent { source: Source; ts: string; ticks: Tick[] }
export interface HeartbeatEvent { seq: number; serverTime: string }
export type MarketEvent =
| ({ type: "ready" } & ReadyEvent)
| ({ type: "snapshot" } & SnapshotEvent)
| ({ type: "tick" } & TickEvent)
| ({ type: "heartbeat" } & HeartbeatEvent);Need a different language? The OpenAPI document at /api/public/market/openapi is consumable by openapi-generator, openapi-typescript, and orval — all event payloads are defined as named schemas so generated clients keep the same names used here.
Postman collection
Import the live collection — every endpoint, pre-request scripts for HMAC signing, and example sandbox keys.
https://docs.tariffos.com/postman/tariffos.postman_collection.jsonOpenAPI 3.1 specification
The full machine-readable spec — generate clients, mock servers, or contract tests directly from it. The streaming endpoint ships a live, self-hosted spec with typed schemas for every SSE event (ready, snapshot, tick, heartbeat).
GET /api/public/market/openapi
Content-Type: application/json
# Try it: curl -s /api/public/market/openapi | jq .pathshttps://api.tariffos.com/v2/openapi.json
sha256: 9f4c…1ab2Changelog
- 2025-04-12ADDEDReciprocal tariff component
Tariff lookup and landed cost now decompose the new reciprocal-tariff component. Existing fields are unchanged.
- 2025-03-30ADDEDEU residency endpoint
eu.api.tariffos.com — all data plane traffic stays within EU regions.
- 2025-02-18CHANGEDStable webhook signing v1
x-tariffos-signature now uses the stable t=…,v1=… format. Legacy x-signature is deprecated for removal 2025-08-01.
- 2025-01-04ADDEDScenario simulate endpoint
POST /v2/scenarios/simulate — model tariff, FX, and sourcing overrides across portfolios.
Need a sandbox key?
Approval in 24 hours. Production keys gated to enterprise plans.