External IDs & Mapping
How to use your own identifiers with the Retraced Public API, in one place. There are two mechanisms, and every resource uses one or both of them:
| Mechanism | What it is | Where |
|---|---|---|
externalId |
A field you write on a record you own (a product, a BOM header, an order, …). From then on you can address that record by your own ID instead of the Retraced ID. | Products, BOM, materials, style properties, orders, order lines |
| Mapping a code to Retraced reference data | A lookup you maintain in Retraced that translates your code into a Retraced ID for data you do not own: a partner company, a style type, a raw material, a facility process. | Companies (Internal Company Code), Constant Mappings |
Both follow the same contract:
- Send either value. Every ID field that supports it accepts the Retraced ID or your own value. You never tell the API which one you sent.
- Exact match. Values are compared character for character, case-sensitively.
"123"does not match"1234"or"abc"vs"ABC". - Only your own. An
externalIdresolves only inside your own company's records, and a code only through the mappings your company maintains. Another company'sexternalIdor code is never matched. - Unknown values pass through. A value that matches nothing is used as-is and then fails exactly like an unknown Retraced ID would on that endpoint -
404,400, or an empty list. - Responses carry Retraced IDs.
id,parentId,styleId,orderId,supplierCompanyId,styleTypeId, … are always Retraced IDs. Records you own also return theirexternalId; mapped codes are returned only by/api/v2/constant-mappingsand, for companies, asinternalCompanyCode.
BASE_URL="https://publicapi.retraced.com/api/v2"
API_KEY="your-api-key"
Calling an endpoint by externalId vs Retraced ID
Once a record carries an externalId, every place the API takes that record's ID accepts the externalId too: path parameters (GET /styles/{id}, PATCH /orders/{id}), query filters (?styleId=, ?orderId=), and references inside a body (parentId, lines[].styleId, bomHeaderId, lines[].id).
# The same product, three ways to reach it
curl "$BASE_URL/styles/style_01KZ3Y5M4X8QW2P7NBJ6R9T0AE" -H "companyapikey: $API_KEY" # Retraced ID
curl "$BASE_URL/styles/DRS-2026-001" -H "companyapikey: $API_KEY" # your externalId
curl "$BASE_URL/styles?level=variant&styleId=DRS-2026-001" -H "companyapikey: $API_KEY" # as a filter
Rules that apply everywhere:
- If a value is both a Retraced ID and one of your
externalIds, the Retraced ID wins. Retraced IDs carry a type prefix (style_,bomhdr_,order_,ordline_, …) and a productexternalIdmay not start withstyle_,variant_orsku_, so a clash is unlikely; in practice this rule matters for the code-based mappings below. - Your own catalogue first. For products, the lookup order is: your own product with that Retraced ID, then your own product with that
externalId, then any other company's product with that Retraced ID (shared network products). A Retraced product ID is therefore readable across companies; anexternalIdnever is. - Uniqueness is per company, per record type. The same string can be a product's
externalId, a BOM header'sexternalIdand an order'sexternalIdat once; it cannot be two products, or two orders. - Duplicates return
409with codeexternal_id_already_exists. The message names the record that already holds the value. Nothing is written. - Omit to keep,
nullto clear. On an update, leavingexternalIdout of the body keeps the stored value, even on endpoints that otherwise replace the whole record (PUT /styles/{id}). Only an explicit"externalId": nullclears it. - Format. 1-255 characters from
A-Z a-z 0-9 _ . ~ : @ -, at least one letter or digit, leading and trailing spaces trimmed. Anything else is rejected with400. ProductexternalIds additionally may not start withstyle_,variant_orsku_. - Written only by the API. The Retraced dashboard shows an
externalIdread-only; bulk imports and dashboard edits never set or change it.
Products
Every product level - style, variant and SKU - goes through the single /styles resource and carries one externalId, unique within your company across all three levels. BOM headers, BOM lines, BOM placements, material headers, material lines and style properties each have their own externalId, each unique within its own type.
| Endpoint | Accepts your externalId in |
|---|---|
GET / PUT /styles/{id} |
{id} |
POST /styles |
parentId |
POST /styles/archive, POST /styles/unarchive |
ids[] |
GET /styles |
styleId, variantId, propertyExternalIds |
POST / PUT /styles |
brands, departments, seasons, collections, customProperties (a style property's externalId) |
GET /supply-chains/{id} |
{id} |
GET / PUT /bom-headers/{id}, POST /bom-headers/delete |
{id}, bomHeaderIds[]; styleId and link.styleId in bodies and ?styleId= |
GET / POST / PUT /bom-lines, POST /bom-lines/delete |
{id}, bomHeaderId, placementId, bomLineIds[], entity.styleId, component.styleId |
GET /bom-placements/{id}, GET /bom-placements?ids= |
{id}, ids |
GET / PUT /material-headers/{id}, POST /material-headers/delete |
{id}, materialHeaderIds[], link.styleId, ?styleId= |
GET / POST / PUT /material-lines, POST /material-lines/delete |
{id}, materialHeaderId, parentId, materialLineIds[], styleId |
GET /style-properties?externalId= |
exact filter on the property's externalId |
Create a style with your own ID, then a variant under it by that ID:
curl -X POST "$BASE_URL/styles" -H "companyapikey: $API_KEY" -H "Content-Type: application/json" -d '{
"name": "Summer Dress 2026",
"externalId": "DRS-2026-001",
"styleTypeId": "DRESSES",
"isComponent": false,
"codes": [{ "codeId": "ERP_ID", "codeValue": "DRS-2026-001" }]
}'
curl -X POST "$BASE_URL/styles" -H "companyapikey: $API_KEY" -H "Content-Type: application/json" -d '{
"name": "Summer Dress 2026 - Blue",
"parentId": "DRS-2026-001",
"externalId": "DRS-2026-001-BLU",
"color": "Blue",
"codes": [{ "codeId": "ERP_ID", "codeValue": "DRS-2026-001-BLU" }]
}'
Response (201) - id and styleId (the parent) are Retraced IDs, externalId is yours:
{
"metadata": { "success": true },
"data": {
"id": "variant_01KZ3Y7D2QH4V9S8E1MWX5B6CN",
"styleId": "style_01KZ3Y5M4X8QW2P7NBJ6R9T0AE",
"externalId": "DRS-2026-001-BLU",
"name": "Summer Dress 2026 - Blue",
"color": "Blue",
"styleTypeId": "DRESSES",
"codes": [{ "codeId": "ERP_ID", "codeValue": "DRS-2026-001-BLU" }],
"isArchived": false,
"createdAt": "2026-10-22T06:33:41.000Z",
"updatedAt": "2026-10-22T06:33:41.000Z"
}
}
A second product with "externalId": "DRS-2026-001" is refused:
{
"metadata": { "success": false },
"statusCode": 409,
"code": "external_id_already_exists",
"message": "externalId 'DRS-2026-001' is already used by product 'style_01KZ3Y5M4X8QW2P7NBJ6R9T0AE'"
}
Products have two more things to know:
externalIdis not a code.codes(ERP_ID,EAN, …) describe a product and are searchable with?codeId=&codeValue=;externalIdaddresses it. You will often set both to the same value, as above.- BOM and material inheritance never copies an
externalId. When a variant or SKU inherits a BOM or material structure and you edit it, the copy that is created for that level starts withexternalId: null, except for the one row your request writes. See the BOM Guide and Materials Guide. A BOM placement'sexternalIdcan only be set on create.
Product fields that take mapped codes rather than externalIds - styleTypeId, rawMaterialId, and the company fields forCompanyId, suppliers[], mainSupplier, vendors[] - are covered under Companies and Constant Mappings below.
Orders and order lines
An order and each of its lines carry their own optional externalId. Order externalIds are unique across your company's orders; line externalIds are unique across all of your company's order lines, not just within one order. The two are separate namespaces, so an order and one of its lines may share a value.
| Endpoint | Accepts your externalId in |
|---|---|
GET / PATCH / DELETE /orders/{id} |
{id} (the order) |
PATCH /orders/{id} |
lines[].id (a line of that order) |
POST / PATCH /orders |
lines[].styleId (a product's externalId) |
GET /order-lines?orderId= |
orderId |
GET /order-lines/{lineId} |
{lineId} |
GET /orders has no externalId filter. To find an order by your ID, call GET /orders/{externalId} directly; to find it by order number, use GET /orders?orderNumber= (exact match).
externalId and orderNumber are independent
orderNumber is the business document number that appears in the dashboard and in tracing; externalId is your addressing key. The API never fills one from the other:
- An order created without
externalIdhasexternalId: null, whatever itsorderNumberis. - Changing
orderNumberleavesexternalIduntouched, and vice versa. orderNumberis unique per company and order type; a duplicate is400order_number_already_existing.externalIdduplicates are409external_id_already_exists.GET /orders/{orderNumber}returns404- only the Retraced ID or theexternalIdaddresses an order.
The one value the API derives is the line's orderLineNumber: when you omit it, it becomes {orderNumber}-{orderType}-L{lineNo}. That derivation uses orderNumber, never externalId, and existing lines keep their number when orderNumber changes later.
Create an order by your own keys end to end - your order ID, your supplier code, your facility-process code, and a product by its externalId:
curl -X POST "$BASE_URL/orders" -H "companyapikey: $API_KEY" -H "Content-Type: application/json" -d '{
"orderNumber": "PO-2026-0042",
"externalId": "ERP-PO-88213",
"buyerCompanyId": "<your-company-id>",
"supplierCompanyId": "SUP-0917",
"supplierFacilityProcess": "ERP-FP-CMT",
"lines": [
{ "styleId": "DRS-2026-001-BLU", "quantity": 300, "unit": "PIECES", "externalId": "ERP-PO-88213-L1" }
]
}'
Response (201, trimmed) - every reference comes back resolved to a Retraced ID:
{
"metadata": { "success": true },
"data": {
"id": "order_01KZ3YB0F2N7XQ4VH9R8J5WT6D",
"externalId": "ERP-PO-88213",
"orderNumber": "PO-2026-0042",
"orderType": "PURCHASE_ORDER",
"buyerCompanyId": "<your-company-id>",
"supplierCompanyId": "FU5651SSQ5Y",
"supplierFacilityProcess": "CUT_MAKE_TRIM",
"lineCount": 1,
"lines": [
{
"id": "ordline_01KZ3YB0F7E2KM8D3SA6PY1Q9C",
"externalId": "ERP-PO-88213-L1",
"orderLineNumber": "PO-2026-0042-PURCHASE_ORDER-L1",
"lineNo": 1,
"styleId": "variant_01KZ3Y7D2QH4V9S8E1MWX5B6CN",
"quantity": 300,
"unit": "PIECES"
}
]
}
}
Then work with it by those keys:
# Read the order and one of its lines
curl "$BASE_URL/orders/ERP-PO-88213" -H "companyapikey: $API_KEY"
curl "$BASE_URL/order-lines/ERP-PO-88213-L1" -H "companyapikey: $API_KEY"
curl "$BASE_URL/order-lines?orderId=ERP-PO-88213" -H "companyapikey: $API_KEY"
# Update a line, addressed by its externalId (omit "externalId" on the order to keep it)
curl -X PATCH "$BASE_URL/orders/ERP-PO-88213" -H "companyapikey: $API_KEY" -H "Content-Type: application/json" -d '{
"lines": [ { "id": "ERP-PO-88213-L1", "quantity": 320, "unit": "PIECES" } ]
}'
PATCH /orders/{id} with lines is a full sync of the order's lines; a line you drop is deleted and its externalId is free again within the same request. DELETE /orders/{id} is a hard delete of the order and its lines - see Archiving vs. deleting. Details of both are in the Orders & Order Lines Guide.
Companies: Internal Company Code
Companies are different from the records above: a partner company is not yours, so it has no externalId you can write. Instead, the Internal Company Code you give a company in your network plays that role. It is a value your company stores about a partner, so each of your partners can carry your ERP's supplier or customer number, and another Retraced account that works with the same partner sees only its own codes.
Where to set it: in the Retraced dashboard (v2), open Network and edit the Internal company code column of the company. There is no public API endpoint that writes it. The code is yours to define: it is not derived from anything, and nothing in Retraced is derived from it. Only companies currently in your network resolve; a code you gave to a company you have since unlinked matches nothing.
Wherever the API takes a company ID, it also accepts that code:
| Endpoint | Accepts your Internal Company Code in |
|---|---|
GET /companies/{id}, GET /companies/{id}/esg |
{id} |
GET /companies |
companyIds, excludeCompanyIds |
POST / PATCH /orders |
buyerCompanyId, supplierCompanyId, receiverCompanyId, vendorCompanyId, factory |
GET /orders |
supplier, buyer |
POST / PUT /styles |
forCompanyId, suppliers[], mainSupplier, vendors[] |
GET /styles |
companyId, suppliers, mainSupplier, vendors |
POST / PUT /material-headers, POST /material-lines |
forCompanyId |
GET /certificates |
companyId, and companyId inside filter |
Rules specific to company codes:
- A code you gave to more than one company is treated as unknown. Codes are not enforced unique in the dashboard, so if two partners carry the same code the API cannot pick one; the value passes through as-is and fails like an unknown company ID (
404company_not_foundonGET /companies/{id},404invalid_company_idon orders). - Choose codes that cannot look like a Retraced company ID. Company IDs are short uppercase strings such as
FU5651SSQ5Y; the code lookup runs first for companies, so a code identical to some company's Retraced ID would shadow that ID. - The list response returns the code as
internalCompanyCode(nullwhen you have not set one), so you can keep your own table in sync fromGET /companies. - The
filterkey is a search, not a lookup.GET /companies?filter={"internalCompanyCode":"SUP"}is a case-insensitive substring match that also returnsSUP-0917andXSUP; the ID fields above match exactly.
# Read a partner by the code you gave it
curl "$BASE_URL/companies/SUP-0917" -H "companyapikey: $API_KEY"
Response (200, trimmed) - the profile of the company that carries code SUP-0917:
{
"metadata": { "success": true },
"data": {
"forCompanyId": "FU5651SSQ5Y",
"byCompanyId": "<your-company-id>",
"profileType": "ABOUT",
"data": { "official_name": { "retracedId": "official_name", "value": "Wynyard Textiles Ltd" } }
}
}
# The same partner's certificates, and the code echoed back in the list
curl "$BASE_URL/certificates?companyId=SUP-0917" -H "companyapikey: $API_KEY"
curl "$BASE_URL/companies?companyIds=SUP-0917" -H "companyapikey: $API_KEY"
{
"metadata": { "success": true, "pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 } },
"data": [
{ "id": "FU5651SSQ5Y", "name": "Wynyard Textiles", "internalCompanyCode": "SUP-0917", "country": "PT" }
]
}
Constant Mappings: materials, product types, facility processes
Three kinds of reference data are maintained by Retraced and shared by every account, so you cannot put an externalId on them. Instead you map your code to the Retraced ID once, and then send the code anywhere the ID is expected:
resourceType |
Retraced ID it maps to | Where you send the code |
|---|---|---|
STYLE_TYPE |
A style type from GET /style-types (TSHIRTS, DRESSES, …) |
styleTypeId on POST / PUT /styles and GET /styles?styleTypeId= |
RAW_MATERIAL |
A raw material (COTTON_BALE, …) |
rawMaterialId on POST / PUT /material-lines and GET /material-lines?rawMaterialId= |
FACILITY_PROCESS |
A facility process from GET /facility-processes (CUT_MAKE_TRIM, …) |
buyerFacilityProcess, supplierFacilityProcess, receiverFacilityProcess on POST / PATCH /orders |
Where to manage them:
- Dashboard (v2): Company settings → API → Constants Mapping, with one tab per kind: Materials, Product Types, Facility Processes. If you do not see it, ask your Customer Success Manager to enable it.
- API:
/api/v2/constant-mappings-GETlists,POSTcreates,PATCHchanges,DELETEremoves, 1-100 mappings per call, any mix of resource types. Every write is all-or-nothing: if one item fails, nothing in that call is saved.
Rules:
- One code maps to one Retraced ID per resource type; many codes may map to the same ID. Creating a code that already exists for that type is
409constant_mapping_already_exists; naming the same code twice in one call is400duplicate_constant_mapping_codes. - A Retraced ID always wins. If a value is a valid style type, material or facility process ID, it is used as such even if you also mapped it as a code. Style type and facility process IDs are plain words (
TSHIRTS,ASSEMBLY), so choose codes that differ from them. For facility processes the API enforces this: mapping a code that is itself a facility process ID is409constant_mapping_code_is_retraced_id. retracedIdis always the Retraced ID. It is the target of the mapping, so it never accepts a code. An unknown target is404style_type_not_found,material_not_foundorfacility_process_not_found.- Raw-material style types mirror their material. A style type that is a raw material (
COTTON_BALEis both a product type and a material) shares its ID with the material, so aSTYLE_TYPEmapping to it also creates theRAW_MATERIALmapping with the same code and vice versa.POSTreturns both rows; aPATCHorDELETEof one carries over to the other, and a call that names both halves is400constant_mapping_mirror_in_batch. - Responses never echo your codes. A product created with
"styleTypeId": "ERP-PT-TEE"returns"styleTypeId": "TSHIRTS". Store the Retraced ID next to your code.
Create three mappings in one call:
curl -X POST "$BASE_URL/constant-mappings" -H "companyapikey: $API_KEY" -H "Content-Type: application/json" -d '[
{ "resourceType": "STYLE_TYPE", "retracedId": "TSHIRTS", "externalId": "ERP-PT-TEE", "externalName": "Tee" },
{ "resourceType": "RAW_MATERIAL", "retracedId": "COTTON_BALE", "externalId": "ERP-MAT-COT" },
{ "resourceType": "FACILITY_PROCESS", "retracedId": "CUT_MAKE_TRIM", "externalId": "ERP-FP-CMT" }
]'
Response (201, trimmed) - four rows, because the raw-material mapping is mirrored onto its style type:
{
"metadata": { "success": true },
"data": [
{ "id": "0T6TPNXPEYBXSW4FE7", "resourceType": "STYLE_TYPE", "retracedId": "TSHIRTS", "name": "T-shirts", "externalId": "ERP-PT-TEE", "externalName": "Tee" },
{ "id": "YJCXF083DKCVV09YFR", "resourceType": "RAW_MATERIAL", "retracedId": "COTTON_BALE", "name": "Cotton bale", "externalId": "ERP-MAT-COT", "externalName": null },
{ "id": "EAQSFK29UCKBXMSX8N", "resourceType": "STYLE_TYPE", "retracedId": "COTTON_BALE", "name": "Cotton bale", "externalId": "ERP-MAT-COT", "externalName": null },
{ "id": "USZK8KJEGO4CX5FNRL", "resourceType": "FACILITY_PROCESS", "retracedId": "CUT_MAKE_TRIM", "name": "Cut make trim (CMT)", "externalId": "ERP-FP-CMT", "externalName": null }
]
}
Use the codes, then manage them:
# A product by product-type code, a material line by material code, an order by facility-process code
curl -X POST "$BASE_URL/styles" -H "companyapikey: $API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "Basic Tee", "externalId": "TEE-2026-001", "styleTypeId": "ERP-PT-TEE", "isComponent": false, "codes": [{ "codeId": "ERP_ID", "codeValue": "TEE-2026-001" }] }'
# -> "styleTypeId": "TSHIRTS"
curl -X POST "$BASE_URL/material-lines" -H "companyapikey: $API_KEY" -H "Content-Type: application/json" \
-d '{ "styleId": "TEE-2026-001", "materialHeaderId": "<material-header-id-or-externalId>", "parentId": null, "rawMaterialId": "ERP-MAT-COT", "percentage": 100 }'
# -> "rawMaterialId": "COTTON_BALE"
# List, filtered by type and code
curl "$BASE_URL/constant-mappings?resourceType=STYLE_TYPE&externalId=ERP-PT-TEE" -H "companyapikey: $API_KEY"
# Rename a code (the mapping id comes from the list or the create response)
curl -X PATCH "$BASE_URL/constant-mappings" -H "companyapikey: $API_KEY" -H "Content-Type: application/json" \
-d '[ { "id": "0T6TPNXPEYBXSW4FE7", "externalId": "ERP-PT-TEE-V2", "externalName": "Tee (v2)" } ]'
# Delete by id, or by code together with its type (never by code alone - a mirrored code exists in two types)
curl -X DELETE "$BASE_URL/constant-mappings?ids=0T6TPNXPEYBXSW4FE7" -H "companyapikey: $API_KEY"
curl -X DELETE "$BASE_URL/constant-mappings?externalIds=ERP-FP-CMT&resourceType=FACILITY_PROCESS" -H "companyapikey: $API_KEY"
# -> 204
An unknown or unmapped code is passed through unchanged and fails as the ID would on that endpoint: 400 style_type_not_found on /styles, 404 material_not_found on /material-lines, 400 invalid_facility_process_ids on /orders. On the list endpoints (GET /styles?styleTypeId=, GET /material-lines?rawMaterialId=) an unknown code simply matches nothing and returns an empty page.
Error reference
| Status | code |
When |
|---|---|---|
409 |
external_id_already_exists |
The externalId is already held by another record of the same type in your company (product, BOM header/line/placement, material header/line, style property, order, order line). The message names the owner. |
404 |
not_found, order_not_found, bom_header_not_found, material_header_not_found, … |
A path ID or externalId matched nothing in your company (also returned for another company's externalId). |
404 |
company_not_found / invalid_company_id |
A company code matched no partner, or matched more than one. |
400 |
style_type_not_found |
styleTypeId is neither a style type ID nor one of your product-type codes. |
404 |
material_not_found |
rawMaterialId on POST / PUT /material-lines is neither a raw material ID nor one of your material codes. |
400 |
invalid_facility_process_ids |
A *FacilityProcess value is neither a facility process ID nor one of your codes. |
404 |
style_type_not_found, material_not_found, facility_process_not_found |
retracedId on POST /constant-mappings is not a Retraced ID. |
404 |
constant_mapping_not_found |
A mapping id or code on PATCH / DELETE /constant-mappings does not exist. |
409 |
constant_mapping_already_exists |
The code is already mapped for that resourceType. |
409 |
constant_mapping_code_is_retraced_id |
A FACILITY_PROCESS code equals a facility process ID. |
400 |
duplicate_constant_mapping_codes |
The same code appears twice for one resourceType in a single call. |
400 |
constant_mapping_mirror_in_batch |
A call names both a mapping and its raw-material mirror. |
Related reading
- Getting Started & Integration FAQ - authentication, envelope, pagination, reference data
- Products Guide, BOM Guide, Materials Guide - the product-side resources and their
externalIds in context - Orders & Order Lines Guide - the full order model, line sync, archiving vs deleting
- API Reference - every field's
.describe()says whether it accepts anexternalIdor a code