OMC Couriers — Consignment API

Reference v1.18 · push consignments in, get labels back · for developers integrating a warehouse or order system directly
Applies to: Consignment API v1.1 (/v1/), shipped August 2026, with the additive result codes of September 2026 Machine-readable description: OpenAPI 3.1 at developers.omccouriers.com/consignment-api/openapi.yaml Published at: developers.omccouriers.com/consignment-api/
This page is kept in step with the running API. It is written against the shipped implementation rather than a design document, and it is updated in the same change as the behaviour it describes. Where this page and anything else you have been told about the API disagree, trust this page.

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.

Contents

  1. Overview
  2. Base URLs
  3. Authentication
  4. Rate limits and the batch cap
  5. Endpoints
  6. Request field reference
  7. Worked examples
  8. Error catalogue
  9. Migrating from Scurri
  10. Change history

Overview

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.

ConceptWhat you need to know
Base URLSee Base URLs below — live and sandbox.
FormatJSON request and response bodies, except the label endpoint, which returns application/pdf.
AuthBearer API key, per customer, generated in Compass. See Authentication.
IdempotencySupply 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.
SynchronousA create call returns label numbers directly — there is no job id to poll. This is why batches are capped at 50 (see Rate limits).

Base URLs

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).

DeploymentBase URL
Livehttps://api.omccouriers.com/api/customer/v1
Sandboxhttps://sandbox-api.omccouriers.com/api/customer/v1

Authentication

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.

Two different things are both called "environment" here, and only one of them is in a response body. Your key's own 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.

Rate limits and the batch cap

LimitDefault
Consignments per POST /consignments call50
Requests per key per minute60 — every authenticated call counts, not only creates
Consignments per key per hour2,000 — only POST /consignments counts against this one
GET /consignments page size100, 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.

Endpoints

GET /ping

Auth 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" }
  ]
}

POST /consignments

Create 1–50 consignments in one call. See Worked examples for full request/response bodies. The short version:

GET /consignments

List 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
}

GET /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.

In the sandbox, 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.

GET /consignments/{ref}/label

Returns the label PDF. ?format=thermal (default, one label per page) or ?format=a4 (two-up).

The first call is not a passive read. For a consignment routed to certain carriers, calling this endpoint for the first time is what actually creates the parcel with that carrier and produces its label — the same as clicking "Print" in the Compass portal. Calling it again re-serves the same label; it does not create a second parcel or draw a second charge — until the consignment ships, after which the label can no longer be retrieved (409).
One cancelled parcel does not block the rest. If a parcel on this consignment has been individually cancelled (via Compass — this API has no per-parcel cancel of its own), this call still returns a PDF covering the parcels that remain live. You only get 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.
A failed carrier call fails the whole request, not just one label. If any parcel on this consignment fails to create with its carrier, you get 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.

POST /consignments/{ref}/cancel

Cancels 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 }
  ]
}
A cancel can partly succeed — check for 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.

Shipped during the cancel (v1.16). If the consignment is shipped or collected while a parcel is being cancelled with the carrier, the carrier may already have cancelled it. That parcel is then 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.

Request field 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".

Consignment level

FieldTreatmentNotes
identifierSupportedYour own reference. Becomes the idempotency key. Unique per your account, not globally. Optional — omitting it means no replay protection.
order_numberRequiredYour order reference.
delivery_instructionsSupportedFree 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_idUsedOptional. 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_idUsed for label selectionOptional. 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_methodUsed for label selectionAs service_id (since 1.12). Also recorded against the request. No warning.
carrierAccepted, ignored + warningNot 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_dateAccepted, ignored + warningEnigma stamps its own timestamps.
order_value, currencyAccepted, ignored + warningNo customs documentation or value-based product in this version.
custom_field_1, custom_field_2Accepted, ignored + warningScurri rules-engine inputs. Not read unless customer service sets one up as a value field (below).
export_customs, invoice, options, custom_attributesAccepted, ignored + one warning eachWhole 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).

Packages

FieldTreatmentNotes
weightSupported, validatedKilograms. 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, heightSupported, validatedCentimetres. Absent or blank → profile defaults, field by field. Same validation as weight above, applied per dimension.
description, referenceAccepted, ignored + warningNo per-parcel reference field exists yet.
items[]Accepted, ignored + warningCustoms 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.

