Commissioning Reliability and Retry Handling
Part of: Integrating Tagnet with Your Systems — handling partial failures, safe retries, and reprints
A commissioning integration deals with the real world: networks drop, printers jam, and batches arrive with bad data. This article explains the exact behavior of the commission endpoint so you can build an integration that recovers cleanly from every failure mode without creating duplicates or losing print data.
For exact request and response schemas, open Scalar with Open API reference on the Overview tab of the Integrations page in your Tagnet instance.
Understanding the response
Every commission response (success or failure) has the same top-level shape.
| Field | What it tells you |
|---|---|
status | Aggregate result: Completed, PartiallyCompleted, or Failed |
totalItems | Number of rows submitted |
succeededItems | Rows that commissioned successfully |
failedItems | Rows that did not commission |
attemptNumber | Which attempt this batch represents (starts at 1, increases on retry) |
replayed | true when the response is a cached replay of a prior successful attempt |
items[] | One entry per input row, in submission order |
Each row in items[] carries:
| Field | What it tells you |
|---|---|
rowRef | The value you sent in the request row (your correlation id) |
status | Commissioned or Failed |
errorMessage | Present only on a failed row; describes why it failed |
instanceId | The durable identity assigned to this physical item |
tagId | The EPC encoded on the RFID tag; the canonical EPC field, and the label data source |
locationCode, locationName | Where the item was placed, if you supplied placement |
HTTP status codes
| Code | Meaning |
|---|---|
200 OK | At least one row succeeded (Completed or PartiallyCompleted) |
422 Unprocessable Entity | Every row failed (Failed) |
400 Bad Request | Envelope problem: missing batchKey, empty rows, or batch exceeds the size limit |
401 Unauthorized | No credential provided |
403 Forbidden | Credential present but lacks the required permission |
A 200 does not mean every row succeeded. If your batch has multiple rows, a 200 with failedItems > 0 means some rows commissioned and some did not. Always inspect the per-row status when processing multi-row batches.
Detecting and handling failures
Step 1: check the aggregate
Check the top-level status and failedItems count to decide whether any rows need attention:
if response.status == "Completed":
# all rows succeeded, proceed to printing
elif response.status == "PartiallyCompleted":
# some rows succeeded, some failed: split and handle both
elif response.status == "Failed":
# every row failed, nothing was created
Step 2: scan per-row results
For any batch with failedItems > 0, iterate items[] and separate by status:
succeeded = [item for item in response.items if item.status == "Commissioned"]
failed = [item for item in response.items if item.status == "Failed"]
for item in failed:
log(item.rowRef, item.errorMessage)
# queue for correction and resubmit
Use the rowRef you sent in the request to correlate each response row back to your own record. If you did not supply a rowRef, rows are still in the same order as your request.
Step 3: print only commissioned rows
The response is the label data source. Filter to status == "Commissioned" before printing. A failed row has no instanceId or tagId, so a printer that tries to use it will error.
for item in succeeded:
print_label(epc=item.tagId, instance_id=item.instanceId, ...)
Idempotency and the batch key
batchKey is required on every commission request. It is your idempotency key: the protection against duplicate inventory when a network problem leaves you unsure whether a request went through.
Choose the right key
Use a stable business key you already have, such as the work order id, the print job id, or the purchase order line. Something that means "this batch."
Do not use a generated value per attempt (a fresh UUID or timestamp). A new key on each retry defeats the duplicate protection entirely: Tagnet sees a new key and treats it as a new batch.
| Good key | Why |
|---|---|
order-123 | Stable: the same order id is the same batch |
job-PO9000-line-3 | Stable: encodes the business identity of this batch |
dc1-cycle-2026-07-02 | Stable: uniquely identifies a specific cycle-count run |
| Poor key | Why |
|---|---|
uuid() (fresh per call) | A new key on retry mints a new batch |
timestamp() | Same problem |
"batch" (generic) | Collides across different batches in the same scope |
How idempotency works
Idempotency in Tagnet is success-scoped and attempt-numbered. The rules differ depending on whether the batch commissioned anything.
A key that commissioned at least one row is locked. Resending the same key replays the stored result: no new inventory is created, no EPCs are minted, and you get back the original items[]. The response includes replayed: true to confirm this. This is the safe retry path after a network blip.
A key whose every attempt fully failed is retryable. Resending the same key runs a new attempt (watch the attemptNumber increment). A corrected payload will succeed. Nothing was created in the failed attempt, so there is nothing to conflict with.
A partial success locks the key. The rows that commissioned created live inventory. Resubmitting the same key replays those committed rows and does not re-run the failed ones. Recover the failed rows under a new, derived key (see below).
Attempt history
Each attempt is its own record. GET /commission/by-key/{batchKey} returns the full list of attempts for a key, oldest first. Use this to inspect the history of a retried batch, or browse it in the UI with the Commissioning Log page.
The full retry decision tree
submit batch with batchKey = "order-123"
|
+-- HTTP 400 --> fix the envelope (missing key, too many rows) and resubmit
|
+-- HTTP 5xx --> retry with SAME batchKey (network or server error; nothing committed)
|
+-- HTTP 422 (all failed) --> fix the row data and retry with SAME batchKey
| (the key is still retryable; attemptNumber increments)
|
+-- HTTP 200, status=Completed --> print all rows
|
+-- HTTP 200, status=PartiallyCompleted
|
+-- print the Commissioned rows
+-- fix the Failed rows (see errorMessage on each)
+-- resubmit ONLY the fixed rows under a DERIVED key: "order-123-fix"
When you submit the derived key, treat it the same as any other batch. If it partially fails again, derive further: order-123-fix-2, and so on.
Per-row gap-fill under the original key (retrying only the failed rows in place) is planned for a future release.
Recovering failed rows: a worked example
Scenario: Order PO-9000 has two line items. Line 2 has an invalid custom field.
First submit (batchKey: "po-9000"):
{
"batchKey": "po-9000",
"scope": "warehouse-east",
"rows": [
{ "rowRef": "line-1", "itemId": "SKU-WIDGET", "suppliedTagId": "EPC-0001" },
{ "rowRef": "line-2", "itemId": "SKU-WIDGET", "suppliedTagId": "EPC-0002",
"customFields": { "treatmentClass": "INVALID-VALUE" } }
]
}
Response, HTTP 200 with status: PartiallyCompleted:
{
"batchKey": "po-9000",
"attemptNumber": 1,
"status": "PartiallyCompleted",
"totalItems": 2,
"succeededItems": 1,
"failedItems": 1,
"items": [
{ "rowRef": "line-1", "status": "Commissioned", "tagId": "EPC-0001", "instanceId": "..." },
{ "rowRef": "line-2", "status": "Failed", "errorMessage": "Invalid value for field 'treatmentClass'" }
]
}
Action: Print the label for line-1. Fix line-2's custom field value. Resubmit under a derived key.
Correction submit (batchKey: "po-9000-fix"):
{
"batchKey": "po-9000-fix",
"scope": "warehouse-east",
"rows": [
{ "rowRef": "line-2", "itemId": "SKU-WIDGET", "suppliedTagId": "EPC-0002",
"customFields": { "treatmentClass": "CLASS-A" } }
]
}
Response, HTTP 200 with status: Completed:
{
"batchKey": "po-9000-fix",
"attemptNumber": 1,
"status": "Completed",
"totalItems": 1,
"succeededItems": 1,
"failedItems": 0,
"items": [
{ "rowRef": "line-2", "status": "Commissioned", "tagId": "EPC-0002", "instanceId": "..." }
]
}
Print the label for line-2. Both items are now live in inventory.
Validating a batch (dry run)
POST /commission/validate runs the same per-row validation that a real submit does, but mints nothing, bumps no sequence generator, and persists nothing. It is useful for checking payloads while you build an integration, and as an optional pre-check before a real submit when you want to catch data problems (like an invalid custom field) up front instead of recovering failed rows afterward.
The validate response includes:
valid(per-row boolean) anderrorMessagefor any invalid rowinstanceIdPreviewandtagIdPreview: the real value for a supplied identifier, or a character-pattern (X...) for an auto-generated one (the actual value is minted at submit time)- Resolved placement details so you can confirm the right location and transaction type
A validate-then-submit flow looks like this:
1. POST /commission/validate -- dry run with the full batch
- if any row is invalid, fix it and re-validate
- repeat until all rows pass
2. POST /commission -- real submit with the same rows and a stable batchKey
- on success, print Commissioned rows
- on partial failure, recover Failed rows under a derived key
Validating adds a second call per batch, so whether to validate every submit or only during development is a tradeoff between that overhead and catching problems before they reach a real submit. An integration sending clean, stable data can skip validation and rely on the per-row failure handling above.
Validation requires the same EditInventory permission as the real submit. A read-only key receives a 403 on the validate endpoint as well.
Declaring where a row's EPC comes from
Most item types settle the EPC source once, on the type: either the caller always supplies the Tag ID, or Tagnet always mints it. Item types set to Sequence Generator (supplied Tag ID wins) instead let each row decide, so that one type can be printed by two processes that source their EPCs differently (see Types).
That flexibility has a failure mode worth closing. If your process is the one that supplies EPCs and a row loses its tagId (a mapping bug, a null upstream field), Tagnet mints one instead of rejecting the row, and a label goes out carrying a Tagnet EPC where your own encoding belonged. Nothing downstream flags it, because the row commissioned successfully.
Send tagIdSource on each row to prevent it:
| Value | Meaning |
|---|---|
supplied | This row provides its own tagId. Fail the row if it does not. |
generate | Tagnet mints this row's EPC. Fail the row if a tagId was sent. |
The field is an assertion, not a switch: it never changes which source is used, it only fails the row when the actual source is not the one you declared. Set it as a constant in each process's payload template, and neither process can silently take the other's path. A row that violates its own assertion fails inline, before any sequence value is consumed, and the rest of the batch still commissions.
tagIdSource is optional and is checked against every strategy, so it also catches a payload sent to the wrong item type: declaring supplied on a type that always mints (or generate on a type that always requires a supplied value) fails the row with a message naming the type's strategy. An unrecognized value fails only its own row, never the batch.
Reprints from history
To reprint labels for an already-commissioned batch, retrieve the stored result and use the saved EPCs. Do not resubmit the batch.
GET /commission/by-key/{batchKey} -- full attempt list, oldest first
GET /commission/{id} -- single attempt by database id
GET /commission?search={partialKey} -- lightweight summary list
Each commissioned row in the response still carries tagId (the EPC) and instanceId. Use those for reprints. The stored data does not expire until the batch history retention window passes (configurable in Policies), and the same rows are browsable in the UI on the Commissioning Log page.
If a batch key is readable in more than one access scope, add ?scope={code} to disambiguate: GET /commission/by-key/po-9000?scope=warehouse-east.
Batch sizing and chunking
The commission endpoint has a maximum row count per call, set by the Commission / Limits policy (default 500, hard ceiling 2000). If you receive a 400 with a message about exceeding the maximum, split the batch into smaller calls, each with its own stable batch key.
A good chunking key encodes the chunk: order-123-chunk-1, order-123-chunk-2, and so on.
Summary
| Situation | Action |
|---|---|
| Network error or 5xx | Retry with the same batchKey |
| All rows failed (422) | Fix row data, retry with the same batchKey |
| Some rows failed (200 PartiallyCompleted) | Print successes; fix failures; resubmit under a derived key |
| Unsure whether a batch went through | Retry with the same key; a locked key replays safely |
| Need to reprint | GET /commission/by-key/{batchKey} and use stored EPCs |
| Want to catch errors before printing | POST /commission/validate first, then submit |
| Your process supplies EPCs on a type that can also mint | Send tagIdSource: "supplied" so a dropped tagId fails instead of minting |