Build with TariffTrust

Connect your trade workflow.

The same sourced estimates as the portal, from your server or your AI agent.

Quick start

  1. Create an API key

    In your workspace, under API keys, create a key with the read and calculate permissions: read for the catalog and the saved estimates, calculate for the estimates. Your plan must include the API. Send the key in Authorization: Bearer (or X-API-Key); keep it on your server, never in a browser or a mobile app. GET /v1/status needs no key.

  2. Estimate a product

    An HTS code, a country of origin and the customs value in USD:

    curl -X POST "https://api.tarifftrust.com/v1/estimates" \
      -H "Authorization: Bearer $TARIFFTRUST_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"hts":"6109.10.00.12","origin":"CN","customs_value":"10000"}'

    Official clients: pip install tarifftrust for Python, npm install tarifftrust for Node.js, with examples on GitHub. The reference shows each call with them.

  3. Read status first

    Then the amounts: they are decimal strings in USD. The fields of the answer are below.

Errors, rate limits, the quota and safe retries are in the reference: errors and limits.

Read the answer

Unknown is never zero. A null amount is unknown: when estimated_duty is null, only known_duty_subtotal is established.

status
What the answer is. freshness and verification are separate from it.
estimated_duty
The duty of the goods, or null when it cannot be established.
known_duty_subtotal
What is established when the duty is not.
assumptions
Every question nobody answered, taken in its ordinary case: the goods are what the measures of their code are aimed at, and no exemption, advantage or certification is claimed. Each comes with its question, its choices and, under alternatives, what each other answer would give.
facts
Your own answers, sent in the request: they replace an assumption, in the first call when you know them. GET /v1/facts lists every question by its name. Set them once for your workspace under Default answers.
as_of
The day of entry, in New York time; unset, today. On a day when a measure takes effect at a stated hour, send the instant with its offset (2026-04-05T00:01:00-04:00).
preference
A trade agreement's symbol (GET /v1/agreements): sending it declares that the goods meet its rules of origin.
quantities
The quantities a duty counts, in its units (kg, l, item, dozen, pair, proof_liter…): a result names each one it needs in required_fields (quantities.kg) and asks it in input_guidance. The units of GET /v1/hts/{code} are the statistical units the tariff prints (No., doz.), not these keys.
needs_information
A question without an ordinary case: input_guidance says what to send, scenarios gives the amount of each answer and the bounds.
review_pending
What withholds part of an estimate: a position under review, a wrong-rate incident, an official notice not yet read, a measure in force whose rate is not announced yet.

One call gives an estimate. A reply that only asks a question, without any duty amount or rate, is not counted against your quota: see what counts. Every field of the answer is described on the reference.

Declarations: fees once per declaration

Estimate a whole import declaration with POST /v1/estimates/entry: each line is calculated, then MPF and HMF once on the declaration.

Do not add up single-product results. The lines of one declaration added one by one give a wrong MPF: ten lines of $1,000 owe $34.64 of MPF, not ten minimums.

  • A batch (/estimates/batch) is a list of independent products, not a declaration.
  • The history of a product (/estimates/history) gives it at every change of the tariff over a period.
  • A CSV of lines is imported in the portal (Declarations); a file covering several declarations names the declaration of every line.

Saving, proof and replay

Calculate and save at once with POST /v1/saved-estimates: {"estimate": {...}} for a product, {"entry": {...}} for a declaration. GET /v1/saved-estimates lists them.

  • An unsaved estimate is not kept: it cannot be replayed, reported or notified individually in an incident.
  • A saved estimate keeps its resolved inputs, its publication and its engine version.
  • It appears in the portal with the others, where you build its proof bundle, share it or report an error on it.
  • TariffTrust replays a saved estimate exactly on request, in the image that produced it: open a support case with its identifier.

MCP

Connect a Streamable HTTP client to https://api.tarifftrust.com/mcp/, with your key as a Bearer token. Pass "save": true to keep an estimate.

Toolsestimateestimate_entryestimate_batchsearch_htsget_htslist_agreementslist_factsestimate_historylist_saved_estimatesget_saved_estimateget_status

The operations of the API under the same names, counted as they are; search_hts takes its text in query.

