Supply Chain
Generating and Distributing Serialized Product Codes with the io.tt Supply Chain API
The Supply Chain module is the upstream counterpart to the consumer-facing experiences. It lets a brand mint unique, scannable identifiers (as plain URLs or GS1 Digital Links), group them under a programme, and attach them to a specific product (touchpoint). Once generated, each code resolves to a branded landing page that can host any io.tt experience — Prize Draw, Loyalty, Golden Ticket, Gifting, or Scan & Register.
Unlike the experience endpoints, the Supply Chain API is designed to be called server-side from your brand tooling (ERP, warehouse, label-generation pipeline) using a brand-scoped API key. The endpoints are exposed through a dedicated OpenAPI spec (tsoa-supply.json) and are consumed via the Speakeasy-generated Supply Chain SDK, not through the consumer @io-tt/sdk package covered by the Getting Started guide.
The API exposes six operations:
generateIds— enqueue a batch of codesgetJobStatus— poll the processing state of a batchgetItemsWithUrl— paginated list of generated codes (URL or SVG)lookupProgramme— find a programme by ID or namecreateProgramme— create a new programme for the brandgetProgrammes— paginated list of programmes
This guide walks through the full flow, the key concepts, and the rules the API enforces.
Core Concepts
Before calling the API, it helps to understand the four main entities:
| Entity | What it is |
|---|---|
| Brand | The entity owning the codes. Derived from your API key — you never pass it explicitly. |
| Programme | A campaign or activation. Every batch of codes belongs to exactly one programme. Referenced by programmeId or programmeName. If a name is given that doesn't exist, the API will create the programme automatically. |
| Touchpoint | The physical product (SKU) the codes are applied to. Optional — if omitted, items are generated with no product linkage. Referenced by touchpointId or gtin. |
| Batch / Supply Job | A batch is the set of items generated by one generateIds call. The supply job is the async handle you poll to check when the batch is ready. |
How codes resolve
Each generated item has a short serial (the slug) printed under the QR/NFC. When scanned, the URL resolves one of two ways:
- Plain redirect —
https://{domain}/{slug}— the default when no GS1 configuration is present. - GS1 Digital Link —
https://{domain}/01/{gtin}/21/{slug}— used automatically when the touchpoint has a valid GTIN and GS1 domain configured. This is the GS1-compliant format used by standards-aware scanners and trade systems.
The domain defaults to your brand's configured domain, but can be overridden per batch.
Job Lifecycle
Code generation is asynchronous. A generateIds call returns immediately with a jobId; the batch is processed off a queue and moves through the following states:
PENDING → PROCESSING → ID_GENERATED → IMAGE_PENDING → COMPLETED
↘
ERROR / FAILEDOnly jobs in the COMPLETED state will return items from getItemsWithUrl. Any earlier state returns 404 with a message indicating the job is still pending, and terminal ERROR / FAILED states return 404 with the failure reason.
1. Authenticate
Every Supply Chain request uses a brand API key (distinct from the Experience API keys used for the consumer SDK). Keys are issued by your io.tt account manager and carry one or both of these scopes:
| Scope | Required for |
|---|---|
read:supply | getJobStatus, getItemsWithUrl, lookupProgramme, getProgrammes |
write:supply | generateIds, createProgramme |
Send the key as a bearer token:
Authorization: Bearer <your-supply-api-key>2. Create or Look Up a Programme
Every batch must belong to a programme. You have three options:
- Look up by name —
GET /v1/supply-chain/programme?programmeName=Spring 2026 Launch - Look up by ID —
GET /v1/supply-chain/programme?programmeId=65f1a… - Skip this step entirely and pass
programmeNamestraight togenerateIds— the API will create the programme on the fly if it doesn't exist.
To create a programme explicitly (with description, tags, date window, or markets):
POST /v1/supply-chain/programme
Content-Type: application/json
{
"name": "Spring 2026 Launch",
"description": "EMEA spring activation",
"tags": ["spring", "emea"],
"startDate": "2026-03-01",
"endDate": "2026-06-30",
"marketIds": ["<marketId>"]
}Response includes a dashboardUrl — a direct link to the programme in the io.tt dashboard.
To browse existing programmes, use the paginated list:
GET /v1/supply-chain/programmes?page=1&pageSize=203. Generate a Batch (generateIds)
The core operation. Submit the quantity you need, a programme reference, an optional touchpoint/GTIN, and a webhookUrl to be notified when processing completes.
POST /v1/supply-chain/generate-ids
Content-Type: application/json
{
"quantity": 500,
"programmeName": "Spring 2026 Launch",
"gtin": "05012345678900",
"url": "https://brand.example/landing",
"webhookUrl": "https://brand.example/hooks/supply",
"extra": {
"poNumber": "PO-2026-0411",
"printRun": "line-A"
}
}Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
quantity | number | ✅ | Number of codes to generate. |
programmeId | string | — | Mongo ObjectId of an existing programme. Provide this or programmeName. |
programmeName | string | — | Programme name. If it doesn't exist, it will be created. |
touchpointId | string | — | Mongo ObjectId of the target touchpoint. |
gtin | string | — | GTIN of the target touchpoint. Validated as a GS1 Digital Link. |
url | string | ✅ | Redirect URL each code should resolve to. |
domain | string | — | Override the domain codes resolve against. Defaults to your brand domain. |
webhookUrl | string | ✅ | URL to POST the completion payload to. |
extra | object | — | Arbitrary client-supplied metadata, echoed back by getJobStatus. Useful for correlating jobs with your own IDs (PO numbers, print runs, etc). |
Response (202 Accepted):
{
"jobId": "65f1a2b3c4d5e6f7a8b9c0d1",
"status": "pending"
}Validation rules
- Either
programmeId(a valid Mongo ObjectId) orprogrammeNamemust be present. gtin, if provided, must be a valid GS1 Digital Link identifier.touchpointIdorgtinmust match a touchpoint owned by your brand (or be omitted entirely).
4. Wait for Completion
There are two ways to know when a batch is ready. Pick whichever fits your pipeline.
Option A — Webhook
When you provide a webhookUrl, the platform will POST a completion payload to it once the job reaches a terminal state:
{
"jobId": "65f1a2b3c4d5e6f7a8b9c0d1",
"status": "completed",
"totalGenerated": 500,
"timestamp": "2026-04-22T10:15:00Z"
}On failure, status is "failed" and an error field carries the reason. Webhook delivery is best-effort and non-blocking — see the API-side webhooks documentation for details.
Option B — Polling
Call getJobStatus on an interval:
GET /v1/supply-chain/{jobId}/statusResponse:
{
"id": "65f1a2b3c4d5e6f7a8b9c0d1",
"batchId": "65f1a2b3c4d5e6f7a8b9c0d2",
"status": "COMPLETED",
"createdAt": "2026-04-22T10:00:00Z",
"updatedAt": "2026-04-22T10:15:00Z",
"metadata": { "quantity": 500, "batchName": "SDK Batch - 2026-04-22T10:00:00Z" },
"extra": { "poNumber": "PO-2026-0411", "printRun": "line-A" }
}Typical batches complete in seconds to minutes depending on size.
5. Fetch the Generated Items (getItemsWithUrl)
Once the job is COMPLETED, page through the items. Choose the response format with responseType:
responseType=url— the raw redirect URL; use this if you render your own QR codes.responseType=svg(default) — a link to a pre-rendered SVG QR code hosted by io.tt.
GET /v1/supply-chain/{jobId}/items?responseType=svg&page=1&pageSize=200Response:
{
"items": [
{
"id": "65f1…",
"slug": "a3k7p",
"brandId": "65ed…",
"batchId": "65f1a2…",
"url": "https://qr.io.tt/65ed…/65f1a2…/000000001-65f1….svg"
}
],
"total": 500,
"page": 1,
"pageSize": 200,
"totalPages": 3,
"dashboardUrl": "https://dashboard.io.tt/65ed…/links/65f1a2…"
}Pagination is offset-based. pageSize is capped at 200 — larger values are silently clamped.
6. End-to-End Example
// Pseudo-code using a Speakeasy-generated Supply Chain SDK
const supply = new SupplyChainSDK({ apiKey: process.env.IOTT_SUPPLY_KEY });
// 1. Generate a batch
const { jobId } = await supply.generateIds({
quantity: 500,
programmeName: 'Spring 2026 Launch',
gtin: '05012345678900',
url: 'https://brand.example/landing',
webhookUrl: 'https://brand.example/hooks/supply',
});
// 2. Poll until complete (or skip this and wait for the webhook)
let job;
do {
await new Promise((r) => setTimeout(r, 2000));
job = await supply.getJobStatus(jobId);
} while (job.status !== 'COMPLETED');
// 3. Page through the items
let page = 1;
while (true) {
const res = await supply.getItemsWithUrl(jobId, 'svg', page, 200);
for (const item of res.items) {
await sendToPrintQueue(item.url);
}
if (page >= res.totalPages) break;
page++;
}7. Error Reference
| HTTP | Error | Why |
|---|---|---|
400 | ProgrammeMissingNameAndIdError | Neither programmeId nor programmeName was provided. |
400 | GtinIsNotValid | The supplied gtin failed GS1 Digital Link validation. |
404 | ProgrammeNotFoundError | programmeId did not match and no fallback programmeName was given. |
404 | TouchpointNotFoundError | touchpointId / gtin did not match any touchpoint owned by the brand. |
404 | Job not found / still pending / errored | Returned from getItemsWithUrl and getJobStatus when the job is not COMPLETED. |
All error responses share the shape:
{ "message": "…", "code": "…" }8. Summary Table
| Method | Scope | Purpose |
|---|---|---|
generateIds | write:supply | Enqueue a batch of codes and return a job handle. |
getJobStatus | read:supply | Inspect the lifecycle state of a supply job. |
getItemsWithUrl | read:supply | Paginated list of items with URL or SVG links. |
lookupProgramme | read:supply | Resolve a programme by ID or name. |
createProgramme | write:supply | Create a new programme for the brand. |
getProgrammes | read:supply | Paginated list of non-archived programmes. |
Notes
- Supply Chain is a brand-side workflow. Keep the API key server-side — never ship it to a consumer-facing client.
- Every resolved code ultimately hands off to an io.tt experience. See the Experience Guides for how to build the consumer-facing side once codes are in circulation.
- GTIN validation and GS1 Digital Link generation follow the GS1 Digital Link 1.x specification.
- Programme and touchpoint lookups are always scoped to the authenticated brand — cross-tenant access is not possible.