TRADE Feed Session Contract
Status: Active
Version: v1
Last updated: 2026-04-08
This document is the normative contract for the public TRADE feed workflow surface. It is the workflow-semantics companion to the OpenAPI artifact
docs/api/openapi/rgl8r-public-api-v1.2.0.yaml, which documents the request/response shapes. This file documents the state machine, idempotency, error envelopes, and the operator handoff that YAML cannot express.
Routes in scope
All routes listed below are callable with an integration-key Bearer JWT obtained via POST /api/auth/token/integration. They are mounted under the tenant route middleware and run authenticate + validateIntegrationToken.
| Method | Route | Purpose |
|---|---|---|
| POST | /api/trade/feeds/upload | Upload a CSV/Excel file into a durable feed session |
| GET | /api/trade/feeds/:sessionId | Get session status + normalization progress |
| GET | /api/trade/feeds/:sessionId/skus | Paginated SKU list for the normalized artifact |
| GET | /api/trade/feeds/:sessionId/issues | Paginated blocking + non-blocking issues |
| POST | /api/trade/feeds/:sessionId/classify-preview | Enqueue a preview classification job |
| PUT | /api/trade/classify/preview/:jobId/review | Persist the reviewed snapshot |
| POST | /api/trade/feeds/:sessionId/apply | Apply the reviewed feed to the catalog |
Routes intentionally excluded from the public agent surface:
POST /api/trade/feeds/:sessionId/issues/:issueKey/acknowledgeis Clerk-only and is the operator-triggered path to acknowledge non-blocking issues in aPREVIEW_BLOCKEDsession. It is not callable with an integration-key Bearer JWT. See “Issue handling and operator handoff” below.
Session state machine
UPLOADED ─▶ NORMALIZING ─┬─▶ READY ─▶ APPLIED
│
├─▶ PREVIEW_BLOCKED ─ (operator acknowledge) ─▶ READY
│
└─▶ FAILEDState definitions:
UPLOADED— file has been accepted and persisted to durable storage, normalization not yet started.NORMALIZING— worker is parsing and normalizing the feed into the canonical artifact.READY— normalization completed with no blocking issues; the session is eligible forclassify-previewandapply.PREVIEW_BLOCKED— normalization surfaced non-blocking issues that require operator acknowledgement before preview and apply can proceed. This is a first-class state, not an error. Customers cannot self-acknowledge from the agent surface.FAILED— normalization errored; the underlying reason is exposed via session detail and issue list. Retry from the raw blob is supported via the operator runbook; the public agent surface does not expose the retry endpoint.APPLIED— the reviewed preview has been written to the tenant catalog; terminal.
The canonical state set is defined in apps/api/src/lib/trade/feed-types.ts and surfaced by apps/api/src/routes/trade-feeds.ts.
Happy path (READY → APPLIED)
- Exchange the integration key for a Bearer JWT.
POST /api/trade/feeds/uploadwith a multipartfilefield plus two required form fields:originCountry(ISO 3166-1 alpha-2, e.g.CN) anddefaultDestinationCountry(e.g.US). These are the feed-level defaults applied to rows that omit their own origin/destination. Individual CSV rows can override via columns. The endpoint returns202 Acceptedwith an envelope of{ sessionId, jobId, status, fileName, replayed? }— the session and its background normalization job are created synchronously, but normalization itself runs asynchronously. The response is an acceptance receipt, not a completion notification.- Poll
GET /api/trade/feeds/:sessionIduntilstatusreachesREADY,PREVIEW_BLOCKED, orFAILED. Use the same polling backoff as the public-contract job loop: every 2–3s for the first minute, then every 5–10s with jitter. POST /api/trade/feeds/:sessionId/classify-preview— enqueues a preview classification job. Response includes ajobId(poll viaGET /api/jobs/:id) and apreviewJobIdfor the next step.- When the preview job reaches
COMPLETED,PUT /api/trade/classify/preview/:jobId/reviewto persist the reviewed snapshot with any edits or toggles. This is the input to staleness detection on apply. POST /api/trade/feeds/:sessionId/apply— the apply path validates the reviewed snapshot against the current normalized artifact and writes to the tenant catalog. On success the session transitions toAPPLIED.
Idempotency on upload
Idempotency for POST /api/trade/feeds/upload is opt-in via the Idempotency-Key header only. Without the header, two uploads of the same file create two independent sessions.
Behavior when the header is supplied:
- First request: creates a session and returns
202 Acceptedwith the newsessionId(andjobId,status,fileName). - Retry with the same key and the same normalized request payload (
requestHashmatch): returns202 Acceptedwith the original session andreplayed: trueon the response body. - Retry with the same key but a different payload: returns
409 ACTION_NOT_ALLOWEDwith the canonical error envelope.
Header constraints:
Idempotency-Keyis a free-form string, 1–200 characters, scoped by tenant + endpoint + key.- Server-side expiry applies. Customers that need replay protection across long windows should generate unique keys per logical upload operation (for example,
feed-upload:<tenant>:<source-hash>:<YYYY-MM-DD>).
Direct apply shortcut
For feeds where providedHs is already populated on each SKU (e.g. when the uploaded CSV contains HS codes), you can skip the classify-preview and review steps entirely. After the session reaches READY (or PREVIEW_BLOCKED with applyable: true and zero issues):
GET /api/trade/feeds/:sessionId/skus— fetch the normalized SKUs.- Build the apply body by mapping each SKU to an
ApplyRowSchemarow:providedHsbecomeshsCode,productNamebecomesname. Onlysku,name,hsCode,originCountry, anddestinationCountryare valid row fields. POST /api/trade/feeds/:sessionId/applywith{ rows, originCountry, screeningAuthority }.
The quickstart examples and Postman collection use this direct-apply path. The full classify-preview path below is for AI-assisted HS classification when providedHs is absent.
Classify-preview → review → apply handoff
POST /api/trade/feeds/:sessionId/classify-previewenforces a per-tenant in-flight preview job limit. Exceeding the limit returns429 RATE_LIMITEDwith retry metadata indetails.PUT /api/trade/classify/preview/:jobId/reviewpersists the reviewer’s chosen row edits and toggles. The reviewed snapshot is used as the apply input and as the staleness baseline.POST /api/trade/feeds/:sessionId/applyis the commit step. If the normalized artifact has changed between review and apply, the server returns409 ACTION_NOT_ALLOWEDand the caller must re-review from a fresh preview. Staleness detection is the reason the review step exists as a separate call — it pins the snapshot the customer approved.
Issue handling and operator handoff
GET /api/trade/feeds/:sessionId/issues returns both blocking and non-blocking issues. Every issue carries isBlocking, issueType, message, and an opaque issueKey used for acknowledgement.
- Blocking issues cannot be acknowledged. They must be resolved by fixing the underlying file and re-uploading. A session with any unresolved blocking issue stays in
PREVIEW_BLOCKED. - Non-blocking issues can be acknowledged individually. Acknowledgement is an operator action, routed through the Clerk-authenticated RGL8R dashboard — it is not part of the public agent surface.
Agent-side behavior when a session enters PREVIEW_BLOCKED:
- Inspect the session response fields:
applyable,issueCount, andblockingIssueCount. - If
applyable: truewithissueCount: 0andblockingIssueCount: 0, proceed directly to the apply step — no operator contact needed and no issues to acknowledge. - If the session has actual issues (
issueCount > 0), callGET /api/trade/feeds/:sessionId/issuesand surface blocking and non-blocking issues to the customer. If the customer wants to proceed without resolving non-blocking issues, instruct them to contact their RGL8R operator. The operator will review and acknowledge the non-blocking issues via the RGL8R dashboard in the customer org context. This is a deliberate launch-time gate — customers cannot self-acknowledge from the agent surface. - Once the operator acknowledges the last non-blocking issue, the session transitions to
READYandclassify-preview/applybecome available.
The internal operator procedure for acknowledging non-blocking issues is documented in the RGL8R operator runbook and is not part of the public contract.
Error envelopes
All errors follow the canonical public envelope (docs/api/public-api-contract-v1.md#canonical-error-taxonomy-v1):
{
"code": "INVALID_REQUEST",
"message": "Invalid request payload",
"details": { "field": "file" }
}Endpoint-specific error summary:
| Endpoint | Code | HTTP | When |
|---|---|---|---|
| upload | INVALID_REQUEST | 400 | Missing file, unsupported defaults, bad shape |
| upload | ACTION_NOT_ALLOWED | 409 | Same Idempotency-Key reused with a different payload |
| upload | FILE_TOO_LARGE | 413 | File exceeds the upload size cap |
| upload | INVALID_FILE_FORMAT | 415 | Unsupported MIME/extension |
| upload | RATE_LIMITED | 429 | Upload bucket or tenant queue cap |
| get session / skus / issues | NOT_FOUND | 404 | Session does not belong to the authenticated tenant |
| classify-preview | ACTION_NOT_ALLOWED | 409 | Session not in READY, or apply already in-flight |
| classify-preview | RATE_LIMITED | 429 | Per-tenant in-flight preview-job limit reached |
| review | ACTION_NOT_ALLOWED | 409 | Preview job not in a reviewable state |
| apply | ACTION_NOT_ALLOWED | 409 | Session not in READY, or reviewed snapshot stale |
| any | INVALID_TOKEN | 401 | JWT expired or revoked — re-exchange via token endpoint |
| any | INTERNAL_ERROR | 500 | Unexpected failure; retry with backoff |
Retry guidance
- Token exchange: up to 3 retries on
429/5xxwith exponential backoff (1s, 2s, 4s) plus jitter. - Upload: do not blind-retry on
5xx. If a retry is required, use anIdempotency-Keyheader to avoid creating duplicate sessions. - Poll session status: 2–3s in the first minute, then 5–10s with jitter. Terminal session states are
READY,PREVIEW_BLOCKED,FAILED, andAPPLIED. - Classify-preview: retry on
429usingdetails.retryAfterSecondsfrom the envelope; do not retry409. - Apply: on a stale-snapshot
409, re-run the preview + review steps before retrying apply.
Cross-references
- OpenAPI artifact:
docs/api/openapi/rgl8r-public-api-v1.2.0.yaml - Public API contract pack (auth, error taxonomy, compatibility policy):
docs/api/public-api-contract-v1.md - Companion contracts for SHIP workflows:
docs/api/ship-finding-contract-v1.md,docs/api/ship-claim-submission-contract-v1.md - Quickstart and agent kit:
docs/developers/quickstart.md,docs/api/agent-integration-kit-v1.md - Sample fixture:
docs/api/examples/fixtures/trade-feed-sample.csv - Example scripts:
docs/api/examples/rgl8r_trade_feed_quickstart.ts,docs/api/examples/rgl8r_trade_feed_quickstart.py