An error is a JSON-RPC error, sent with HTTP 200 but for a missing or invalid key (401) and an error of the service (500): -32602 for invalid arguments, the fields in error.data.fields; -32000 for a refusal of the service, its code of the API in error.data.code (quota_exceeded, rate_limited, data_unavailable…) and Retry-After when trying again helps; -32603 for an error of the service; -32700 and -32600 for a body that is not JSON, or not JSON-RPC 2.0.

Runnable examples

Python scripts that run against your workspace, with only httpx.

estimate.py

Runnable TariffTrust integration example: a product, then a whole declaration, saved.

Download
Show the code · 85 lines
"""Runnable TariffTrust integration example: a product, then a whole declaration, saved.

    TARIFFTRUST_URL=https://api.tarifftrust.com TARIFFTRUST_API_KEY=tarifftrust_... python estimate.py

Uses only ``httpx``. Keep the API key on your server, never in a browser or a mobile app.
"""

import os
import sys
import uuid

import httpx


def client(url=None, key=None):
    return httpx.Client(
        base_url=(url or os.environ["TARIFFTRUST_URL"]).rstrip("/") + "/v1",
        headers={"Authorization": f"Bearer {key or os.environ['TARIFFTRUST_API_KEY']}"},
        timeout=30,
    )


def money(amount):
    """An amount as the API gives it: a decimal string in USD, or null when unknown, never zero."""
    return "unknown" if amount is None else f"${amount}"


def describe(result):
    """Relay a result honestly: unknown is never zero, and the status always goes with it."""
    lines = [f"Status: {result['status']}"]
    if result["estimated_duty"] is not None:
        lines.append(f"Estimated duty: {money(result['estimated_duty'])}")
    else:
        lines.append(f"Established duties only: {money(result['known_duty_subtotal'])} (total unknown)")
    for item in result.get("assumptions") or []:
        # What nobody answered took its ordinary case: show it, with what each other answer gives,
        # to the user who may know better, and send their answer in ``facts``.
        others = ", ".join(
            f"{other['value']}: {money(other['estimated_duty'])}" for other in item.get("alternatives") or []
        )
        assumed = f"Assumed: {item.get('question') or item['fact']} {item['value']}"
        lines.append(f"{assumed} (otherwise {others})" if others else assumed)
    for question in result.get("input_guidance") or []:
        lines.append(f"Question: {question.get('question') or question['label']} ({question['field']})")
    for item in result.get("review_pending", []):
        lines.append(f"Under review: {item.get('position') or item.get('reference')}: {item['reason']}")
    return "\n".join(lines)


def estimate_product(api, as_of=None, facts=None):
    """``as_of`` is the date of entry; without it, the service uses today and records it. The first
    call estimates. ``facts`` are your user's own answers: a question without an ordinary case
    (``needs_information``, asked in ``input_guidance``) is theirs to answer, never yours in their
    place: "no" to "does Section 232 apply?" takes that duty out of the estimate."""
    product = {"hts": "6109.10.00.12", "origin": "CN", "customs_value": "10000", "as_of": as_of}
    return api.post("/estimates", json={**product, "facts": facts or {}}).raise_for_status().json()


def save_declaration(api, as_of=None, key=None):
    """Ten lines of $1,000: one MPF on the declaration, not ten minimums. ``key`` makes the request
    safe to send again, after a timeout say: one per declaration, kept to send it again, for 7 days;
    the same key with another request is refused (409)."""
    entry = {
        "reference": "Example entry",
        "as_of": as_of,
        "include_fees": True,
        "lines": [{"hts": "6109.10.00.12", "origin": "CN", "customs_value": "1000"} for _ in range(10)],
    }
    headers = {"Idempotency-Key": key or str(uuid.uuid4())}
    return api.post("/saved-estimates", json={"entry": entry}, headers=headers).raise_for_status().json()


def main():
    with client() as api:
        print(describe(estimate_product(api)))
        saved = save_declaration(api)
        print(f"Saved declaration {saved['entry_id']} (saved estimate {saved['id']})")
        print(describe(saved["result"]))
        fees = {fee["label"]: money(fee["amount"]) for fee in saved["result"]["fees"]}
        print(f"Fees once per declaration: {fees}")
    return 0


if __name__ == "__main__":
    sys.exit(main())

mcp_entry.py

Runnable MCP example: estimate and save a declaration with the estimate_entry tool.

