Overview
The Order Intake API is a server-to-server JSON API for partners to submit orders into ChoiceLink. A submitted order is staged as a normal order with status Pending in the shop bound to your API key. A person at that shop then reviews it and clicks Send to FCM inside ChoiceLink to finalize it.
This API never sends to First Choice Metals on its own. Submitting an order only stages it for human review. Sending always stays a deliberate action taken by a person in the shop.
- One API key is bound to exactly one shop. Orders you submit land in that shop’s queue.
- You reference products by ChoiceLink item codes and color codes (pull them from
/api/v1/catalog). - All endpoints live under
/api/v1. Base URL:https://v2.choice-link.net.
Quick start
Three steps: confirm your key, pull the catalog, submit an order.
curl -s https://v2.choice-link.net/api/v1/preflight \
-H "Authorization: Bearer ck_live_…"curl -s https://v2.choice-link.net/api/v1/catalog \
-H "Authorization: Bearer ck_live_…"curl -s -X POST https://v2.choice-link.net/api/v1/orders \
-H "Authorization: Bearer ck_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7b1c0e2a-…(unique per order)" \
-d '{
"externalRef": "PO-10432",
"lines": [
{ "itemCode": "RC9-10", "color": "BR", "quantity": 12 },
{ "itemCode": "AG9", "color": "BR", "quantity": 8, "feet": 12, "inches": 0 }
]
}'Authentication
Every request must carry your API key. Send it either as a Bearer token (preferred) or an x-api-key header:
Authorization: Bearer ck_live_ab12cd34ef56_…
# or
x-api-key: ck_live_ab12cd34ef56_…Keys look like ck_live_<id>_<secret>. ChoiceLink stores only a SHA-256 hash of the key — the full token is shown once when it is created and cannot be recovered, so store it securely. Keys can be scoped, revoked, and given an expiry. A missing, malformed, invalid, revoked, or expired key returns 401; a valid key that lacks the scope an endpoint needs returns 403.
Scopes
Each key carries one or more scopes. Endpoints require a specific scope:
| Scope | Grants |
|---|---|
catalog:read | GET /api/v1/catalog and GET /api/v1/preflight |
orders:write | POST /api/v1/orders |
orders:read | GET /api/v1/orders/:id |
Most partner keys are issued with all three scopes.
Setup & onboarding
Done once, by a ChoiceLink administrator, before a partner can submit:
- The shop belongs to a company that has its First Choice Metals API key configured.
- The shop has its FCM customer id set, and its warehouse has the FCM warehouse/department ids (warehouse ids are optional but recommended).
- At least one person belongs to that shop to review and click Send to FCM.
- An API key is minted for the shop and handed to the partner (shown once).
The partner then pulls /api/v1/catalog, maps their own SKUs to our item codes and color codes once, and submits our codes on every order. You can verify readiness at any time with /api/v1/preflight — its missing array tells you exactly what is not yet configured.
How product matching works
Each order line is our item code + color code + quantity + optional length. At send time, the First Choice Metals product id is derived from the item code and color code by this rule (the API runs it up front so any unmatched line is rejected immediately, not days later at send):
base = itemCode (e.g. "RC9-10")
if base contains "ST9-" -> replace the first "-" with the color code
elif base contains "POLY" -> use base as-is (the color is ignored)
elif base contains "-" -> replace the first "-" with the color code
else -> base + color code
# Special-trim items — codes containing "ST9-" or "ST6-" — additionally carry a
# required, user-supplied "description" to FCM (see Submit an order).| itemCode | color | Resulting FCM product id |
|---|---|---|
ST9- | BR | ST9BR |
POLY100 | CL | POLY100 (color ignored) |
RIDGE- | WH | RIDGEWH |
GUTTER | AL | GUTTERAL |
A color is required on every line. POLY codes ignore the color’s value, but one must still be present — without it the order would stage and then fail when a human tries to send it, so the API rejects it up front.
Conventions
- All requests and responses are JSON. Send
Content-Type: application/jsonon requests with a body. - The API is versioned in the path (
/api/v1). Breaking changes ship under a new version. - Timestamps are ISO-8601 UTC strings. Order ids are 24-character hex strings.
- Errors always return a JSON body with an
errorstring (see Errors).
Check readiness
/api/v1/preflightany valid keyConfirms your key works and reports whether the bound shop can stage orders and have them sent to FCM. Any valid key works.
{
"ok": true,
"keyName": "Acme ERP",
"scopes": ["orders:write", "orders:read", "catalog:read"],
"expiresAt": null,
"shop": { "id": "665f0a1b2c3d4e5f60718293", "name": "North Shop" },
"company": "First Choice Metals",
"canStageOrders": true, // shop is linked to a company
"canBeSentToFcm": true, // company API key + shop customer id are set
"missing": [], // blocking gaps to fix if canBeSentToFcm is false
"warnings": [] // non-blocking (e.g. optional warehouse ids)
}Pull the catalog
/api/v1/catalogscope: catalog:readThe authoritative list of item codes and color codes your shop may order. Pull this to build your crosswalk and re-sync when it changes.
{
"company": "First Choice Metals",
"shop": "North Shop",
"updatedAt": "2026-06-03T15:00:00.000Z",
"itemCodes": [
{ "code": "AG9", "description": "29ga AG Panel" },
{ "code": "RC9-10", "description": "RIDGE CAP (29ga) 10'6\"" },
{ "code": "ST9-LF", "description": "SPECIAL TRIM (29ga) Custom-Length" }
],
"colors": [
{ "code": "BR", "name": "Brown" },
{ "code": "WH", "name": "White" }
]
}Submit an order
/api/v1/ordersscope: orders:writeStages an order as Pending in your shop. Validation is strict and up front: unknown or cross-account references, special-trim lines missing a description, and lines that can't be matched to an FCM product are rejected with 422 and no order is created.
Request body
| Field | Required | Notes |
|---|---|---|
externalRef | no | Your PO / reference. Becomes the order number and the FCM customer PO. |
lines | see note | Flat array of line items — the standard form. |
groups | see note | Labeled groups for multi-building orders: [{ "reference": "Building A", "lines": [ … ] }]. customPackages is an alias. If lines is present it takes precedence. |
lines[].itemCode | yes | An item code from /catalog. |
lines[].color | yes | A color code from /catalog. Required on every line. |
lines[].quantity | yes | Positive integer. |
lines[].feet, lines[].inches | conditional | Length (decimals). Required for custom-length items; for fixed-length items the catalog length is used and any value you send is normalized to it. Default 0. |
lines[].description | conditional | Required for special-trim items (codes containing ST9- or ST6-); ignored for everything else. Sent to FCM as "<description> - <COLOR>". |
Lengths: fixed-length items (e.g. RC9-10 = RIDGE CAP 10’6") take their length from the catalog — you may omit feet/inches, and any value you send is normalized to match. Custom-/linear-length items (e.g. …-LF, panels) use the length you send. Special trim: any item code containing ST9- or ST6- must include a non-empty description; omitting it returns 422.
{
"externalRef": "PO-10432",
"lines": [
{ "itemCode": "RC9-10", "color": "BR", "quantity": 12 },
{ "itemCode": "AG9", "color": "BR", "quantity": 8, "feet": 12, "inches": 0 }
]
}{
"externalRef": "PO-10433",
"lines": [
{ "itemCode": "ST9-LF", "color": "BR", "quantity": 4, "feet": 10, "inches": 6,
"description": "L-trim 3.5x1.5 bent 90" }
]
}{
"externalRef": "PO-10432",
"groups": [
{ "reference": "Building A", "lines": [ { "itemCode": "RC9-10", "color": "BR", "quantity": 12 } ] },
{ "reference": "Building B", "lines": [ { "itemCode": "GUTTER", "color": "WH", "quantity": 8 } ] }
]
}{
"id": "665f0a1b2c3d4e5f60718293",
"orderNumber": "PO-10432",
"status": "Pending",
"sentToFCM": false,
"externalRef": "PO-10432",
"lines": [
{ "itemCode": "RC9-10", "color": "BR", "resolvedProductId": "RC9BR10" },
{ "itemCode": "ST9-LF", "color": "BR", "resolvedProductId": "ST9BRLF",
"description": "L-trim 3.5x1.5 bent 90" }
]
}resolvedProductId is the FCM product id each line will send as. Send an Idempotency-Key header to make retries safe — see Idempotency. When externalRef is omitted, the order number is an API-generated value (API-<timestamp>).
Get an order's status
/api/v1/orders/:idscope: orders:readReturns the current status of one order, scoped to your shop. Poll it to follow an order through Pending → sent → Confirmed/Shipped.
{
"id": "665f0a1b2c3d4e5f60718293",
"orderNumber": "PO-10432",
"fcmOrderNumber": null, // FCM's number once sent
"status": "Pending",
"sentToFCM": false,
"source": "api",
"externalRef": "PO-10432",
"createdAt": "2026-06-03T15:01:00.000Z",
"sentAt": null,
"groups": [
{ "reference": "PO-10432",
"lines": [
{ "itemCode": "RC9-10", "color": "BR", "quantity": 12, "feet": 10, "inches": 6 },
{ "itemCode": "ST9-LF", "color": "BR", "quantity": 4, "feet": 10, "inches": 6,
"description": "L-trim 3.5x1.5 bent 90" }
] }
]
}An unknown id (or one in another shop, or a malformed id) returns 404.
Idempotency
Send a unique Idempotency-Key header with each POST /api/v1/orders request. It makes network retries safe — order numbers are not unique on their own, so this is how duplicates are prevented.
| Scenario | Result |
|---|---|
| Same key, same body (a retry) | Returns the original response with idempotentReplay: true. No second order. |
| Same key, different body | 409 — the key was already used with a different request. |
| New key (or no key) | Processed normally. |
Errors
Errors return the matching HTTP status and a JSON body with an error string.
| Status | Meaning |
|---|---|
400 | Request body is not valid JSON. |
401 | Missing, malformed, invalid, revoked, or expired key. |
403 | Key is valid but lacks the required scope, or is not bound to a shop. |
409 | Shop not linked to a company, or an Idempotency-Key reused with a different body. |
422 | Validation failed — see below. No order is created. |
500 | Unexpected server error while staging the order. |
A 422 from POST /orders is self-correcting: it names exactly which lines failed and why. Reference problems come back as details; lines that resolve but can’t be matched to an FCM product come back as lines.
{
"error": "One or more lines could not be resolved against your catalog.",
"details": [
{ "line": "lines[1]", "itemCode": "WIDGET", "problem": "unknown item code" },
{ "line": "lines[2]", "itemCode": "ST9-LF",
"problem": "special-trim items require a `description`" }
]
}{
"error": "One or more lines cannot be matched to an FCM product.",
"lines": [
{ "label": "Ridge Cap", "itemCode": "RIDGE-", "ok": false,
"problem": "item code \"RIDGE-\" requires a color code" }
]
}Per-line problem strings include: missing itemCode, quantity must be a positive integer, unknown item code, item code does not belong to this account, missing color code, unknown color code for this account, and special-trim items require a `description`.
Order lifecycle
- Submitted — you POST the order; it is created as
Pendingin the shop’s queue. - Reviewed — a person at the shop opens the order in ChoiceLink and checks it.
- Sent — that person clicks Send to FCM; ChoiceLink posts it to First Choice Metals and records an FCM order number (
sentToFCMbecomes true). - Fulfilled — the order moves through
Confirmed→Shipped→Received.
Poll GET /api/v1/orders/:id to follow these transitions.
Key management (ChoiceLink admins)
These endpoints are for ChoiceLink administrators and use a logged-in ADMIN or COMPANY session (not an API key). Partners do not call them.
/api/admin/api-keysany valid keyMint a key for a shop. Returns the plaintext token once.
{ "shopName": "North Shop",
"name": "Acme ERP",
"scopes": ["orders:write", "orders:read", "catalog:read"],
"expiresAt": "2027-01-01" }{ "id": "…", "token": "ck_live_…", "prefix": "ck_live_ab12cd34ef56",
"scopes": ["orders:write","orders:read","catalog:read"], "shop": "North Shop",
"warning": "Store this token now — it is shown only once and cannot be recovered." }/api/admin/api-keysany valid keyList keys (never returns hashes or tokens). Optional ?shopId= filter.
/api/admin/api-keys/:idany valid keyRevoke a key (soft delete via revokedAt; history is preserved).
Keys can also be minted from the server with node scripts/mint-api-key.js "<shop name>" --name "Acme ERP".
Versioning & support
This is v1 (1.0.0). The version lives in the path; backward-incompatible changes will ship under a new path version. The machine-readable contract is the OpenAPI 3.1 spec — import it into Postman, Insomnia, or your codegen of choice. For access or help, contact your ChoiceLink administrator.