Skip to Content
APITrade Feed Contract

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.

MethodRoutePurpose
POST/api/trade/feeds/uploadUpload a CSV/Excel file into a durable feed session
GET/api/trade/feeds/:sessionIdGet session status + normalization progress
GET/api/trade/feeds/:sessionId/skusPaginated SKU list for the normalized artifact
GET/api/trade/feeds/:sessionId/issuesPaginated blocking + non-blocking issues
POST/api/trade/feeds/:sessionId/classify-previewEnqueue a preview classification job
PUT/api/trade/classify/preview/:jobId/reviewPersist the reviewed snapshot
POST/api/trade/feeds/:sessionId/applyApply the reviewed feed to the catalog

Routes intentionally excluded from the public agent surface:

  • POST /api/trade/feeds/:sessionId/issues/:issueKey/acknowledge is Clerk-only and is the operator-triggered path to acknowledge non-blocking issues in a PREVIEW_BLOCKED session. 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 │ └─▶ FAILED

State 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 for classify-preview and apply.
  • 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)

  1. Exchange the integration key for a Bearer JWT.
  2. POST /api/trade/feeds/upload with a multipart file field plus two required form fields: originCountry (ISO 3166-1 alpha-2, e.g. CN) and defaultDestinationCountry (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 returns 202 Accepted with 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.
  3. Poll GET /api/trade/feeds/:sessionId until status reaches READY, PREVIEW_BLOCKED, or FAILED. Use the same polling backoff as the public-contract job loop: every 2–3s for the first minute, then every 5–10s with jitter.
  4. POST /api/trade/feeds/:sessionId/classify-preview — enqueues a preview classification job. Response includes a jobId (poll via GET /api/jobs/:id) and a previewJobId for the next step.
  5. When the preview job reaches COMPLETED, PUT /api/trade/classify/preview/:jobId/review to persist the reviewed snapshot with any edits or toggles. This is the input to staleness detection on apply.
  6. 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 to APPLIED.

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 Accepted with the new sessionId (and jobId, status, fileName).
  • Retry with the same key and the same normalized request payload (requestHash match): returns 202 Accepted with the original session and replayed: true on the response body.
  • Retry with the same key but a different payload: returns 409 ACTION_NOT_ALLOWED with the canonical error envelope.

Header constraints:

  • Idempotency-Key is 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):

  1. GET /api/trade/feeds/:sessionId/skus — fetch the normalized SKUs.
  2. Build the apply body by mapping each SKU to an ApplyRowSchema row: providedHs becomes hsCode, productName becomes name. Only sku, name, hsCode, originCountry, and destinationCountry are valid row fields.
  3. POST /api/trade/feeds/:sessionId/apply with { 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-preview enforces a per-tenant in-flight preview job limit. Exceeding the limit returns 429 RATE_LIMITED with retry metadata in details.
  • PUT /api/trade/classify/preview/:jobId/review persists 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/apply is the commit step. If the normalized artifact has changed between review and apply, the server returns 409 ACTION_NOT_ALLOWED and 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:

  1. Inspect the session response fields: applyable, issueCount, and blockingIssueCount.
  2. If applyable: true with issueCount: 0 and blockingIssueCount: 0, proceed directly to the apply step — no operator contact needed and no issues to acknowledge.
  3. If the session has actual issues (issueCount > 0), call GET /api/trade/feeds/:sessionId/issues and 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.
  4. Once the operator acknowledges the last non-blocking issue, the session transitions to READY and classify-preview / apply become 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:

EndpointCodeHTTPWhen
uploadINVALID_REQUEST400Missing file, unsupported defaults, bad shape
uploadACTION_NOT_ALLOWED409Same Idempotency-Key reused with a different payload
uploadFILE_TOO_LARGE413File exceeds the upload size cap
uploadINVALID_FILE_FORMAT415Unsupported MIME/extension
uploadRATE_LIMITED429Upload bucket or tenant queue cap
get session / skus / issuesNOT_FOUND404Session does not belong to the authenticated tenant
classify-previewACTION_NOT_ALLOWED409Session not in READY, or apply already in-flight
classify-previewRATE_LIMITED429Per-tenant in-flight preview-job limit reached
reviewACTION_NOT_ALLOWED409Preview job not in a reviewable state
applyACTION_NOT_ALLOWED409Session not in READY, or reviewed snapshot stale
anyINVALID_TOKEN401JWT expired or revoked — re-exchange via token endpoint
anyINTERNAL_ERROR500Unexpected failure; retry with backoff

Retry guidance

  • Token exchange: up to 3 retries on 429/5xx with exponential backoff (1s, 2s, 4s) plus jitter.
  • Upload: do not blind-retry on 5xx. If a retry is required, use an Idempotency-Key header 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, and APPLIED.
  • Classify-preview: retry on 429 using details.retryAfterSeconds from the envelope; do not retry 409.
  • 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