A zero weight is not the same as an absent one. Sending "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.

Recipient

FieldTreatmentNotes
name or first_name + last_nameSupportedSupplying 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_nameSupportedAt most 40 characters; longer makes the consignment a DRAFT (label_field_too_long).
email_addressSupportedOptional. 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_numberSupported, effectively requiredOptional 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_numberAccepted, ignored + warningCustoms identifiers.

Address

FieldTreatmentNotes
address1 / address2 / address3Supportedaddress1 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.
cityRequiredAt most 40 characters; longer makes the consignment a DRAFT (label_field_too_long).
stateSupportedMaps to county.
postcodeRequired for GB/IEChecked 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.
countryRequiredISO 3166-1 alpha-2, or a full country name.
store_codeAccepted, ignored + warningNo pick-up/drop-off service in this version.
A field that appears nowhere in these tables at all — not even in one of the "accepted, ignored" rows — is still accepted and warned about, never rejected. A future field your system starts sending, or your own private annotation on the payload, will never break an otherwise-correct integration.

Text values

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):

Values that choose the label

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:

  1. In Compass, under Integrations → API access, add each value your system sends, exactly as it sends it, under the field it is sent in: 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.
  2. Customer service links each value to a label. Compass shows which label each value leads to, or "Not linked yet". A linked value can't be edited or removed in Compass; contact customer service if it needs to change.

Then, for each consignment:

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.

Worked examples

A single consignment

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": []
    }
  ]
}

A multi-parcel consignment

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, with a mixed outcome

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.

Result codes

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.

codestatusMeaning
missing_fieldsDRAFTRequired details are missing; reason names them (for example "phone number").
unknown_countryDRAFTThe delivery country isn't one we recognise.
invalid_postcodeDRAFTThe 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_postcodeDRAFTThe 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_notationDRAFTThe 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_configuredDRAFTNo label rules are set up for your API profile yet. Contact customer service.
no_matching_label_ruleDRAFTLabel 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_sizeDRAFTEvery 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_availableDRAFTThe labels set up for your account aren't available at the moment. Contact customer service.
no_label_for_collection_pointDRAFTNone of your labels can be collected from the warehouse this consignment ships from. Contact customer service.
no_label_for_destinationDRAFTNone of your labels deliver to this address.
parcel_over_limitsDRAFTA parcel is too heavy or too large for every label available for it.
insufficient_balanceDRAFTThe account does not hold enough of the chosen label. Once more is available, finish the draft in Compass.
label_field_too_longDRAFTThe 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_reviewDRAFTAny other reason the consignment couldn't be finalised automatically.
label_not_determinedDRAFTOnly on results stored before codes were introduced (replayed duplicates): no label rule matched, cause not recorded.
invalid_payloadREJECTEDThe 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_warehouseREJECTEDwarehouse_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_failureREJECTEDAn unexpected error; nothing was created. Resending under the same identifier is processed afresh.
duplicate_timeoutREJECTEDWith "duplicate": true: an earlier request with this identifier was still in progress. Check GET /consignments?identifier= before retrying.

Error catalogue

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.

This is guaranteed, not just true of the cases the application code shapes itself. A dedicated middleware converts anything Django's own framework would otherwise return unmodified — an unhandled exception, or a request using a method an endpoint doesn't support — before it reaches you, for every endpoint on this page. A client that always calls something like response.json() on a non-2xx status never needs a special case for "the server fell over" or "I used the wrong verb".
HTTPerrorWhen
400invalid_requestMalformed 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.
401authentication_requiredNo Authorization header, or not a bearer token.
401invalid_keyThe key is unknown, revoked, or inactive.
401wrong_environmentA live key was presented to the sandbox, or a test key to production.
403customer_inactiveThe key is valid but your account is not currently active.
404not_foundNo consignment with that ref exists for your account — including a ref that belongs to someone else, or was created a different way.
405method_not_allowedThe 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.
409not_printableGET /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).
409not_cancellablePOST /cancel only: the consignment is not currently OPEN (already DRAFT, already cancelled, or shipped).
429rate_limitedPer-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.
422carrier_errorPOST /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.
500internal_errorAn 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.
An HTML error page or a dropped connection means the request never reached the API. If you get an HTML page (or a short plain-text error) instead of the JSON above, or the connection is reset before any response, the request was stopped on its way to us and nothing was processed. Set a 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.

