# border.bot API (v1)

Source: https://border.bot/docs/api
Version: 1.3.0. Base URL: `https://api.border.bot`. OpenAPI: https://api.border.bot/v1/openapi.json

Classify products to HS/HTS codes and calculate duties, taxes, fees and landed cost for cross-border parcels.

**Versions**: This is **v1**, the current version. Within a version, changes are only additive (new endpoints, new optional fields, new enum values). Breaking changes ship as a new version; every version and its dates: https://api.border.bot/versions

**Authentication**: send a workspace API key: `Authorization: Bearer bb_live_…`. Create keys in the dashboard.

**Credits**: billable requests use prepaid credits (see `GET /v1/pricing`). Prices are set server-side; failed requests are refunded automatically. Every response includes `X-Request-Id` and `API-Version`; billable responses include `X-Credits-Charged` and `X-Credits-Remaining`.

**Retries**: send an `Idempotency-Key` header on POST requests; repeating it returns the original result without charging again.

**Rate limits**: 120 requests per 60 seconds per API key, and per IP address for public endpoints. Responses carry the IETF `RateLimit-Policy` header (`"key";q=120;w=60` with a key, `"ip";q=120;w=60` without). Over the limit you get 429 `rate_limited` with `RateLimit: "key";r=0;t=60` and `Retry-After` in seconds; wait that long before retrying.

**Errors**: `{ "error": { "code", "message", "details"? } }` with a stable `code` (e.g. 402 `insufficient_credits`, 429 `rate_limited`).

**MCP**: use border.bot from Claude, ChatGPT, Codex, Cursor, VS Code and other MCP clients at `https://api.border.bot/mcp` (OAuth, no API key). Setup guides: https://border.bot/mcp

## Classification

HS/HTS codes.

### POST /v1/classify

**Classify a product** (API key required)

Find the HS/HTS code for a product shipped to `destinationCountry`. Give a description, a product page URL, or both. Costs the mode’s credits (`GET /v1/classify/modes` lists the modes and their prices); a weak answer may escalate to another mode at no extra charge. Failed requests are refunded automatically.

API key scope: `classify`.

Headers:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `idempotency-key` | string | no | Make retries safe: a repeat with the same key returns the original result without charging again (keys are scoped to your workspace). |

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `description` | string | no | What the product is: type, material/composition, intended use and user. Required unless `productUrl` or `imageUrl` is given. |
| `productUrl` | string | no | Product page URL. border.bot reads the listing (title, brand, materials, price and any stated country of origin) and classifies from it, with your description when you give one. |
| `imageUrl` | string | no | A product photo, classified with vision (in the modes that read photos: see `GET /v1/classify/modes`): a public URL, or the picture itself as a `data:image/jpeg;base64,…` URL (up to 6 MB). When given, the photo is classified instead of the listing URL. |
| `destinationCountry` | string | yes | Where the parcel is going: selects the tariff (the national one where border.bot has it, else the 6-digit HS). ISO 3166-1 alpha-2, case-insensitive. |
| `originCountry` | string | no | Where the product was made (context only). ISO 3166-1 alpha-2, case-insensitive. |
| `mode` | string | no | The classification mode: one of the modes `GET /v1/classify/modes` lists (each with its price and what it reads). Omitted: the default mode. An unknown or unavailable mode is refused (`invalid_input`, with `details.availableModes`). |
| `title` | string | no | Product title (optional extra context). |
| `brand` | string | no | Brand (optional extra context). |
| `sku` | string | no | Your SKU (stored with the usage record). |
| `price` | number | no | Unit price (optional context). |
| `currency` | string | no | ISO 4217 currency of `price`. |
| `hsCodeHint` | string | no | An HS code you believe is close (2–10 digits) — used as a hint, not trusted blindly. |

Responses:

- `200`: The classification and the credits charged. (`ClassifyResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `402`: Not enough credits (`insufficient_credits`). Nothing was charged. (`Error`)
- `403`: Workspace suspended, action disabled, or the key lacks the scope (`org_suspended`, `action_disabled`, `forbidden`). (`Error`)
- `409`: A request with this Idempotency-Key is still in progress (`conflict`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)
- `502`: The engine failed (`upstream_error`). The credits are refunded automatically. (`Error`)
- `504`: The engine timed out (`upstream_timeout`). The credits are refunded automatically. (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/classify' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"description":"Men'\''s short-sleeve t-shirt, 100% cotton, knitted","destinationCountry":"US","originCountry":"PT"}'
```

### POST /v1/classify/batch

**Classify up to 25 products** (API key required)

Each item is a classify request, run four at a time and charged as one classification by its own mode; one failing (an unclassifiable description, insufficient credits) never stops the others and is refunded. With an `Idempotency-Key`, each item replays on its own.

API key scope: `classify`.

Headers:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `idempotency-key` | string | no | Make retries safe: a repeat with the same key returns the original result without charging again (keys are scoped to your workspace). |

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | ClassifyRequest[] | yes |  |

Responses:

- `200`: One result per item, in order. (`ClassifyBatchResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `402`: Not enough credits (`insufficient_credits`). Nothing was charged. (`Error`)
- `403`: Workspace suspended, action disabled, or the key lacks the scope (`org_suspended`, `action_disabled`, `forbidden`). (`Error`)
- `409`: A request with this Idempotency-Key is still in progress (`conflict`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)
- `502`: The engine failed (`upstream_error`). The credits are refunded automatically. (`Error`)
- `504`: The engine timed out (`upstream_timeout`). The credits are refunded automatically. (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/classify/batch' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"items":[{"description":"Men'\''s short-sleeve t-shirt, 100% cotton, knitted","destinationCountry":"US"},{"description":"Stainless steel water bottle, 750 ml","destinationCountry":"GB"}]}'
```

### POST /v1/classify/regulator

**Find an agency product code** (API key required)

The product code the destination’s import agency asks for, in that agency’s coding scheme: for the US, FDA’s import product code (e.g. `16AYN07`) for foods, drugs, devices, cosmetics and other goods FDA regulates. The answer names the `agency` and `scheme`, lists the code’s `parts` in order, and says whether the agency’s own check accepted it. Costs 1 credit; goods the agency doesn’t regulate are refused (`details.reason: "not_regulated"`, with `details.agency`) and refunded.

API key scope: `classify`.

Headers:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `idempotency-key` | string | no | Make retries safe: a repeat with the same key returns the original result without charging again (keys are scoped to your workspace). |

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `description` | string | yes | What the product is, what it is made of, and how it is processed or packed (frozen, smoked, canned…). |
| `destinationCountry` | string | yes | Where the goods are imported. Its import agency and coding scheme follow from it (for `US`, FDA’s import product codes). |
| `hsCode` | string | no | The goods’ tariff code, when known: a hint for the product pick. |

Responses:

- `200`: The agency product code and the credits charged. (`RegulatorClassifyResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `402`: Not enough credits (`insufficient_credits`). Nothing was charged. (`Error`)
- `403`: Workspace suspended, action disabled, or the key lacks the scope (`org_suspended`, `action_disabled`, `forbidden`). (`Error`)
- `409`: A request with this Idempotency-Key is still in progress (`conflict`). (`Error`)
- `422`: No agency product codes for this destination yet (`unsupported_country`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)
- `502`: The engine failed (`upstream_error`). The credits are refunded automatically. (`Error`)
- `504`: The engine timed out (`upstream_timeout`). The credits are refunded automatically. (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/classify/regulator' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"description":"<string>","destinationCountry":"<string>"}'
```

### GET /v1/classify/modes

**Classification modes** (public)

The classification modes you can use (admin configures them: today `mini`, `pro` and `max`), with what each costs and reads, in general or for one destination: whether it reads a product photo, and a photo sent inline, and which mode runs when a request names none. A product page URL works in every mode. No API key needed.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `destination` | string | no | ISO-2 destination: what each mode offers there (photos sent inline need border.bot’s own classifier). |

Responses:

- `200`: The modes. (`ClassifyModesResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/classify/modes'
```

### GET /v1/classify/blocklist

**Blocked codes** (API key required)

Codes your classifications never suggest: a blocked line, or every line under a blocked heading, is skipped and the next best answers. Free.

API key scope: `settings`.

Responses:

- `200`: Your blocked codes. (`BlockedCodesResponse`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `settings` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/classify/blocklist' \
  -H 'Authorization: Bearer bb_live_...'
```

### POST /v1/classify/blocklist

**Block a code** (API key required)

Never suggest this code (or any line under it) again, for one destination or all. Adding a code that is already blocked updates its reason. Free; up to 5,000 codes.

API key scope: `settings`.

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | string | yes |  |
| `destination` | "*" \| string | no |  |
| `reason` | string | no |  |

Responses:

- `200`: The blocked code. (`BlockedCodeResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `settings` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/classify/blocklist' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"code":"6109.10","destination":"US","reason":"Our broker files these under 6109.90"}'
```

### DELETE /v1/classify/blocklist

**Unblock a code** (API key required)

Lets classifications suggest the code again. Free.

API key scope: `settings`.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | string | yes |  |
| `destination` | "*" \| string | no |  |

Responses:

- `200`: Whether a blocked code was removed. (`BlockedCodeRemoveResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `settings` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X DELETE 'https://api.border.bot/v1/classify/blocklist' \
  -H 'Authorization: Bearer bb_live_...'
```

### POST /v1/classify/blocklist/import

**Block many codes** (API key required)

Block a list of codes at once, or with `replace: true` make the list exactly these. All or nothing: an import that would pass 5,000 codes changes nothing. Free.

API key scope: `settings`.

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `codes` | object[] | yes |  |
| `replace` | boolean | no |  |

Responses:

- `200`: What the import added, updated and removed. (`BlockedCodesImportResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `settings` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/classify/blocklist/import' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"codes":[{"code":"6109.10","destination":"US","reason":"Our broker files these under 6109.90"},{"code":"9503"}],"replace":false}'
```

### POST /v1/classify/feedback

**Say whether a classification was right** (API key required)

Tell border.bot a code was right, or what it should have been. Corrections are reviewed and become test cases the classifier must pass. With the classification’s `usageId`, sending again replaces your earlier verdict until it is reviewed. Free; up to 1,000 a day.

API key scope: `classify`.

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `usageId` | string | no |  |
| `destinationCountry` | string | yes |  |
| `description` | string | yes |  |
| `predictedCode` | string | yes |  |
| `verdict` | "right" \| "wrong" | yes |  |
| `correctCode` | string | no |  |
| `reason` | string | no |  |
| `mode` | string | no |  |
| `productUrl` | string (uri) | no |  |

Responses:

- `200`: The feedback was recorded. (`ClassifyFeedbackResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `classify` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/classify/feedback' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"usageId":"use_01J9ZK8Q7X4N2M","destinationCountry":"US","description":"Yoga mat, 6 mm, TPE","predictedCode":"3926909985","verdict":"wrong","correctCode":"9506910030","reason":"Exercise equipment, not an article of plastics"}'
```

## Landed cost

Duties, taxes and fees.

### POST /v1/calculate

**Calculate duties, taxes and landed cost** (API key required)

Duties, taxes, fees and the landed cost for one HS code / origin / destination (the destinations `GET /countries` marks `landedCost`). Costs 1 credit. Unsupported destinations return 422 `unsupported_country` without charging.

API key scope: `calculate`.

Headers:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `idempotency-key` | string | no | Make retries safe: a repeat with the same key returns the original result without charging again (keys are scoped to your workspace). |

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `destinationCountry` | string | yes | Destination: one `GET /countries` marks `landedCost`. ISO 3166-1 alpha-2, case-insensitive. |
| `currency` | string | no | ISO 4217 currency of `value`, `shippingCost` and `insuranceCost`. |
| `shippingCost` | number | no | Shipping cost (affects CIF-based duty/VAT bases). |
| `insuranceCost` | number | no | Insurance cost. |
| `transportMode` | "air" \| "sea" \| "road" \| "rail" | no | Transport mode (fees such as HMF apply to sea freight). |
| `shippingTerms` | "EXW" \| "FCA" \| "FAS" \| "FOB" \| "CFR" \| "CIF" \| "CPT" \| "CIP" \| "DAP" \| "DPU" \| "DDP" | no | Incoterm of the price (Incoterms 2020). Under C and D terms (CFR, CIF, CPT, CIP, DAP, DPU, DDP) the price already carries the goods to the destination, so `shippingCost` isn’t added to the customs value again. |
| `shipmentChannel` | "courier" \| "postal" | no | How the parcel travels: `courier` (express carriers, default) or `postal` — affects de minimis and channel-specific regimes. |
| `entryDate` | string | no | Calculate with the rates in force on this date (YYYY-MM-DD). Default: today. |
| `tradeAgreement` | string | no | Ignored. Preferential rates are applied automatically from the origin and destination; the agreement used is returned as `tradeAgreement`. |
| `region` | string | no | State, province or territory, where import taxes differ inside the country. Canada needs one (`ON`, `QC`, `BC`…). |
| `purpose` | "sale" \| "gift" \| "sample" \| "return" | no | Why the goods are sent: some thresholds differ for gifts, samples and returns. Default: `sale`. |
| `businessBuyer` | boolean | no | The buyer is a business: some taxes are reverse-charged or apply differently. |
| `sellerRegistrations` | string[] | no | Tax schemes the seller is registered for (`EU_IOSS`, `GB_VAT`, `AU_GST`, `NZ_GST`, `NO_VOEC`…). Taxes the seller collects at checkout are returned with `collectedBy: "seller"`. |
| `preference` | string | no | `best` (default): the lowest preferential rate the origin qualifies for. `none`: the general rate only. Or a programme code (`S` for USMCA into the US). Every option is returned in `options`. |
| `claims` | string[] | no | Exemption, relief and quota codes being claimed (US Chapter 99 exclusions such as `9903.88.69`; an end-use authorisation such as TARIC document `N990`; a tariff quota order number such as `050331`). Codes you could claim are returned in `claims.available`. |
| `enforceValidation` | boolean | no | Refuse a shipment that matches one of the destination’s `reject` validation rules (400 `invalid_input`, `details.reason: "validation_failed"`, refunded) instead of reporting it in `validation`. |
| `hsCode` | string | yes | HS/HTS code (4–10 digits; dots and spaces are ignored). |
| `originCountry` | string | yes | Country of origin — drives preferential rates and additional duties (e.g. Section 301). ISO 3166-1 alpha-2, case-insensitive. |
| `value` | number | yes | Total customs value of the goods in the shipment (unit price × quantity), in `currency`, excluding shipping. |
| `quantity` | integer | no | Number of units. |
| `weight` | number | no | Shipment weight (needed for weight-based duties). Requires `weightUnit`. |
| `weightUnit` | "kg" \| "lb" | no | Unit of `weight`. |
| `volumeLiters` | number | no | Volume of the goods in litres, for duties charged per litre (wine, spirits, fuel). |
| `alcoholPercent` | number | no | Alcohol by volume (%), for duties charged per litre of pure alcohol. |
| `components` | object[] | no | Metal content by value (line totals in `currency`), for duties charged on it: US Section 232 steel, aluminum and copper derivatives. Without it, those duties come back as `regulatory` (conditional). |
| `metalWeightPercent` | number | no | Share of the product’s weight that is metal (0–100), for content-based exemptions. |
| `conditions` | string[] | no | Rate conditions the goods meet, when the destination reserves a rate for them: a harmonised kind (`pharmaceutical`, `end_use`, `certificate`, `company`, `quality`, `route`) or the publisher’s own code (EU TARIC additional code `2500`). Without it the default rate is charged (the unconditional one, or the highest when every rate has a condition); the rates you could claim are returned in `options` with their `condition`. The importer must hold what justifies a claimed condition. |

Responses:

- `200`: The landed-cost breakdown and the credits charged. (`CalculateResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `402`: Not enough credits (`insufficient_credits`). Nothing was charged. (`Error`)
- `403`: Workspace suspended, action disabled, or the key lacks the scope (`org_suspended`, `action_disabled`, `forbidden`). (`Error`)
- `409`: A request with this Idempotency-Key is still in progress (`conflict`). (`Error`)
- `422`: Destination not supported (`unsupported_country`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)
- `502`: The engine failed (`upstream_error`). The credits are refunded automatically. (`Error`)
- `504`: The engine timed out (`upstream_timeout`). The credits are refunded automatically. (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/calculate' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"hsCode":"6109.10.00.12","originCountry":"CN","destinationCountry":"US","value":120,"currency":"USD","shippingCost":15}'
```

### POST /v1/calculate/shipment

**Calculate a whole shipment** (API key required)

Duties, taxes, fees and the landed cost of a cart or order to one destination: up to 50 lines, freight and insurance shared out by value, one de minimis check, and per-entry fees charged once. Costs 1 credit per line. Priced on border.bot’s own data: a destination it doesn’t cover yet returns 422 `unsupported_country` without charging.

API key scope: `calculate`.

Headers:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `idempotency-key` | string | no | Make retries safe: a repeat with the same key returns the original result without charging again (keys are scoped to your workspace). |

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `destinationCountry` | string | yes | Destination: one `GET /countries` marks `landedCost`. ISO 3166-1 alpha-2, case-insensitive. |
| `currency` | string | no | ISO 4217 currency of `value`, `shippingCost` and `insuranceCost`. |
| `shippingCost` | number | no | Shipping cost (affects CIF-based duty/VAT bases). |
| `insuranceCost` | number | no | Insurance cost. |
| `transportMode` | "air" \| "sea" \| "road" \| "rail" | no | Transport mode (fees such as HMF apply to sea freight). |
| `shippingTerms` | "EXW" \| "FCA" \| "FAS" \| "FOB" \| "CFR" \| "CIF" \| "CPT" \| "CIP" \| "DAP" \| "DPU" \| "DDP" | no | Incoterm of the price (Incoterms 2020). Under C and D terms (CFR, CIF, CPT, CIP, DAP, DPU, DDP) the price already carries the goods to the destination, so `shippingCost` isn’t added to the customs value again. |
| `shipmentChannel` | "courier" \| "postal" | no | How the parcel travels: `courier` (express carriers, default) or `postal` — affects de minimis and channel-specific regimes. |
| `entryDate` | string | no | Calculate with the rates in force on this date (YYYY-MM-DD). Default: today. |
| `tradeAgreement` | string | no | Ignored. Preferential rates are applied automatically from the origin and destination; the agreement used is returned as `tradeAgreement`. |
| `region` | string | no | State, province or territory, where import taxes differ inside the country. Canada needs one (`ON`, `QC`, `BC`…). |
| `purpose` | "sale" \| "gift" \| "sample" \| "return" | no | Why the goods are sent: some thresholds differ for gifts, samples and returns. Default: `sale`. |
| `businessBuyer` | boolean | no | The buyer is a business: some taxes are reverse-charged or apply differently. |
| `sellerRegistrations` | string[] | no | Tax schemes the seller is registered for (`EU_IOSS`, `GB_VAT`, `AU_GST`, `NZ_GST`, `NO_VOEC`…). Taxes the seller collects at checkout are returned with `collectedBy: "seller"`. |
| `preference` | string | no | `best` (default): the lowest preferential rate the origin qualifies for. `none`: the general rate only. Or a programme code (`S` for USMCA into the US). Every option is returned in `options`. |
| `claims` | string[] | no | Exemption, relief and quota codes being claimed (US Chapter 99 exclusions such as `9903.88.69`; an end-use authorisation such as TARIC document `N990`; a tariff quota order number such as `050331`). Codes you could claim are returned in `claims.available`. |
| `enforceValidation` | boolean | no | Refuse a shipment that matches one of the destination’s `reject` validation rules (400 `invalid_input`, `details.reason: "validation_failed"`, refunded) instead of reporting it in `validation`. |
| `items` | object[] | yes | The shipment’s lines (up to 50), each with its code, origin and line value. Freight and insurance are shared out by value. |

Responses:

- `200`: The shipment’s landed cost, line by line, and the credits charged. (`CalculateShipmentResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `402`: Not enough credits (`insufficient_credits`). Nothing was charged. (`Error`)
- `403`: Workspace suspended, action disabled, or the key lacks the scope (`org_suspended`, `action_disabled`, `forbidden`). (`Error`)
- `409`: A request with this Idempotency-Key is still in progress (`conflict`). (`Error`)
- `422`: Destination not covered by border.bot’s own data (`unsupported_country`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)
- `502`: The engine failed (`upstream_error`). The credits are refunded automatically. (`Error`)
- `504`: The engine timed out (`upstream_timeout`). The credits are refunded automatically. (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/calculate/shipment' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"destinationCountry":"GB","currency":"USD","shippingCost":12,"items":[{"hsCode":"6109.10","originCountry":"CN","value":60,"quantity":3},{"hsCode":"6204.62","originCountry":"BD","value":45}]}'
```

### POST /v1/calculate/stacking

**Duty stacking** (API key required)

Which duty lines apply to a code from an origin on a date, without a shipment: the base duty, then every additional measure (Section 301, 232, reciprocal…), which exemption codes remove which, the measures that depend on metal content or a claim, and the claim codes you could file. Up to 100 lines; free unless your pricing says otherwise. Answers from border.bot’s own data: a destination it doesn’t cover returns 422 `unsupported_country`.

API key scope: `calculate`.

Headers:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `idempotency-key` | string | no | Make retries safe: a repeat with the same key returns the original result without charging again (keys are scoped to your workspace). |

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | object[] | yes |  |

Responses:

- `200`: Each line’s stack. (`StackingResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `402`: Not enough credits (`insufficient_credits`). Nothing was charged. (`Error`)
- `403`: Workspace suspended, action disabled, or the key lacks the scope (`org_suspended`, `action_disabled`, `forbidden`). (`Error`)
- `409`: A request with this Idempotency-Key is still in progress (`conflict`). (`Error`)
- `422`: A destination border.bot’s own data doesn’t cover yet (`unsupported_country`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)
- `502`: The engine failed (`upstream_error`). The credits are refunded automatically. (`Error`)
- `504`: The engine timed out (`upstream_timeout`). The credits are refunded automatically. (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/calculate/stacking' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"items":[{"hsCode":"8479.89.94","originCountry":"CN","destinationCountry":"US"}]}'
```

## Products

Product pages and country of origin.

### POST /v1/products/extract

**Read a product page** (API key required)

Fetch a product page and extract its title, brand, price, materials, images and any stated country of origin.

API key scope: `origin`.

Headers:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `idempotency-key` | string | no | Make retries safe: a repeat with the same key returns the original result without charging again (keys are scoped to your workspace). |

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | Product page URL (http/https). |

Responses:

- `200`: The extracted product. (`ExtractProductResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `402`: Not enough credits (`insufficient_credits`). Nothing was charged. (`Error`)
- `403`: Workspace suspended, action disabled, or the key lacks the scope (`org_suspended`, `action_disabled`, `forbidden`). (`Error`)
- `409`: A request with this Idempotency-Key is still in progress (`conflict`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)
- `502`: The engine failed (`upstream_error`). The credits are refunded automatically. (`Error`)
- `504`: The engine timed out (`upstream_timeout`). The credits are refunded automatically. (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/products/extract' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"url":"<string>"}'
```

## Compliance

Restricted and prohibited goods (free).

### POST /v1/restrictions

**Check restricted goods** (API key required)

Check items against the destination’s import prohibitions and restrictions and, when `originCountry` is given, the origin’s export rules (sanctions, agency requirements such as FDA or APHIS), plus your own rules (`/restrictions/rules`). It uses no credits and is limited to 10 checks per minute per workspace (up to 100 items each). Destinations without coverage return 422 `unsupported_country`.

API key scope: `compliance`.

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `destinationCountry` | string | yes | Ship-to country — its import rules are checked. ISO 3166-1 alpha-2, case-insensitive. |
| `originCountry` | string | no | Ship-from country — its export rules are checked when given. ISO 3166-1 alpha-2, case-insensitive. |
| `items` | RestrictionItem[] | yes | 1–100 items. |
| `reference` | string | no | Your reference (an order or customer id), kept with the check as evidence. |

Responses:

- `200`: Matches per item (an empty `restrictions` list means nothing matched). (`RestrictionsResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `compliance` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `422`: Restricted-goods checks are not available for this destination (`unsupported_country`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)
- `502`: The engine failed (`upstream_error`). (`Error`)
- `504`: The engine timed out (`upstream_timeout`). (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/restrictions' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"destinationCountry":"US","originCountry":"CN","items":[{"sku":"LAMP-01","title":"Lithium battery desk lamp","hsCode":"9405.21"}]}'
```

### GET /v1/restrictions/checks

**Restricted-goods checks** (API key required)

Your restricted-goods checks, newest first (up to 200): the evidence of what was checked, for which lane, by which engine, and what matched. Free.

API key scope: `compliance`.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no |  |

Responses:

- `200`: Your checks. (`RestrictionChecksResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `compliance` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/restrictions/checks' \
  -H 'Authorization: Bearer bb_live_...'
```

### GET /v1/restrictions/rules

**Your restricted-goods rules** (API key required)

Rules your restricted-goods checks apply on top of border.bot’s (a compliance team’s own list). Free.

API key scope: `settings`.

Responses:

- `200`: Your rules. (`RestrictionRulesResponse`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `settings` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/restrictions/rules' \
  -H 'Authorization: Bearer bb_live_...'
```

### POST /v1/restrictions/rules

**Add a restricted-goods rule** (API key required)

A rule matched by HS code (and everything under it), by words in the item, or by origin, for one country or `*`. Free.

API key scope: `settings`.

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `country` | "*" \| string | no |  |
| `direction` | "import" \| "export" | no |  |
| `ruleType` | "prohibition" \| "restriction" \| "observation" | yes |  |
| `code` | string | no |  |
| `keywords` | string[] | no |  |
| `origins` | string[] | no |  |
| `title` | string | yes |  |
| `summary` | string | no |  |

Responses:

- `200`: The rule. (`RestrictionRuleResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `settings` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/restrictions/rules' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"country":"US","ruleType":"prohibition","code":"9304","keywords":["airsoft"],"title":"No airsoft or air guns (company policy)"}'
```

### DELETE /v1/restrictions/rules

**Remove a restricted-goods rule** (API key required)

Removes one of your rules by its id. Free.

API key scope: `settings`.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes | The rule’s id. |

Responses:

- `200`: Whether a rule was removed. (`RestrictionRuleRemoveResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `settings` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X DELETE 'https://api.border.bot/v1/restrictions/rules' \
  -H 'Authorization: Bearer bb_live_...'
```

### POST /v1/screen

**Screen a person or company** (API key required)

Check a name (and a company) against denied-party lists: the US Consolidated Screening List (the SDN list, the Entity List, the Denied Persons List and the other export-screening lists of Commerce, State and the Treasury). Names and aliases match exactly or fuzzily (from 0.7); each check is kept as your evidence. Free, up to 60 a minute per workspace. A match is a lead to review, not a verdict.

API key scope: `compliance`.

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | The person’s or company’s name, as you have it. |
| `company` | string | no | A company to screen too (a buyer and their employer, say). |
| `country` | string | no | The party’s country (ISO-2): matches with an address, nationality or flag there are flagged `countryMatch`. |
| `type` | "individual" \| "entity" | no | Only parties of this type. Vessels, aircraft and parties of unknown type are always screened. |
| `reference` | string | no | Your reference (an order or customer id), kept with the check as evidence. |

Responses:

- `200`: The matches, best first, and the lists screened. (`ScreenResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `compliance` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/screen' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"name":"<string>"}'
```

### GET /v1/screen/checks

**Your screening checks** (API key required)

Every screening your workspace ran, newest first: the evidence that a name was screened, when, against which lists, and what matched. Free.

API key scope: `compliance`.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no |  |

Responses:

- `200`: Your checks. (`ScreeningChecksResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `compliance` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/screen/checks' \
  -H 'Authorization: Bearer bb_live_...'
```

## Account

Your workspace, credits and usage.

### GET /v1/me

**Who am I** (API key required)

The workspace and API key behind this request, with the current credit balance.

Any API key may call this.

Responses:

- `200`: The calling workspace. (`MeResponse`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/me' \
  -H 'Authorization: Bearer bb_live_...'
```

### GET /v1/credits

**Credit balance** (API key required)

Current balance, lifetime totals and the 20 most recent ledger entries (purchases, usage, refunds).

API key scope: `account`.

Responses:

- `200`: Balance and recent ledger. (`CreditsResponse`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `account` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/credits' \
  -H 'Authorization: Bearer bb_live_...'
```

### GET /v1/usage

**Usage history** (API key required)

Every billable request made by the workspace (dashboard, API and MCP), newest first, with cursor pagination.

API key scope: `account`.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Page size (1–100). |
| `cursor` | string | no | Opaque cursor from a previous page. |
| `action` | "classify" \| "classify_max" \| "classify_regulator" \| "calculate" \| "calculate_stacking" \| "extract_product" \| "infer_origin" | no | Only this action. |

Responses:

- `200`: A page of usage events. (`UsageResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `account` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/usage' \
  -H 'Authorization: Bearer bb_live_...'
```

### GET /v1/requests

**Request log** (API key required)

Every request your API keys made in the last 30 days, newest first: what it called, how it ended (status and error code), how long it took, and, for requests that change something, the body. Quote a request's `requestId` (its `X-Request-Id`) to support.

API key scope: `account`.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Page size (1–100). |
| `cursor` | string | no | The `next` of the previous page. |
| `failed` | "true" \| "false" | no | `true`: only requests that failed (status 400 and up). |
| `requestId` | string | no | One request, by the `X-Request-Id` its response carried. |

Responses:

- `200`: A page of requests. (`ApiRequestsResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `account` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/requests' \
  -H 'Authorization: Bearer bb_live_...'
```

## Reference

Public reference data (no key needed).

### GET /v1/countries

**Supported countries** (public)

All ISO countries with the nomenclature classification answers in (a 10-digit national tariff where border.bot has one, 6-digit HS elsewhere) and whether landed-cost calculation is supported. No API key needed.

Responses:

- `200`: Country coverage. (`CountriesResponse`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/countries'
```

### GET /v1/pricing

**Pricing** (public)

Credits per action, credit packs and free-tool limits. No API key needed.

Responses:

- `200`: Current pricing. (`PricingResponse`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/pricing'
```

## Origin

### POST /v1/origin

**Infer country of origin** (API key required)

Where a product is made, as a probability per country with the evidence behind each: the product page’s data, “Made in …” text, a label in the photo, an AI estimate from brand, materials and price, the ship-from country and the barcode’s GS1 prefix. An AI estimate alone is never reported as high confidence. `needsReview` (with `reviewReason`) says when a person should confirm, because origin drives duty rates: below `reviewThreshold` (the request’s, else the workspace’s setting, else the platform default of 0.8), or when strong evidence disagrees.

API key scope: `origin`.

Headers:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `idempotency-key` | string | no | Make retries safe: a repeat with the same key returns the original result without charging again (keys are scoped to your workspace). |

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `productUrl` | string | no | Product page URL: its structured data and “Made in …” text are read first. |
| `imageUrl` | string | no | Product photo (JPEG, PNG, WebP or GIF, up to 6 MB), as an http(s) URL or a base64 data URL: a visible “Made in …” label is strong evidence. |
| `description` | string | no | Product description. |
| `title` | string | no | Product name. |
| `brand` | string | no | Brand. |
| `sku` | string | no | Your SKU (kept with the result). |
| `gtin` | string | no | Barcode (GTIN, UPC, EAN). Its GS1 prefix shows where the brand registered: a weak hint, never proof. |
| `material` | string | no | Main materials. |
| `categories` | string[] | no | Category path, broadest first. |
| `price` | number | no | Selling price, in `currency`. |
| `currency` | string | no | ISO 4217 currency of `price`. |
| `shipFromCountry` | string | no | Country the goods ship from (often, not always, where they are made). ISO 3166-1 alpha-2, case-insensitive. |
| `reviewThreshold` | number | no | When `needsReview` is set (0–1). Inferring: an answer below this probability. Validating: a declaration whose `probabilityOfMisrepresentation` is at or above it. Without it, the workspace’s setting (dashboard → Settings → Origin review) applies, then the platform default (0.8 to infer, 0.3 to validate). |

Responses:

- `200`: The most likely origin, its probability, the alternates and the evidence. (`InferOriginResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `402`: Not enough credits (`insufficient_credits`). Nothing was charged. (`Error`)
- `403`: Workspace suspended, action disabled, or the key lacks the scope (`org_suspended`, `action_disabled`, `forbidden`). (`Error`)
- `409`: A request with this Idempotency-Key is still in progress (`conflict`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)
- `502`: The engine failed (`upstream_error`). The credits are refunded automatically. (`Error`)
- `504`: The engine timed out (`upstream_timeout`). The credits are refunded automatically. (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/origin' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"title":"Organic cotton tee","brand":"Example","productUrl":"https://shop.example.com/products/organic-tee"}'
```

### POST /v1/origin/validate

**Check a declared country of origin** (API key required)

How believable a declared origin is, given everything else known about the product: the share of the evidence pointing elsewhere (`probabilityOfMisrepresentation`), a verdict, the likely origin, and the reasons in words. With no evidence either way the verdict is `unknown`, never a guess. `needsReview` flags it when the misrepresentation probability reaches `reviewThreshold` (the request’s, else the workspace’s setting, else the platform default of 0.3), or when there is no evidence.

API key scope: `origin`.

Headers:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `idempotency-key` | string | no | Make retries safe: a repeat with the same key returns the original result without charging again (keys are scoped to your workspace). |

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `productUrl` | string | no | Product page URL: its structured data and “Made in …” text are read first. |
| `imageUrl` | string | no | Product photo (JPEG, PNG, WebP or GIF, up to 6 MB), as an http(s) URL or a base64 data URL: a visible “Made in …” label is strong evidence. |
| `description` | string | no | Product description. |
| `title` | string | no | Product name. |
| `brand` | string | no | Brand. |
| `sku` | string | no | Your SKU (kept with the result). |
| `gtin` | string | no | Barcode (GTIN, UPC, EAN). Its GS1 prefix shows where the brand registered: a weak hint, never proof. |
| `material` | string | no | Main materials. |
| `categories` | string[] | no | Category path, broadest first. |
| `price` | number | no | Selling price, in `currency`. |
| `currency` | string | no | ISO 4217 currency of `price`. |
| `shipFromCountry` | string | no | Country the goods ship from (often, not always, where they are made). ISO 3166-1 alpha-2, case-insensitive. |
| `reviewThreshold` | number | no | When `needsReview` is set (0–1). Inferring: an answer below this probability. Validating: a declaration whose `probabilityOfMisrepresentation` is at or above it. Without it, the workspace’s setting (dashboard → Settings → Origin review) applies, then the platform default (0.8 to infer, 0.3 to validate). |
| `declaredOrigin` | string | yes | The country of origin declared (by a supplier, a listing, a customs entry). ISO 3166-1 alpha-2, case-insensitive. |

Responses:

- `200`: The verdict on the declared origin, with reasons. (`ValidateOriginResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `402`: Not enough credits (`insufficient_credits`). Nothing was charged. (`Error`)
- `403`: Workspace suspended, action disabled, or the key lacks the scope (`org_suspended`, `action_disabled`, `forbidden`). (`Error`)
- `409`: A request with this Idempotency-Key is still in progress (`conflict`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)
- `502`: The engine failed (`upstream_error`). The credits are refunded automatically. (`Error`)
- `504`: The engine timed out (`upstream_timeout`). The credits are refunded automatically. (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/origin/validate' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"declaredOrigin":"US","title":"Wireless earbuds","description":"Bluetooth 5.3. Made in China."}'
```

### POST /v1/origin/batch

**Infer or check the origin of up to 50 products** (API key required)

Each item is inferred, or validated when it has `declaredOrigin`. Items run four at a time, each charged as one origin call; one failing (bad photo, insufficient credits) never stops the others. With an `Idempotency-Key`, each item replays on its own.

API key scope: `origin`.

Headers:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `idempotency-key` | string | no | Make retries safe: a repeat with the same key returns the original result without charging again (keys are scoped to your workspace). |

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | object[] | yes |  |
| `reviewThreshold` | number | no | The review threshold for every item that doesn’t set its own `reviewThreshold` (see `POST /v1/origin`). |

Responses:

- `200`: One result per item, in order. (`OriginBatchResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `402`: Not enough credits (`insufficient_credits`). Nothing was charged. (`Error`)
- `403`: Workspace suspended, action disabled, or the key lacks the scope (`org_suspended`, `action_disabled`, `forbidden`). (`Error`)
- `409`: A request with this Idempotency-Key is still in progress (`conflict`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)
- `502`: The engine failed (`upstream_error`). The credits are refunded automatically. (`Error`)
- `504`: The engine timed out (`upstream_timeout`). The credits are refunded automatically. (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/origin/batch' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"items":[{"title":"Organic cotton tee","brand":"Example"},{"title":"Wireless earbuds","declaredOrigin":"US","gtin":"6901234567892"}]}'
```

## Bulk

### GET /v1/bulk/runs

**Your bulk runs** (API key required)

Your workspace’s bulk runs, newest first.

API key scope: `bulk`.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no |  |

Responses:

- `200`: The runs. (`BulkRunsResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `bulk` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/bulk/runs' \
  -H 'Authorization: Bearer bb_live_...'
```

### POST /v1/bulk/runs

**Start a bulk run** (API key required)

Classify, calculate, find agency product codes or find and check countries of origin for up to 10,000 items in the background. Each item is charged when it runs (as its single request would be) and refunded if it fails; a run stops when your credits run out. Follow it with `GET /bulk/status`, read its answers with `GET /bulk/results`.

API key scope: `bulk`.

Request body (JSON):

| Name | Type | Required | Description |
| --- | --- | --- | --- |

Responses:

- `200`: The run, queued. (`BulkRunResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `bulk` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X POST 'https://api.border.bot/v1/bulk/runs' \
  -H 'Authorization: Bearer bb_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"kind":"classify","reference":"catalogue-2026-10","items":[{"description":"Men'\''s cotton t-shirt","destinationCountry":"US"},{"description":"Stainless steel water bottle, 750 ml","destinationCountry":"GB"}]}'
```

### DELETE /v1/bulk/runs

**Cancel a bulk run** (API key required)

Stops a run after the chunk it is working on. Items already done stay done (and charged).

API key scope: `bulk`.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

Responses:

- `200`: The run, cancelled. (`BulkRunResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `bulk` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X DELETE 'https://api.border.bot/v1/bulk/runs' \
  -H 'Authorization: Bearer bb_live_...'
```

### GET /v1/bulk/status

**A bulk run’s progress** (API key required)

How far a run has got: items done, succeeded and failed, and the credits charged so far.

API key scope: `bulk`.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

Responses:

- `200`: The run. (`BulkRunResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `bulk` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/bulk/status' \
  -H 'Authorization: Bearer bb_live_...'
```

### GET /v1/bulk/results

**A bulk run’s results** (API key required)

The answers so far, in item order, up to 1,000 at a time (`next` is the following page’s `offset`). Each is exactly what its single endpoint returns, or the error it failed with.

API key scope: `bulk`.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `offset` | integer \| null | no |  |
| `limit` | integer | no |  |

Responses:

- `200`: A page of results. (`BulkResultsResponse`)
- `400`: Invalid input (`invalid_input`). (`Error`)
- `401`: Missing, invalid, revoked or expired API key (`unauthorized`). (`Error`)
- `403`: The API key doesn’t have the `bulk` scope (`forbidden`, `details.reason: "missing_scope"`). (`Error`)
- `429`: Rate limited (`rate_limited`). Retry after `Retry-After` seconds; `RateLimit` says which limit was hit (`r=0`, `t` seconds until its window ends) and `RateLimit-Policy` its quota. (`Error`)
- `500`: Unexpected error (`internal`). (`Error`)

```bash
curl -X GET 'https://api.border.bot/v1/bulk/results' \
  -H 'Authorization: Bearer bb_live_...'
```

Interactive reference: https://api.border.bot/v1/docs/interactive