Download
Show the code · 56 lines
"""Runnable MCP example: estimate and save a declaration with the ``estimate_entry`` tool.

TARIFFTRUST_URL=https://api.tarifftrust.com TARIFFTRUST_API_KEY=tarifftrust_... python mcp_entry.py
"""

import json
import os
import sys

import httpx


def call(url, key, method, params=None, identifier=1):
    response = httpx.post(
        url.rstrip("/") + "/mcp/",
        json={"jsonrpc": "2.0", "id": identifier, "method": method, "params": params or {}},
        headers={"Authorization": f"Bearer {key}"},
        timeout=30,
    )
    response.raise_for_status()
    message = response.json()
    if "error" in message:
        raise RuntimeError(message["error"]["message"])
    return message["result"]


def main(url=None, key=None):
    url, key = url or os.environ["TARIFFTRUST_URL"], key or os.environ["TARIFFTRUST_API_KEY"]
    call(url, key, "initialize", {"protocolVersion": "2025-11-25"})
    tools = [tool["name"] for tool in call(url, key, "tools/list", identifier=2)["tools"]]
    result = call(
        url,
        key,
        "tools/call",
        {
            "name": "estimate_entry",
            "arguments": {
                "lines": [
                    {"hts": "6109.10.00.12", "origin": "CN", "customs_value": "1000"},
                    {"hts": "6109.10.00.12", "origin": "VN", "customs_value": "2500"},
                ],
                "facts": {},
                "save": True,
            },
        },
        identifier=3,
    )["structuredContent"]
    print(
        json.dumps({"tools": tools, "status": result["status"], "calculation_id": result["calculation_id"]})
    )
    return result


if __name__ == "__main__":
    main()
    sys.exit(0)

E-commerce integration examples

These examples use customs fields that you configure explicitly in your catalog: a description or a retail price cannot establish a customs code or a customs value. They are integration examples, not installed marketplace plugins.

  1. Add the shared helper to your server

    It checks the three customs fields and calls the API with your key.

    Show the helper · Node.js 22+
    // Node.js 22+; keep the API key on the server.
    // Configure these customs fields explicitly in your catalog:
    // {hts: '0901110015', origin: 'BR', customs_value_usd: '10000'}
    // customs_value_usd is the shipment's declared value, not a retail price.
    async function estimateCustoms(customs) {
      for (const key of ['hts', 'origin', 'customs_value_usd']) {
        if (!customs?.[key]) throw new Error(`Missing customs field: ${key}`);
      }
      const response = await fetch('https://api.tarifftrust.com/v1/estimates', {
        method: 'POST',
        headers: {'Authorization': `Bearer ${process.env.TARIFFTRUST_API_KEY}`,
                  'Content-Type': 'application/json'},
        body: JSON.stringify({hts: customs.hts, origin: customs.origin,
                              customs_value: customs.customs_value_usd}),
        signal: AbortSignal.timeout(20000)
      });
      const result = await response.json();
      if (!response.ok) throw new Error(result.error?.message || `HTTP ${response.status}`);
      return result;
    }
  2. Call it with your platform's customs fields

    const customs = /* where your platform keeps them: see the table */;
    const estimate = await estimateCustoms(customs);
    // Persist estimate.publication_id and preserve estimate.status.
    // Only show a final duty when estimate.estimated_duty is not null: unknown is never zero.
PlatformCustoms fieldsWhere
Medusavariant.metadataStore the customs fields in variant metadata; invoke the helper from a backend workflow.
SolidusexportedVariant.customsExport a variant's customs attributes from your Rails backend into the customs object below.
Spreevariant.public_metadataPopulate explicit variant metadata; call from your backend, never expose the TariffTrust key in Storefront code.
SaleorObject.fromEntries(variant.privateMetadata.map(x => [x.key, x.value]))Read private variant metadata with your server-side Saleor app permissions.
BigCommerceObject.fromEntries(product.custom_fields.map(x => [x.name, x.value]))Map product custom fields in your server-side catalog integration.
Adobe CommerceObject.fromEntries(product.custom_attributes.map(x => [x.attribute_code, x.value]))Create customs attributes and read them from the backend product response.
Swellproduct.metadataSupply customs metadata from your backend product export; adapt the field location to your catalog model.
FoxyexportedItem.customsPopulate the customs object from trusted catalog data on your server; do not trust shopper-supplied tariff fields.