Carrier failure codes (422)

failures[].codeMeaning
address_rejectedThe 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_rejectedThe 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_errorA problem on our side stopped the label request, or a cancel. Contact customer service.
carrier_errorThe 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_shippedPOST /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[] valueYour request field
contact_namerecipient.name (or first_name / last_name)
companyrecipient.company_name
phonerecipient.contact_number
emailrecipient.email_address
address_line_1, address_line_2, address_line_3recipient.address.address1, address2, address3
cityrecipient.address.city
countyrecipient.address.state
postcoderecipient.address.postcode
countryrecipient.address.country
delivery_notesdelivery_instructions
weightpackages[].weight, for the failed parcel's package
length, width, heightpackages[].length, width, height (reserved: not returned by any carrier today)

Migrating from Scurri

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:

  1. 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.
  2. Your system can still choose the label per consignment, once it's set up. If your Scurri integration picks the service for each shipment in 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.
  3. The batch cap is 50, not Scurri's 50,000. Scurri's manifest call doesn't return allocated labels, so it can accept a huge batch and process it asynchronously. This API returns real label numbers in the same response, so each item does real work — validation, rule resolution, a label draw, and for some destinations a live carrier call — before it can reply. If your current integration pushes very large batches in one call, split them into batches of 50 or fewer.

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.

Change history

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.

Reference v1.18 (6 November 2026) — tracking link form. A parcel's 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.
Reference v1.17 (5 November 2026) — carrier failures answer 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.
Reference v1.16 (3 November 2026) — a cancel that meets a ship. 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.
Reference v1.15 (1 November 2026) — tracking links in the sandbox. Wording only; nothing in the API changed. 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.
Reference v1.14 (30 October 2026) — an invalid email address is left off with a warning. A 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.
Reference v1.13 (29 October 2026) — label rules for a weight or size. Customer service can now limit a label rule to parcels of a certain weight, or whose longest side is up to a certain length, so that, for example, small parcels go on one label and the rest on another. Every parcel in the consignment must fit; a parcel sent without all three dimensions (and with no default dimensions on your API profile) never fits a size limit. New 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.
Reference v1.12 (29 October 2026) — 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.
Reference v1.11 (29 October 2026) — numbers in text fields. A number sent in a text field (for example "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.
Reference v1.10 (29 October 2026) — 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.
Reference v1.9 (28 October 2026) — phone numbers in scientific notation. A 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.
Reference v1.8 (28 October 2026) — postcodes in standard form. A UK postcode or Eircode is now stored, and returned, in its standard form: spaces and capital letters are tidied, so 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.
Reference v1.7 (27 October 2026) — you generate your own keys. Keys are now set up, generated and revoked by you in Compass, under Integrations → API access, rather than issued by customer service (see Authentication). The API itself is unchanged: no request, response or error changes.
Reference v1.6 (27 October 2026) — the recipient's phone on a partner carrier's label. A partner carrier's label now prints the recipient's 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.
Reference v1.5 (26 October 2026) — carrier minimum weights, and a new carrier failure code. Some partner carriers have a minimum weight. A parcel lighter than its carrier's minimum is no longer refused: it is declared to the carrier at the minimum, so the carrier's label shows the minimum, while the 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.
Reference v1.4 (22 October 2026) — 40 characters for label fields, on every consignment. 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.
Reference v1.3.2 (19 October 2026) — no labels after shipping. Once a consignment has shipped, 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.
Reference v1.3.1 (2 October 2026) — field length limits. A field longer than we can store is now 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.
Reference v1.3 (2 October 2026) — one new status code, one new limit. 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.
Reference v1.2.1 (30 September 2026) — wording only, no API change. The descriptions of insufficient_balance and the cancel endpoint now say plainly what happens, and summary.labels no longer implies its keys arrive in any particular order.
What changed in reference v1.2 (September 2026). Every 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.