Latest change: reference v1.18 (6 November 2026): tracking_url now has the form https://omccouriers.com/track/?label=<label>. Links in the old form still work. Store and pass on tracking_url as given rather than building it yourself.
Every change since v1.2 is listed, newest first, in Change history at the end of this page.
The Consignment API lets your system push consignments into our label allocation engine and get label numbers back, synchronously, in the same call. It is the inverse of a sales channel connection: instead of our system reaching out to yours on a schedule, your system reaches in, whenever you have an order ready to ship, using an API key you generate in Compass.
Everything after the HTTP boundary — validation, label selection, serial allocation — is the same pipeline that handles a CSV import or a Shopify order, so a consignment created through this API behaves identically to one created any other way once it exists: it appears in Compass, it can be tracked, and (subject to the note on cancellation below) it can be cancelled.
| Concept | What you need to know |
|---|---|
| Base URL | See Base URLs below — live and sandbox. |
| Format | JSON request and response bodies, except the label endpoint, which returns
application/pdf. |
| Auth | Bearer API key, per customer, generated in Compass. See Authentication. |
| Idempotency | Supply your own identifier per consignment and a retried
request replays the original result rather than creating a duplicate. An identifier stays tied to
its first answer: resending a corrected item under it changes nothing. See POST
/consignments. |
| Synchronous | A create call returns label numbers directly — there is no job id to poll. This is why batches are capped at 50 (see Rate limits). |
Two deployments, each with its own base URL and its own keys — a key issued for one is
rejected by the other with wrong_environment rather than a confusing
invalid_key (see Authentication).
| Deployment | Base URL |
|---|---|
| Live | https://api.omccouriers.com/api/customer/v1 |
| Sandbox | https://sandbox-api.omccouriers.com/api/customer/v1 |
Every request carries your API key as a bearer token:
Authorization: Bearer omc_live_7f3aK2p9xQ1z...
A key looks like omc_ + live or test + _ + 43
further characters. The live/test segment must match the environment you are
calling — a live key against the sandbox, or a test key against production, is rejected
immediately with an explicit error rather than a confusing "invalid key", so a finished test harness
accidentally left pointed at the wrong place fails loudly on its first request.
Getting a key. You generate keys yourself in Compass, under Integrations → API access: Set up API access creates an API profile (the warehouse your parcels ship from and the default parcel size) and its first key together. A key is shown exactly once, when it is generated; copy it into wherever your system keeps its secrets before you leave that panel. After that Compass shows only its prefix, and neither you nor we can see the rest again. A lost key can't be recovered: revoke it and generate another. Keys are managed in Compass only; there is no key management endpoint in this API.
An API profile can hold two active keys at once, so you can rotate without downtime: generate the
new key, deploy it, then revoke the old one. A revoked key is refused on the very next request, and is
never switched back on. Sandbox Compass generates omc_test_ keys and live Compass
omc_live_ keys; there is no choice to make. If a key can't be generated yet because your
account's label set-up isn't finished, Compass says so; contact customer service to finish it.
Call GET /ping first. It is the fastest way to confirm your key works, see which
environment you are talking to, and see which import profiles your key can use.
live/test tag is part of the key
itself, checked when you authenticate; it never appears in a response. Every environment
field you get back — on GET /ping and on POST /consignments —
answers a different question: which deployment you are talking to, live or
sandbox. The two can never disagree (a key that doesn't match the deployment is rejected
before either endpoint's own response is built), so in practice this distinction rarely matters —
but the field always reports the deployment, not your key.| Limit | Default |
|---|---|
Consignments per POST /consignments call | 50 |
| Requests per key per minute | 60 — every authenticated call counts, not only creates |
| Consignments per key per hour | 2,000 — only POST /consignments counts against this one |
GET /consignments page size | 100, fixed |
The two counters meter different things and are worth telling apart. The per-minute limit
exists to stop a runaway client and is checked on every call — a reconciliation loop
hammering GET /consignments trips it exactly like a create call would, and
GET /consignments/{ref}/label in particular is not a cheap read: its first call makes a
live request to the carrier. The per-hour limit meters actual work created and is only ever
bumped by POST /consignments.
Exceeding either returns 429 with a Retry-After header, in seconds.
Under a genuinely simultaneous burst you may occasionally be refused slightly before the nominal limit
is reached — that is a deliberate trade (the counter is checked with no locking, so it can only
ever under-admit, never over-admit) and not a bug to work around.
The batch cap of 50 is what makes a synchronous response practical at all: each item in a batch runs the full pipeline, including a live carrier call for some destinations. If you have more than 50 consignments ready at once, send them as multiple calls rather than waiting for a larger cap.
/pingAuth and connectivity check. Call this first. Returns your customer name, which environment you are
talking to (live or sandbox — the deployment, not your key's own
live/test tag; see the note below), and the import profiles the key may use.
curl https://api.omccouriers.com/api/customer/v1/ping \ -H "Authorization: Bearer omc_live_..."
200 OK
{
"customer": "Acme Logistics Ltd",
"environment": "live",
"profiles": [
{ "profile_id": "3f2a1c9e-1111-4a2b-9c3d-abcdef123456", "name": "Default API profile" }
]
}
/consignmentsCreate 1–50 consignments in one call. See Worked examples for full request/response bodies. The short version:
200 whenever the request itself was well-formed — even when every
item failed to import, because a DRAFT item is a successful import at the HTTP level (the
consignment exists, waiting to be completed) and a machine-readable failure would tell your retry
logic to resend, which would then be treated as a duplicate. Check each item's own
status and the top-level summary, not just the HTTP status code.identifier per item to make retries safe: re-sending the same
identifier for the same customer replays the original result rather than creating a
second consignment, even if the original request is still being processed when the retry arrives.
Omitting it means no protection — a genuine network retry with no identifier
will create a duplicate consignment.identifier you have used before replays the stored result and does not read the new
payload at all, so a corrected resend under the same identifier changes nothing. To
correct a REJECTED item, send it under a new identifier. To correct a
DRAFT, finish it in Compass, or delete the draft there and resend. There are two
exceptions, where a resend under the same identifier is processed afresh, as if new
(new consignment_ref, no duplicate flag): the earlier attempt
failed (unexpected_failure, nothing was created), or its draft was
removed (the DRAFT consignment it created has since been deleted in
Compass).warnings array, so a customer migrating a Scurri payload builder can
send it here unmodified on day one and clean it up at leisure.environment field is the same one GET /ping returns
— the deployment, not your key's own tag (see the note under Authentication)./consignmentsList consignments this API created for you, for reconciliation. Filters: created_from,
created_to (both YYYY-MM-DD), status, order_number,
identifier. Cursor-paginated, 100 per page, newest first — pass the response's
next_cursor back as ?cursor= to fetch the next page; treat it as an opaque
token, never construct one yourself.
GET /consignments?status=DRAFT&created_from=2026-08-01
200 OK
{
"results": [
{ "consignment_ref": "CN0004417", "identifier": "wms-2026-98213",
"order_number": "8384832", "status": "OPEN", "created_at": "2026-08-28T09:14:02.331Z" }
],
"next_cursor": null
}
/consignments/{ref}One consignment's current status: every parcel, its label number and tracking URL, whether it has
been individually cancelled (is_voided), and the most recent tracking scan for it, if any.
Only ever resolves a consignment this API created for you — a ref from another customer, or from
another route into Enigma (the Compass portal, a CSV import), is reported as a plain 404.
200 OK
{
"consignment_ref": "CN0004417", "identifier": "wms-2026-98213", "order_number": "8384832",
"status": "OPEN", "created_at": "2026-08-28T09:14:02.331Z",
"parcels": [
{ "parcel_index": 1, "is_voided": false, "label_number": "BY0001561392",
"tracking_url": "https://omccouriers.com/track/?label=BY0001561392",
"last_scan": { "event_type": "DELIVERY", "description": "Delivered",
"event_time": "2026-08-29T14:02:11.000Z",
"ext_carrier": null, "ext_tracking_number": "" } },
{ "parcel_index": 2, "is_voided": true, "label_number": "BY0001561393",
"tracking_url": "https://omccouriers.com/track/?label=BY0001561393", "last_scan": null }
]
}
tracking_url is the parcel's public tracking page, showing its full tracking history: the
link to pass on to your own customer. last_scan is only the latest event your customer can
see; some scans are internal and never appear in either.
tracking_url doesn't open a page. It has the same
form as on live, but there are no tracking pages for sandbox parcels, and the live tracking page doesn't
know sandbox labels. To see a sandbox parcel's full tracking history, use Track on its consignment
in sandbox Compass. Nothing scans a sandbox parcel, so last_scan changes only when you use
Simulate next scan on the consignment's page in sandbox Compass./consignments/{ref}/labelReturns the label PDF. ?format=thermal (default, one label per page) or
?format=a4 (two-up).
409).409 when
nothing on the consignment is printable any more: it's still DRAFT, every parcel has
been voided, or it has shipped. GET /consignments/{ref} tells you which parcels are voided, so you can
reconcile exactly why a PDF has fewer labels than you expected.422 and no PDF at
all — never a PDF silently missing one label. Nothing on the failed parcel(s) is charged. Each
failure's code says what to do: retry the same request for
carrier_error; for address_rejected or parcel_rejected, the same
request will be refused again, so correct the consignment first./consignments/{ref}/cancelCancels a consignment that has not yet shipped: voids every currently-live label on it and hands
each label back, so it no longer counts as used. Not DELETE — the record, its now-voided labels and the
reversal all stay for your own audit trail and ours. No request body. Only works on a consignment
currently OPEN; cancelling one that is still DRAFT, already
CANCELLED, or has shipped, returns 409.
200 means every parcel was cancelled and the consignment is now
CANCELLED:
200 OK
{
"consignment_ref": "CN0004417", "identifier": "wms-2026-98213", "order_number": "8384832",
"status": "CANCELLED", "created_at": "2026-08-28T09:14:02.331Z",
"parcels": [
{ "parcel_index": 1, "is_voided": true, "label_number": "BY0001561392",
"tracking_url": "https://omccouriers.com/track/?label=BY0001561392", "last_scan": null }
]
}
207, not just
for success. A parcel handed to a partner carrier is cancelled with that carrier too, and the
carrier can refuse or be unreachable. Each parcel is then dealt with on its own: one the carrier
cancelled is cancelled (and its label handed back), one it refused stays open. If some were
cancelled and some were not, the response is 207 Multi-Status and the consignment
stays OPEN. Treat 207 as “not finished”: the body is the usual
consignment detail plus a one-line detail and a results array with one entry per
parcel the request touched — "status": "CANCELLED", or
"status": "NOT_CANCELLED" with a code (see carrier
failure codes) and our own reason, the same per-item convention as
POST /consignments. To finish, send the same
POST /cancel again; it touches only the parcels still open. If it keeps failing for a
parcel, contact customer service rather than retrying indefinitely — that parcel may already have
been cancelled at the carrier.207 Multi-Status
{
"consignment_ref": "CN0004418", "identifier": "wms-2026-98214", "order_number": "8384833",
"status": "OPEN", "created_at": "2026-08-28T09:20:11.042Z",
"parcels": [
{ "parcel_index": 1, "is_voided": true, "label_number": "BY0001561393", "tracking_url": "...", "last_scan": null },
{ "parcel_index": 2, "is_voided": false, "label_number": "BY0001561394", "tracking_url": "...", "last_scan": null }
],
"detail": "1 of 2 parcels were cancelled. The other parcel couldn't be cancelled with the carrier just now and is still open — see each parcel's reason.",
"results": [
{ "parcel_index": 1, "label_number": "BY0001561393", "status": "CANCELLED" },
{ "parcel_index": 2, "label_number": "BY0001561394", "status": "NOT_CANCELLED",
"code": "carrier_error",
"reason": "We couldn't cancel this parcel with the carrier just now, so it is still open. Try cancelling it again shortly, and contact customer service if it keeps failing." }
]
}
If the carrier refuses every parcel, nothing is cancelled and the response is 422
with "error": "carrier_error" and the same body — status still
OPEN, every results entry NOT_CANCELLED. The carrier's own error
text is never returned; customer service can see it.
NOT_CANCELLED with "code": "cancelled_at_carrier_but_shipped" and the
reason “Cancelled with the carrier, but the consignment was shipped or collected at the same time.
Contact customer service.”, and no further parcels are attempted. Do not retry: the consignment
is locked now, so another POST /cancel returns 409. Contact customer service with the
consignment reference.Field names and envelope shape follow Scurri's Create Consignment API wherever a direct Enigma
equivalent exists, so an existing Scurri integration is mostly a base-URL and credential change. A
field Scurri sends that has no Enigma equivalent is always accepted, never rejected — it
is silently recorded as ignored in that item's warnings array, so "ignored" never quietly
means "lost" or "your integration is broken".
| Field | Treatment | Notes |
|---|---|---|
identifier | Supported | Your own reference. Becomes the idempotency key. Unique per your account, not globally. Optional — omitting it means no replay protection. |
order_number | Required | Your order reference. |
delivery_instructions | Supported | Free text. For a parcel handed to a partner carrier, up to 50 characters are printed on its label, below the recipient's phone number; a longer note is cut short there and ends with “...” (the full text is kept). |
warehouse_id | Used | Optional. Chooses the warehouse this consignment ships from (since 1.10). First match wins: (1) absent or blank — your API profile's warehouse; (2) the ID of one of your warehouses — that warehouse; (3) one of your warehouse values (Compass, Integrations → API access), matched ignoring capital letters and extra spaces — the warehouse it points at; (4) no match and you have no warehouse values — your profile's warehouse, with the warning "warehouse_id 'X' isn't set up, so your default warehouse was used."; (5) no match and you do have warehouse values — that item is REJECTED with unknown_warehouse; the rest of the request carries on. Another customer's warehouse ID never matches. |
service_id | Used for label selection | Optional. When you have added the value in Compass and customer service has linked it to a label, the consignment gets that label (since 1.12; see Values that choose the label). Otherwise your label rules choose as before. Never warned about as unused. |
shipping_method | Used for label selection | As service_id (since 1.12). Also recorded against the request. No warning. |
carrier | Accepted, ignored + warning | Not read by default: the label is chosen by your label rules, or by service_id/shipping_method values you have set up (above). Customer service can set carrier up as a value field too, after which it is used like service_id and no longer warned about. |
create_date, shipping_date | Accepted, ignored + warning | Enigma stamps its own timestamps. |
order_value, currency | Accepted, ignored + warning | No customs documentation or value-based product in this version. |
custom_field_1, custom_field_2 | Accepted, ignored + warning | Scurri rules-engine inputs. Not read unless customer service sets one up as a value field (below). |
export_customs, invoice, options, custom_attributes | Accepted, ignored + one warning each | Whole blocks — customs and carrier extras don't apply to an NI/ROI-only network. A key inside one (for example options.package_type) can be set up as a value field (below). |
Customer service can also set up other fields to be read for label selection, including keys inside
an object such as options.package_type or a top-level field such as
custom_field_1. While such a field is set up, it and the object it sits in (here
options) are no longer warned about as "not used by this API" or "not recognised"
(since 1.12).
| Field | Treatment | Notes |
|---|---|---|
weight | Supported, validated | Kilograms. Absent or blank → your import profile's default weight. If supplied it must be a finite number greater than 0 — a zero, negative, NaN or Infinity value is a REJECTED result, not silently substituted. A value that isn't a number at all is treated differently: it falls back to the profile default and is reported in warnings instead of being rejected. Some partner carriers have a minimum weight: a lighter parcel is declared to that carrier at its minimum, so the carrier's label shows the minimum, while the weight you sent is stored and returned unchanged. |
length, width, height | Supported, validated | Centimetres. Absent or blank → profile defaults, field by field. Same validation as weight above, applied per dimension. |
description, reference | Accepted, ignored + warning | No per-parcel reference field exists yet. |
items[] | Accepted, ignored + warning | Customs line-item detail (SKU, HS code, etc.) — NI/ROI generates no customs documentation. |
No packages array at all, or an empty one, produces a single parcel using every profile
default.
"weight": 0 to mean "use the profile default" is a common Scurri-migration mistake —
here it is a REJECTED result ("reason": "weight must be greater than 0."),
not a substitution. Omit the field, or send it as null, to get the profile default. The
same applies to length, width and height.| Field | Treatment | Notes |
|---|---|---|
name or first_name + last_name | Supported | Supplying both forms is rejected outright (400) — there is no principled way to pick one. At most 40 characters (both parts together); longer makes the consignment a DRAFT (label_field_too_long). |
company_name | Supported | At most 40 characters; longer makes the consignment a DRAFT (label_field_too_long). |
email_address | Supported | Optional. A value that isn't a valid email address is left off: the consignment is created and labelled as normal, with no email, and the item's warnings says so (“The email address 'X' isn't valid, so it was left off.”). It is not a DRAFT reason. Since 1.14. |
contact_number | Supported, effectively required | Optional in Scurri; here a missing phone number always sends the consignment to DRAFT instead of failing outright, as does one in scientific notation (4.47737E+11; since 1.9). See Migrating from Scurri — this is the single most likely surprise. |
tax_identifier, eori_number | Accepted, ignored + warning | Customs identifiers. |
| Field | Treatment | Notes |
|---|---|---|
address1 / address2 / address3 | Supported | address1 required. Each line may be at most 40 characters on its own, on every consignment. A longer line makes the consignment a DRAFT (label_field_too_long) with the value kept as sent; it is not rejected or shortened. |
city | Required | At most 40 characters; longer makes the consignment a DRAFT (label_field_too_long). |
state | Supported | Maps to county. |
postcode | Required for GB/IE | Checked against a local pattern. A failure sends the consignment to DRAFT, it does not reject the item. A GB/IE postcode is stored and returned in standard form (BT45 8QX, D02 X285) whatever its spacing or case; since 1.8. |
country | Required | ISO 3166-1 alpha-2, or a full country name. |
store_code | Accepted, ignored + warning | No pick-up/drop-off service in this version. |
These fields take text: identifier, order_number,
shipping_method, warehouse_id, delivery_instructions; on
recipient, name, first_name, last_name,
company_name, email_address and contact_number; and on
recipient.address, address1, address2, address3,
city, state, postcode and country. In any of them
(since 1.11):
8384832 is "8384832",
12.5 is "12.5". Send a phone number as text if it starts with
0 or +: as a number it has already lost them;true/false rejects that item
(REJECTED, invalid_payload), with a reason naming the field in this
reference's terms, such as "recipient.address.city must be text, not a list." Nothing is
created for it; the other items in the request are unaffected;null is the same as leaving the field out.Since 1.12. Your account's label selection rules, set up by customer service, decide which label each consignment gets. If your system already says which service it wants, per consignment, you can use that instead of relying on the rules' order:
service_id, shipping_method, or another
field customer service has set up (such as options.package_type). Values are matched ignoring
capital letters and extra spaces; punctuation counts, so Express|Parcel and
Express | Parcel are different values.Then, for each consignment:
DRAFT with that reason (for example
no_label_for_destination or insufficient_balance naming that label). It is never
quietly sent on a different label.DRAFT with no_matching_label_rule.Customer service can also tie a label rule to one of your warehouses, so consignments shipping from it
use a particular label. That follows the warehouse each consignment ships from, however
warehouse_id named it, and needs nothing in the request.
Since 1.13, customer service can also limit a label rule to a weight or size: parcels weighing more
than one figure and up to and including another, and/or whose longest side (the largest of
length, width and height, whichever way round) is up to a given
length. Weight is the parcel's chargeable weight when the label judges parcels by size as well as weight,
otherwise the weight sent. Every parcel in the consignment must fit, and a parcel without all
three dimensions (sent, or as defaults on your API profile) never fits a size limit. A consignment outside
a rule's limits moves on to the next rule, but never from a linked value's rules to your other rules. If
every rule that could apply has a limit and a parcel fits none of them, it is a DRAFT with
no_label_rule_for_parcel_size.
POST /api/customer/v1/consignments
Authorization: Bearer omc_live_...
Content-Type: application/json
{
"consignments": [
{
"identifier": "wms-2026-98213",
"order_number": "8384832",
"recipient": {
"name": "John Doe",
"contact_number": "07700 900123",
"address": {
"address1": "12 Ormeau Road", "city": "Belfast",
"postcode": "BT7 1JR", "country": "GB"
}
},
"packages": [ { "weight": 1.4, "length": 30, "width": 20, "height": 10 } ]
}
]
}
200 OK
{
"environment": "live",
"summary": { "submitted": 1, "open": 1, "draft": 0, "rejected": 0, "duplicate": 0, "labels": { "BY": 1 } },
"results": [
{
"index": 0, "identifier": "wms-2026-98213", "order_number": "8384832",
"status": "OPEN", "consignment_ref": "CN0004417",
"parcels": [
{ "parcel_index": 1, "label_number": "BY0001561392",
"tracking_url": "https://omccouriers.com/track/?label=BY0001561392" }
],
"warnings": []
}
]
}
One item, several packages — each becomes its own labelled parcel on the same consignment.
Unlike a CSV upload, the API never merges separate items sharing an order number into one consignment:
if you mean one consignment with several parcels, send one item with several entries in
packages, exactly as below.
{
"consignments": [
{
"identifier": "wms-2026-98214",
"order_number": "8384840",
"recipient": {
"name": "Priya Shah", "contact_number": "07700 900456",
"address": { "address1": "4 Falls Road", "city": "Belfast", "postcode": "BT12 6AA", "country": "GB" }
},
"packages": [
{ "weight": 3.2, "length": 40, "width": 30, "height": 25 },
{ "weight": 1.1 },
{ "weight": 0.6, "length": 15, "width": 15, "height": 10 }
]
}
]
}
200 OK
{
"environment": "live",
"summary": { "submitted": 1, "open": 1, "draft": 0, "rejected": 0, "duplicate": 0, "labels": { "BY": 1 } },
"results": [
{
"index": 0, "identifier": "wms-2026-98214", "order_number": "8384840",
"status": "OPEN", "consignment_ref": "CN0004420",
"parcels": [
{ "parcel_index": 1, "label_number": "BY0001561400", "tracking_url": "https://omccouriers.com/track/?label=BY0001561400" },
{ "parcel_index": 2, "label_number": "BY0001561401", "tracking_url": "https://omccouriers.com/track/?label=BY0001561401" },
{ "parcel_index": 3, "label_number": "BY0001561402", "tracking_url": "https://omccouriers.com/track/?label=BY0001561402" }
],
"warnings": []
}
]
}
A batch of several consignments in one call. One item here is missing a phone number, so it lands in
DRAFT rather than failing the whole call — the caller still gets 200
and must inspect each item's own status.
{
"consignments": [
{
"identifier": "wms-2026-99001", "order_number": "9001",
"recipient": { "name": "Sam Reid", "contact_number": "07700 900001",
"address": { "address1": "1 High St", "city": "Lisburn", "postcode": "BT28 1AB", "country": "GB" } },
"packages": [ { "weight": 1.0 } ]
},
{
"identifier": "wms-2026-99002", "order_number": "9002",
"recipient": { "name": "Alex Kim",
"address": { "address1": "2 High St", "city": "Lisburn", "postcode": "BT28 1AC", "country": "GB" } },
"packages": [ { "weight": 1.0 } ]
}
]
}
200 OK
{
"environment": "live",
"summary": { "submitted": 2, "open": 1, "draft": 1, "rejected": 0, "duplicate": 0, "labels": { "BY": 1 } },
"results": [
{ "index": 0, "identifier": "wms-2026-99001", "order_number": "9001", "status": "OPEN",
"consignment_ref": "CN0004421",
"parcels": [ { "parcel_index": 1, "label_number": "BY0001561410",
"tracking_url": "https://omccouriers.com/track/?label=BY0001561410" } ],
"warnings": [] },
{ "index": 1, "identifier": "wms-2026-99002", "order_number": "9002", "status": "DRAFT",
"consignment_ref": "CN0004422",
"reason": "This order is missing required details: phone number.", "code": "missing_fields",
"parcels": [], "warnings": [] }
]
}
The second consignment still exists — it needs a phone number added in Compass, or a
corrected resend of just that item, before it can be finalised and labelled. summary.labels
counts the labels this request drew, per label prefix ({"BY": 1} here). Your label selection
rules can fall back to a second label when the first runs out, so more than one prefix appearing is worth
noticing: it may be a different service at a different price. summary.draft
being non-zero is what your integration should alert on; 200 alone tells you nothing about
whether every item succeeded.
Every DRAFT or REJECTED result carries a reason (text for a
person, which may be reworded) and a code (stable, for your code to branch on). New codes may
be added; treat one you don't recognise like needs_review (DRAFT) or
unexpected_failure (REJECTED). A result replayed from a request stored before
codes were introduced may have a reason and no code.
code | status | Meaning |
|---|---|---|
missing_fields | DRAFT | Required details are missing; reason names them (for example "phone number"). |
unknown_country | DRAFT | The delivery country isn't one we recognise. |
invalid_postcode | DRAFT | The postcode isn't a valid UK postcode or Eircode. If swapping the letters O and I for the digits 0 and 1 would make a valid Eircode, the reason names it as a suggestion; the value isn't corrected for you. |
not_a_postcode | DRAFT | The UK or Irish postcode has no digit in it, so it can't be one (often a town or county sent as the postcode). Since 1.8. |
phone_scientific_notation | DRAFT | The contact_number is in scientific notation (for example 4.47737E+11), so some of its digits are lost; send the full number, or complete it in Compass. Since 1.9. |
no_rules_configured | DRAFT | No label rules are set up for your API profile yet. Contact customer service. |
no_matching_label_rule | DRAFT | Label rules are set up, but each is linked to a value or a warehouse, and this consignment matched none of them. Contact customer service to set one up. Since 1.12; see Values that choose the label. |
no_label_rule_for_parcel_size | DRAFT | Every label rule that could apply is limited to a weight or size, and a parcel in this consignment fits none of them: "None of your label rules covers a parcel of this weight or size." Check the weight and dimensions you sent; if they're right, contact customer service. Since 1.13; see Values that choose the label. |
label_not_available | DRAFT | The labels set up for your account aren't available at the moment. Contact customer service. |
no_label_for_collection_point | DRAFT | None of your labels can be collected from the warehouse this consignment ships from. Contact customer service. |
no_label_for_destination | DRAFT | None of your labels deliver to this address. |
parcel_over_limits | DRAFT | A parcel is too heavy or too large for every label available for it. |
insufficient_balance | DRAFT | The account does not hold enough of the chosen label. Once more is available, finish the draft in Compass. |
label_field_too_long | DRAFT | The name, company, an address line or the city is longer than the 40 characters labels allow; reason names the fields. The value is kept as sent. Shorten it in Compass before finishing the draft, and keep these fields to 40 characters in future requests. |
needs_review | DRAFT | Any other reason the consignment couldn't be finalised automatically. |
label_not_determined | DRAFT | Only on results stored before codes were introduced (replayed duplicates): no label rule matched, cause not recorded. |
invalid_payload | REJECTED | The item itself is unusable (no order_number, both name forms, a zero/negative/non-finite package measurement, or an object, list or true/false in a text field; since 1.11). Resending the same identifier replays this; fix the payload and use a new identifier. |
unknown_warehouse | REJECTED | warehouse_id matches none of your warehouses or warehouse values, and you have warehouse values set up. Nothing was created for this item; the others in the request are unaffected. Add the value in Compass or correct it, then resend under a new identifier (the same identifier replays this). Since 1.10. |
unexpected_failure | REJECTED | An unexpected error; nothing was created. Resending under the same identifier is processed afresh. |
duplicate_timeout | REJECTED | With "duplicate": true: an earlier request with this identifier was still in progress. Check GET /consignments?identifier= before retrying. |
Every error response — every one, with no exceptions — is application/json,
shaped {"error": "<code>", "detail": "<message>"} (the label endpoint's
422 adds a failures array — see below). Treat error as the
stable value to branch your code on; detail is for a human, and its wording may change.
response.json() on a non-2xx status never needs a special case for "the server fell over" or
"I used the wrong verb".| HTTP | error | When |
|---|---|---|
| 400 | invalid_request | Malformed or empty consignments array (including an unparseable JSON body — see the note below), batch over 50, unknown profile_id, or (label endpoint) an unrecognised format. |
| 401 | authentication_required | No Authorization header, or not a bearer token. |
| 401 | invalid_key | The key is unknown, revoked, or inactive. |
| 401 | wrong_environment | A live key was presented to the sandbox, or a test key to production. |
| 403 | customer_inactive | The key is valid but your account is not currently active. |
| 404 | not_found | No consignment with that ref exists for your account — including a ref that belongs to someone else, or was created a different way. |
| 405 | method_not_allowed | The HTTP method isn't one this endpoint accepts — for example DELETE /consignments/{ref}, which no endpoint here supports. Carries an Allow header naming the methods that are accepted. |
| 409 | not_printable | GET /label only: the consignment is still DRAFT, every parcel on it has been voided, or it has shipped (a shipped label can no longer be printed). |
| 409 | not_cancellable | POST /cancel only: the consignment is not currently OPEN (already DRAFT, already cancelled, or shipped). |
| 429 | rate_limited | Per-minute request limit or per-hour consignment limit exceeded. Carries a Retry-After header, in seconds. |
| 207 | — | POST /cancel only, and not an error: some parcels were cancelled and an external carrier refused the rest, which stay open. See partial cancel. |
| 422 | carrier_error | POST /cancel: an external carrier refused to cancel every parcel, so nothing was cancelled; the body carries results as for 207. GET /label: an external carrier refused to create one or more parcels on their first print. No PDF is produced for the request at all. The body carries a failures array, one entry per failed parcel: {"label_number", "code", "detail", "fields"} — see carrier failure codes. The carrier's own error text is not returned; customer service can see it. |
| 500 | internal_error | An unhandled failure. Per-consignment failures never reach here — a bad item becomes a DRAFT or REJECTED result, not a 500. detail is always this generic message — the underlying error is logged on our side and never returned to you. |
User-Agent header that names your integration (for example AcmeShop-Shipping/2.1)
rather than relying on your HTTP library's default, and retry. Retrying POST /consignments with the
same identifier is safe: a consignment is never created twice. If it keeps happening, contact
customer service with the time of the request and any Ray ID shown on the error page.422)failures[].code | Meaning |
|---|---|
address_rejected | The carrier couldn't accept the delivery address — including a field longer than it allows. If it's wrong, cancel the consignment and send it again with the address corrected. |
parcel_rejected | The carrier couldn't accept the parcel itself — today, its weight. Do not retry: the same request is refused every time. Cancel the consignment and send it again with the parcel corrected, under a new identifier. fields names what was refused. |
carrier_setup_error | A problem on our side stopped the label request, or a cancel. Contact customer service. |
carrier_error | The carrier didn't accept the label request, or (in POST /cancel's results) wouldn't cancel the parcel just now. Retry the same request shortly. |
cancelled_at_carrier_but_shipped | POST /cancel's results only (since v1.16): the carrier cancelled the label, but the consignment was shipped or collected at the same moment, so the parcel was not cancelled here. Do not retry; contact customer service. See shipped during the cancel. |
For address_rejected, fields lists the address fields the carrier queried, when it said which,
and detail then ends with a sentence naming them — for example
“The carrier queried: City, Postcode / Eircode.” When a field is longer than the carrier
allows, the sentence says so instead — “The carrier allows at most 40 characters in:
Address line 2.” For parcel_rejected, fields lists the parcel fields
the carrier refused — today always weight. For every other code, and when the carrier
didn't say, fields is an empty array. Each value corresponds to a field in your request:
fields[] value | Your request field |
|---|---|
contact_name | recipient.name (or first_name / last_name) |
company | recipient.company_name |
phone | recipient.contact_number |
email | recipient.email_address |
address_line_1, address_line_2, address_line_3 | recipient.address.address1, address2, address3 |
city | recipient.address.city |
county | recipient.address.state |
postcode | recipient.address.postcode |
country | recipient.address.country |
delivery_notes | delivery_instructions |
weight | packages[].weight, for the failed parcel's package |
length, width, height | packages[].length, width, height (reserved: not returned by any carrier today) |
This API adopts Scurri's Create Consignment field names and envelope shape wherever a direct Enigma equivalent exists, so most of a migration is a base-URL and credential change rather than a rewrite. Three differences are worth knowing about before you send your first live batch:
contact_number is effectively mandatory. Scurri treats it as optional. Here,
a consignment submitted without a phone number is not rejected — it is created and finalised
up to the point of needing that field, then parked in DRAFT for someone to complete
in Compass. If your existing integration doesn't always send a phone number, you will start seeing
a steady trickle of DRAFT results with "code": "missing_fields" (reason
"This order is missing required details: phone number.") where you previously saw
success. Backfill the field at source if you can; it is
the single most common reason a consignment doesn't come straight through.service_id (or
shipping_method), add each value it sends in Compass, under
Integrations → API access → Service and warehouse values, and
customer service links each one to a label. From then on a consignment sending that value gets
that label, or is held as a DRAFT with the reason if the label can't be used; it is
never quietly sent on another one (see Values that choose the label).
A value you haven't added, or no value at all, is routed by your account's label selection rules,
the same rules that route Compass and CSV imports, so an integration that sends a fixed or
leftover value keeps working unchanged. carrier itself isn't read unless customer
service sets it up as a value field; until then it is accepted with a warning, not an error.Everything else Scurri sends that has no Enigma equivalent — customs blocks, custom fields, pick-up/drop-off store codes, and so on — is accepted and ignored with a warning, not rejected. You can point an existing Scurri payload builder at this API essentially unmodified and get a working integration on day one; the field reference above tells you exactly what each field does here so you can clean up the payload at your own pace afterwards.
Every change to this reference since v1.2, newest first. Each entry says what changed in the API, or that only the wording did, and links to the section it affects.
tracking_url now has the form https://omccouriers.com/track/?label=<label>, the form our
tracking page serves, instead of https://omccouriers.com/track/<label>. Links in the old form still work.
Store and pass on tracking_url as given rather than building it yourself. Nothing in the request changes.422, not 502.
GET /label and POST /cancel now answer
422 where they answered 502: a carrier refused or failed the label, or refused to cancel every
parcel. The JSON bodies, error and failures[].code values, fields and
results are unchanged, and 207 is unchanged. A 502 body was being replaced with an
HTML error page on its way to you, so you never received these details. If your code branches on 502 for these
two endpoints, change it to 422. Whether a retry can help is still in each failure's
code, not in the status. Older entries below describe the status as it was
at the time. Nothing in the request changes. Eircodes are also checked against the real character set: the digits and
the letters A C D E F H K N P R T V W X Y, with a routing key of one of those letters plus two digits, or D6W. An Eircode
outside that, such as D02 XKO3, is now DRAFT with invalid_postcode, and the
reason suggests D02 XK03 when swapping O and I for 0 and 1 would make it valid. The value is
never corrected for you. UK postcodes are unchanged.POST /cancel has one new per-parcel code in 207 and
502 results: cancelled_at_carrier_but_shipped. The carrier cancelled the
label, but the consignment was shipped or collected at the same moment, so the parcel was not cancelled here. Don't
retry; contact customer service. Before v1.16 the parcel could be voided on a consignment that had just shipped.
Nothing in the request changes. See shipped during the cancel and
carrier failure codes.GET /consignments/{ref} now says what
tracking_url and last_scan are for, and that a sandbox tracking_url doesn't open a
page: use Track in sandbox Compass to see a sandbox parcel's history.recipient.email_address that isn't a valid email address used to be stored as sent, and the
carrier could then refuse the parcel's label over it. It is now left off: the consignment is created and goes
OPEN as before, with no email, and the item's warnings carries
“The email address 'X' isn't valid, so it was left off.” The result's status and
code are unchanged, and a duplicate replays the warning. A carrier address refusal that names the
email now lists email in fields. Nothing in the request changes. See
Recipient.DRAFT code no_label_rule_for_parcel_size,
when every rule that could apply has a weight or size limit and a parcel fits none of them. Nothing in the
request changes. See Values that choose the label.service_id and other values can choose the label.
If your system sends a value per consignment to say which service it wants — in
service_id, shipping_method, or a key inside options such as
options.package_type — you can now add those values in Compass (Integrations →
API access) and customer service links each one to a label. A consignment sending a linked value gets
that label, and if that label can't be used (for example it doesn't deliver to the address) it becomes a
DRAFT rather than going on a different label. A value that isn't linked uses your usual
label rules as before, with a warning if you have values set up for that field. service_id
is no longer warned about as "not used by this API". New DRAFT code
no_matching_label_rule. Sending none of these fields behaves exactly as before. See
Values that choose the label."order_number": 8384832 or
"warehouse_id": 12) is now taken as its text, "8384832". An object, a list,
or true/false in a text field rejects that item only, with
invalid_payload and a reason naming the field, such as
recipient.address.city must be text, not a list.; the rest of the request carries on.
Before, any of these failed the whole request with a 500. A false, which was
read as if the field were absent, is now rejected like true. Text and
null behave exactly as before. See Text values.warehouse_id chooses the warehouse.
warehouse_id now chooses which of your warehouses each consignment ships from, instead of
having to repeat your API profile's own warehouse. Send the ID of any of your warehouses, or a value
your system already sends (for example Company | London Warehouse) once you have added it
as a warehouse value in Compass (Integrations → API access). A value that isn't set up no longer
fails the whole request: if you have no warehouse values, your profile's warehouse is used with a
warning; if you do, only that item is rejected, with the new code unknown_warehouse, and
the rest of the request carries on. Sending nothing behaves exactly as before.contact_number written in scientific notation, such as 4.47737E+11 (how a
spreadsheet shows a long number, keeping only its first few digits), is treated as missing: the
consignment goes to DRAFT with the new code phone_scientific_notation, to be
completed in Compass. A number that has only lost its leading zero is not affected. Additive: no
existing code or field changes.bt4 5 8qx comes back as BT45 8QX and d02x285 as
D02 X285. Only a value whose letters and digits already make a complete postcode is
reformatted; anything else is kept as sent (trimmed) and drafts as before. Other countries'
postcodes are trimmed only. New draft code not_a_postcode: a UK or Irish postcode with no
digit in it at all, such as a town or county. Additive: no existing code or field changes.phone, in its note box
below the contact name. delivery_instructions follows it there, and up to 50 characters of
it are printed (was 100); a longer note is cut short on the label and ends with “...”. The
full text is stored and returned unchanged. No request or response field changes.weight you sent is stored and returned unchanged. The
label endpoint's 502 has a new carrier failure code,
parcel_rejected, for a carrier refusing the parcel itself, with fields naming
it (weight → packages[].weight). Do not retry it: the same request
is refused every time. Cancel the consignment and send it again with the parcel corrected. The
502's top-level detail no longer says that retrying is always the answer.
Additive: no existing code or field changes.name (or first_name and last_name together),
company_name, address1, address2, address3 and
city may each be at most 40 characters, whichever label or carrier the consignment uses
— each address line on its own (lines are no longer joined). An item with a longer value is not
rejected and not shortened: it is created as a DRAFT with
"code": "label_field_too_long" and a reason naming the fields, and the value is
kept as sent, for the customer to shorten in Compass before finishing the consignment. If another
problem also sends it to DRAFT (a missing phone number, say), that problem's code is
returned instead. The 100-character limits of v1.3.1 (a REJECTED
invalid_payload, and maxLength in the OpenAPI description) are unchanged. To
avoid drafts, keep these fields to 40 characters before sending. A carrier's own
address_rejected length message now names a single field.GET /consignments/{ref}/label no longer returns its labels: it
answers 409 not_printable, with a detail naming the label and
saying it has been shipped. Fetch and print your labels before the consignment is shipped. A
consignment whose labels have never been fetched is also not shipped by the customer’s own
Ship action until they have been, so fetching the label is part of getting a parcel on its way.REJECTED with "code": "invalid_payload" and a
reason naming your field and its limit — for example
“recipient.address.address2 is too long: at most 100 characters.” — instead of
failing on our side. Limits: postcode 20; contact_number 30;
email_address 254; order_number, name (or
first_name and last_name together), company_name,
address1–address3, city and state 100; also
identifier and shipping_method 100 (from 11 October 2026; an over-long
identifier is rejected without being stored, so it is not replayed). They are
also in the OpenAPI description as maxLength. A partner-carrier parcel has the tighter
40-character limits below, which apply when its label is requested. Superseded by v1.4: the
40-character limit now applies to every consignment, not only partner-carrier parcels, and a longer
value makes the consignment a DRAFT rather than failing at the label request. The
100-character limits above are unchanged.POST /cancel can now report a partial cancel. When some
parcels are handed to a partner carrier and it refuses to cancel some of them, the ones it did cancel
are cancelled, the rest stay open, and the response is 207 Multi-Status with a
results entry per parcel — never 200, which now means every parcel was
cancelled. If the carrier refuses them all, the response is 502 and nothing is cancelled.
Separately, address lines 2 and 3, the county (state) and
delivery_instructions now reach a partner carrier, which allows at most 40 characters per
address line (address2 and address3 together count as one); a longer value is
refused when its label is requested, as address_rejected naming the field. The
fields list can now also name county and delivery_notes.
The 40-character rule here is superseded by v1.4: each address line is checked on its own, and the
limit applies to every consignment when it is created.insufficient_balance and the cancel endpoint now say plainly what
happens, and summary.labels no longer implies its keys arrive in any particular
order.DRAFT and
REJECTED result now carries a stable code (Result
codes). The label endpoint's 502 returns our own text with a code and the
fields the carrier queried, not the carrier's own error (Carrier
failure codes). Both are additive. The idempotency rules now say when a resend under the same
identifier is processed afresh (POST /consignments). Example
consignment references now show the real format, CN plus seven digits.