API Documentation
Integrate TTClassify into your systems with our Upload APIs. Post purchase orders and products for automated tariff classification.
Overview
TTClassify has been built from the ground up to be RESTful and secure. Use your credentials to connect and obtain a token, then use that token to make calls to our system.
All endpoints are protected with OAuth 2.0 Client Credentials via Microsoft Entra ID. You must obtain an access token and send it as a Bearer token in the Authorization header on every request.
Base Endpoints
/api/v2/system/purchase-orders/upload/api/v2/system/products/uploadRequest Content-Type
application/jsonToken Request
application/x-www-form-urlencodedToken Retrieval
Authenticate using Microsoft Entra ID Client Credentials flow. Request an access token from the token endpoint below, then include it as a Bearer token in all subsequent API calls.
https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token The tenant-id, client_id, client_secret, and scope values are provided during onboarding. to get your API credentials.
Form Fields
| Field | Type | Required | Notes |
|---|---|---|---|
| grant_type | string | Yes | Must be client_credentials |
| client_id | string | Yes | Provided per tenant |
| client_secret | string | Yes | Provided per tenant |
| scope | string | Yes | Use api://<api-client-id>/.default (value provided by TariffTel) |
Sample Request
curl -X POST 'https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id=<your-client-id>' \ --data-urlencode 'client_secret=<your-client-secret>' \ --data-urlencode 'scope=api://<api-client-id>/.default'
Sample Response
{
"token_type": "Bearer",
"expires_in": 3599,
"ext_expires_in": 3599,
"access_token": "eyJ0eXAiOiJKV1QiL..."
} Use the access_token value in the Authorization header of all API requests: Authorization: Bearer eyJ0eXAiOiJKV1QiL...
Common Request Requirements
Headers
Authorization: Bearer <token>Content-Type: application/jsonAccept: application/json
Batching
Both upload endpoints accept a JSON array of items in a single request.
Rate Limiting
Both endpoints are rate limited to 30 requests per minute per client.
Purchase Orders Upload
TTClassify takes a purchase order and classifies each item listed. The specification below allows this to happen automatically using a structured, nested request body.
/api/v2/system/purchase-orders/uploadRate limited to 30 requests per rolling 1-minute fixed window.
Request Body Example
[
{
"orderNumber": "ORD-12122",
"supplierIdentifier": "THOC",
"dueDateForClassification": "2025-11-05",
"product": {
"code": "PRD-ORD-12121",
"description": "Grated Cheese",
"productDetails": {
"primarySize": "NA",
"unitCost": 1.95,
"currencyCode": "GBP",
"gender": "NA",
"productGroup": "Food & Drink - Cheese"
},
"movement": {
"countryOfOriginCode": "CN",
"destinationCountryCode": "GB"
}
},
"orderDetails": {
"incoTermCode": "",
"incoTermDescription": "",
"shipmentMethod": "",
"orderLineTypeCode": "string",
"orderLineTypeDescription": "string",
"orderQuantity": 2
}
}
]Request Body Example (minimal)
[
{
"orderNumber": "ORD-12122",
"supplierIdentifier": "THOC",
"dueDateForClassification": "2025-11-05",
"product": {
"code": "PRD-ORD-12121",
"description": "Grated Cheese",
"movement": {
"destinationCountryCode": "GB"
}
}
}
]Field Reference
Order
| Field | Type | Notes |
|---|---|---|
| orderNumber | string | Required. Unique order ref within the organization. Max 50 chars. |
| supplierIdentifier | string | Required. Supplier code known to TariffTel; the supplier must already exist. Max 50 chars. |
| dueDateForClassification | string (yyyy-MM-dd) | Required for SLA routing; UTC recommended. |
| product | object | Required. The product being ordered (see below). |
| orderDetails | object | Optional. Additional order line details (see below). |
product
| Field | Type | Notes |
|---|---|---|
| code | string | Required. Your product/SKU identifier. Max 100 chars. |
| description | string | Required. Human-readable description; read by TCAS to suggest classifications. Max 500 chars. |
| productDetails | object | Optional (see below). |
| movement | object | Required for orders (see below). |
product.productDetails (optional)
| Field | Type | Notes |
|---|---|---|
| primarySize | string | Optional (e.g., NA). Max 50 chars. Required only when product group is Garment/Footwear. |
| unitCost | number | Optional; if provided must be > 0. Up to 15 digits before the decimal point and 4 after. Used for duty estimation. |
| currencyCode | string | ISO currency (e.g., GBP). Max 10 chars. |
| gender | string | Optional (e.g., NA). Max 250 chars. |
| productGroup | string | E.g., Food & Drink - Cheese. Max 500 chars. |
product.movement (required for orders)
| Field | Type | Notes |
|---|---|---|
| countryOfOriginCode | string | ISO 3166-1 alpha-2 (e.g., CN). Max 2 chars. |
| destinationCountryCode | string | Required. ISO 3166-1 alpha-2 (e.g., GB). Max 2 chars. Must differ from countryOfOriginCode. |
orderDetails (optional)
| Field | Type | Notes |
|---|---|---|
| incoTermCode | string | Optional; e.g., FOB, DDP. Max 50 chars. |
| incoTermDescription | string | Optional free text. Max 50 chars. |
| shipmentMethod | string | Optional; e.g., Sea, Air. Max 1000 chars. |
| orderLineTypeCode | string | Optional code for line type. Max 50 chars. |
| orderLineTypeDescription | string | Optional description for line type. Max 100 chars. |
| orderQuantity | number (integer) | Optional; if provided must be > 0. |
Sample Request
curl -X POST 'https://<your-api-host>/api/v2/system/purchase-orders/upload' \ -H 'Authorization: Bearer <access_token>' \ -H 'Content-Type: application/json' \ -d '[ { "orderNumber": "ORD-12122", "supplierIdentifier": "THOC", "dueDateForClassification": "2025-11-05", "product": { "code": "PRD-ORD-12121", "description": "Grated Cheese", "productDetails": { "primarySize": "NA", "unitCost": 1.95, "currencyCode": "GBP", "gender": "NA", "productGroup": "Food & Drink - Cheese" }, "movement": { "countryOfOriginCode": "CN", "destinationCountryCode": "GB" } }, "orderDetails": { "incoTermCode": "", "incoTermDescription": "", "shipmentMethod": "", "orderLineTypeCode": "string", "orderLineTypeDescription": "string", "orderQuantity": 2 } } ]'
Typical Responses
Products Upload
TTClassify takes a product list and classifies each item listed. The specification below allows this to happen automatically using a structured, nested request body.
/api/v2/system/products/uploadRate limited to 30 requests per minute.
Request Body Example
[
{
"code": "PRD3312",
"description": "Grated Cheese",
"productDetails": {
"primarySize": "N/A",
"unitCost": 1.95,
"currencyCode": "GBP",
"gender": "N/A",
"productGroup": "Food & Drink - Cheese"
},
"movement": {
"countryOfOriginCode": "CN",
"destinationCountryCode": "GB"
}
}
]Request Body Example (minimal)
[
{
"code": "PRD3312",
"description": "Grated Cheese"
}
]Field Reference
Product (top-level)
| Field | Type | Notes |
|---|---|---|
| code | string | Required. Unique product/SKU code within the organization. Max 100 chars. |
| description | string | Required. Human-readable description; read by TCAS to suggest classifications. Max 500 chars. |
| productDetails | object | Optional (see below). |
| movement | object | Optional (see below). |
productDetails (optional)
| Field | Type | Notes |
|---|---|---|
| primarySize | string | Optional (e.g., N/A). Max 50 chars. Required only when product group is Garment/Footwear. |
| unitCost | number | Optional; if provided must be > 0. Up to 15 digits before the decimal point and 4 after. Used for duty estimation. |
| currencyCode | string | ISO currency (e.g., GBP). Max 10 chars. |
| gender | string | Optional (e.g., N/A). Max 250 chars. |
| productGroup | string | Category to aid classification (e.g., Food & Drink - Cheese). Max 500 chars. |
movement (optional)
| Field | Type | Notes |
|---|---|---|
| countryOfOriginCode | string | ISO 3166-1 alpha-2 (e.g., CN). Max 2 chars. |
| destinationCountryCode | string | ISO 3166-1 alpha-2 (e.g., GB). Max 2 chars. |
Both-or-none rule: for a standalone product upload, movement may be omitted entirely, but if it is supplied then both countryOfOriginCode and destinationCountryCode must be provided (one alone is rejected), and the two must resolve to different countries.
Sample Request
curl -X POST 'https://<your-api-host>/api/v2/system/products/upload' \ -H 'Authorization: Bearer <access_token>' \ -H 'Content-Type: application/json' \ -d '[ { "code": "PRD3312", "description": "Grated Cheese", "productDetails": { "primarySize": "N/A", "unitCost": 1.9500, "currencyCode": "GBP", "gender": "N/A", "productGroup": "Food & Drink - Cheese" }, "movement": { "countryOfOriginCode": "CN", "destinationCountryCode": "GB" } } ]'
Typical Responses
Get Product Classifications
Returns the current classification for a single product, identified by its code.
/api/v1/system/classifications/product?code=<code>Rate limited to 30 requests per minute (shared SystemToSystem policy).
Query Parameters
| Parameter | Type | Notes |
|---|---|---|
| code | string | Required. The product/SKU code. Whitespace is trimmed; an empty/missing value returns 400. |
Sample Request
curl -X GET 'https://<your-api-host>/api/v1/system/classifications/product?code=PRD3312' \ -H 'Authorization: Bearer <access_token>'
Response Body Example (200 OK)
{
"product": {
"code": "PRD3312",
"description": "Grated Cheese",
"status": "Active",
"primarySize": "N/A",
"primaryTariffCodeUnitCost": 1.95,
"secondaryTariffCodeUnitCost": null,
"unitCostCodeCurrencyDescription": "GBP",
"genderDescription": "N/A",
"unitCount": 1
},
"classification": {
"status": "Approved",
"rejectionReason": null,
"import": {
"importRegionCode": "GB",
"destinationCountryCode": "GB",
"countryOfOriginCode": "CN"
},
"attributes": {
"Type of Cheese": "Grated"
},
"customsDescription": "Grated cheese, prepacked",
"isPrimaryClassification": true,
"tariffCode": "0406200000",
"vatRatePercent": 20,
"dutyRate": "10.00 %",
"preferentialDutyRate": null,
"selectedDutyRate": "non-preferential",
"dutyRateJustification": "Goods do not qualify as originating products...",
"meursingCode": null,
"validityStartDate": "2025-01-01T00:00:00Z",
"dueDate": "2025-03-18T00:00:00Z",
"createdAt": "2026-05-26T16:05:46Z",
"createdBy": "Tiffany Rhodes",
"lastUpdatedAt": "2026-05-26T16:06:12Z",
"lastUpdatedBy": "Ben O'Neill",
"submittedAt": "2026-05-26T16:05:46Z",
"submittedBy": "Tiffany Rhodes",
"audit": [
{
"action": "Status changed from Submitted to Approved",
"info": null,
"at": "2026-05-26T16:06:12Z",
"by": "Ben O'Neill"
}
]
}
}Field Reference
product
| Field | Type | Notes |
|---|---|---|
| code | string | Product/SKU code. |
| description | string | Human-readable description. |
| status | string | Product status (e.g., Active). |
| primarySize | string | null | Optional. |
| primaryTariffCodeUnitCost | number | null | Primary unit cost used for duty estimation. |
| secondaryTariffCodeUnitCost | number | null | Secondary unit cost, if applicable. |
| unitCostCodeCurrencyDescription | string | null | ISO currency (e.g., GBP). |
| genderDescription | string | null | Optional. |
| unitCount | integer | null | Number of units. |
classification
| Field | Type | Notes |
|---|---|---|
| status | string | Classification status (e.g., Pending, Submitted, Approved, Rejected). |
| rejectionReason | object | null | Present when rejected; { reason, notes }. |
| import | object | Movement details (see below). |
| attributes | object (map) | Key/value classification attributes. |
| customsDescription | string | null | Customs description of the goods. |
| isPrimaryClassification | boolean | Whether this is the primary classification. |
| tariffCode | string | null | Assigned commodity/tariff code. |
| vatRatePercent | number | null | VAT rate as a percentage. |
| dutyRate | string | null | Standard (non-preferential) duty rate. |
| preferentialDutyRate | string | null | Preferential duty rate, if any. |
| selectedDutyRate | string | null | The duty rate type selected. |
| dutyRateJustification | string | null | Justification for the selected duty rate. |
| meursingCode | string | null | Meursing code, if applicable. |
| validityStartDate | date-time | null | Classification validity start. |
| dueDate | date-time | null | Classification due date. |
| createdAt / createdBy | date-time / string | Creation audit. |
| lastUpdatedAt / lastUpdatedBy | date-time / string | Last-update audit. |
| submittedAt / submittedBy | date-time | null / string | null | Submission audit. |
| audit | array | Audit events; each { action, info, at, by }. |
classification.import
| Field | Type | Notes |
|---|---|---|
| importRegionCode | string | Import region code. |
| destinationCountryCode | string | null | ISO 3166-1 alpha-2 destination. |
| countryOfOriginCode | string | ISO 3166-1 alpha-2 origin. |
Typical Responses
| Status | Meaning |
|---|---|
| 200 OK | Classification found; body as above. |
| 400 Bad Request | code missing or blank. Body: { "error": "Product code is required." } |
| 401 / 403 | Invalid/missing token or insufficient scope. |
| 404 Not Found | No product/classification matches the supplied code. Body: { "error": "<description>" } |
| 503 Service Unavailable | Rate limit exceeded. |
| 500 Internal Server Error | Unexpected error. Body: { "error": "<description>" } |
Get Order Classifications
Returns the classification state of every line on an order, including a summary and per-item audit trail.
/api/v1/system/order-classifications?orderNumber=<orderNumber>Rate limited to 30 requests per minute (shared SystemToSystem policy).
Query Parameters
| Parameter | Type | Notes |
|---|---|---|
| orderNumber | string | Required. The order number. An empty/missing value returns 400. |
Sample Request
curl -X GET 'https://<your-api-host>/api/v1/system/order-classifications?orderNumber=1643761' \ -H 'Authorization: Bearer <access_token>'
Response Body Example (200 OK)
{
"orderNumber": "1643761",
"classificationSummary": {
"totalItems": 1,
"classifiedItems": 1,
"pendingItems": 0
},
"orderLines": [
{
"destination": {
"code": "GB",
"description": "United Kingdom",
"importRegion": {
"code": "GB",
"description": "United Kingdom"
}
},
"supplier": {
"identifier": "CT002",
"supplierName": "ChefTech Equipment Co"
},
"classificationStatus": "Approved",
"product": {
"code": "945315842",
"description": "Broad beans 650g",
"type": "Single",
"items": [
{
"origin": {
"code": "IN",
"description": "India"
},
"code": "945315842",
"description": "Broad beans 650g",
"classification": {
"primary": false,
"dueDate": "2025-03-18T00:00:00Z",
"tariffCode": "0708900010",
"status": "Approved",
"auditTrail": [
{
"oldStatus": "Pending",
"newStatus": "Submitted",
"timestamp": "2026-05-26T16:05:46.6067173Z",
"changedBy": {
"firstName": "Tiffany",
"lastName": "Rhodes",
"email": "[email protected]",
"organizationName": "ChefTech Equipment Co"
},
"notes": "System: Status changed from Pending to Submitted",
"submission": {
"productGroup": "Food & Drink - Vegetables - Not Prepared - Fresh or Chilled",
"attributes": [
{
"name": "Type of Vegetable",
"value": "Beans - Broad (Vicia Faba major L.)"
}
],
"tariffCode": "0708900010"
},
"rejection": null
}
],
"customsDescription": "Beans - Broad (Vicia Faba major L.)",
"customsCompliance": {
"dutyRate": "10.00 %",
"vatRate": null,
"preferentialDutyRate": null,
"selectedDutyRateType": "non-preferential",
"preferentialJustification": "I declare that the goods... non-preferential tariff rates under customs regulations.",
"meursingCode": null
}
}
}
]
}
}
]
}Field Reference
Top level
| Field | Type | Notes |
|---|---|---|
| orderNumber | string | The order number echoed back. |
| classificationSummary | object | Counts (see below). |
| orderLines | array | One entry per order line (see below). |
classificationSummary
| Field | Type | Notes |
|---|---|---|
| totalItems | integer | Total items on the order. |
| classifiedItems | integer | Items that have been classified. |
| pendingItems | integer | Items still pending classification. |
orderLines[]
| Field | Type | Notes |
|---|---|---|
| destination | object | null | { code, description, importRegion: { code, description } }. |
| supplier | object | { identifier, supplierName }. |
| classificationStatus | string | Line-level status. |
| product | object | The ordered product (see below). |
orderLines[].product
| Field | Type | Notes |
|---|---|---|
| code | string | Product code. |
| description | string | Product description. |
| type | string | null | Product type (e.g., Single). |
| items | array | Per-origin product items (see below). |
orderLines[].product.items[]
| Field | Type | Notes |
|---|---|---|
| origin | object | { code, description } — country of origin. |
| code | string | Item code. |
| description | string | Item description. |
| classification | object | null | Classification detail (see below). |
items[].classification
| Field | Type | Notes |
|---|---|---|
| primary | boolean | Whether this is the primary classification. |
| dueDate | date-time | null | Classification due date. |
| tariffCode | string | null | Assigned tariff code. |
| status | string | Classification status. |
| auditTrail | array | Status-change history (see below). |
| customsDescription | string | Customs description. |
| customsCompliance | object | null | { dutyRate, vatRate, preferentialDutyRate, selectedDutyRateType, preferentialJustification, meursingCode }. |
classification.auditTrail[]
| Field | Type | Notes |
|---|---|---|
| oldStatus / newStatus | string | null | Status transition (null for non-status events). |
| timestamp | date-time | When the event occurred. |
| changedBy | object | null | { firstName, lastName, email, organizationName }. |
| notes | string | null | Free-text note. |
| submission | object | null | Present on submission events: { productGroup, attributes: [{ name, value }], tariffCode }. |
| rejection | object | null | Present on rejection events: { reason, notes }. |
Typical Responses
| Status | Meaning |
|---|---|
| 200 OK | Order found; body as above. |
| 400 Bad Request | orderNumber missing or blank. Body: { "error": "orderNumber is required." } |
| 401 / 403 | Invalid/missing token or insufficient scope. |
| 404 Not Found | No order matches the supplied orderNumber. Body: { "error": "<description>" } |
| 503 Service Unavailable | Rate limit exceeded. |
| 500 Internal Server Error | Unexpected error. Body: { "error": "<description>" } |
Ready to integrate?
Speak to our development team about your project and we'll get you set up with credentials and onboarding support.