From 1 November 2026, every item on an EU-bound B2C parcel must carry customs product identifiers, under Commission Delegated Regulation (EU) 2026/1022. They are collected per item, so a parcel with three different products carries three sets.This page tells you what to change in an existing integration, then specifies each field.Migrating an existing integration#
If you already call create-or-update or create-or-update-many, the whole change is inside parcel.items[]:1.
Add nsCode, spCode and spCodeNotApplicable to each item. Nothing else in the request structure changes.
2.
Keep sending skuCode as you do today — it is the Merchant PID, unchanged.
3.
Decide per item what spCodeNotApplicable should be. This is the only part that needs a rule rather than a mapping.
4.
Optionally add originCountryCode if your items differ in origin.
5.
Check hasErrors in the response, which echoes every item as we stored it.
The shipment calls need no change — they carry no item-level data. And there is no cutover to coordinate: all four fields stay optional until 1 November, so you can deploy the day you are ready.Before and after#
The one decision: spCodeNotApplicable#
Does the product carry a manufacturer barcode (GTIN/EAN/UPC/ISBN)?
├── Yes → spCode = the barcode + spCodeNotApplicable = false
├── No → spCode = "" (or omit) + spCodeNotApplicable = true
└── Don't know yet → omit both fields entirely
Never send false with an empty spCode, and never send true alongside a barcode. Both are contradictions and both are returned as parcel errors. Omitting the field is always safe — it records "not stated", which is not the same as false.The identifiers#
| Field | Customs element | What to send | Max length |
|---|
skuCode | C127 — Merchant PID | Your own product code | 50 |
nsCode | C128 — Non-standardised manufacturer PID | The manufacturer's model or part number | 50 |
spCode | C129 — Standardised PID | The barcode: GTIN-8/12/13/14, EAN, UPC or ISBN | 30 |
spCodeNotApplicable | Y081 — exception code | true only when the product genuinely carries no barcode | — |
Each item also takes an optional originCountryCode (ISO 3166-1 alpha-2, e.g. US). It is not one of the identifiers and is not part of the mandate — origin is a separate element we otherwise resolve per shipment/account. Send it when your items differ in origin; omit it otherwise.📌 Two different fields share the name originCountryCode. The one here is per item, inside parcel.items[]. The shipment request has its own arrivalInfo.originCountryCode — the origin of the transport, unrelated. Setting one does not affect the other.
Timeline#
| Period | Behaviour |
|---|
| Now → 31 Oct 2026 | All four fields optional. The Commission has stated no sanction applies to missing or incorrect PIDs during this window. |
| From 1 Nov 2026 | Mandatory per item on EU-bound B2C parcels. A declaration missing a mandatory PID is incomplete and will not be accepted for release. |
There is no soft-enforcement period afterwards. Treat the date as hard.Field-by-field rules#
skuCode — Merchant PID (C127)#
Your own product code, e.g. CGN-02333. Must be unique per product variant: same brand, size, colour and packaging is one code; two sizes are two codes. Nothing changes in what you already send — only that from 1 November it must be on every item.nsCode — Non-standardised manufacturer PID (C128)#
The manufacturer's own model or part number, e.g. NOW-0674. Mandatory, with no exception code — there is no nsCodeNotApplicable, and Y081 covers a missing spCode only.Where the manufacturer assigned no separate reference, DG TAXUD guidance permits repeating the spCode value here — but send it explicitly; we never copy it across for you. The declaration is yours; a value we filled in would be our assertion, not yours.spCode — Standardised PID (C129)#
The manufacturer's barcode — GTIN-8/12/13/14, EAN, UPC or ISBN — exactly as printed on the product. On receipt:Grouping spaces are stripped. 590 1234 123457 is stored as 5901234123457; those spaces are typography, not data.
Hyphens are kept. In an ISBN they are part of how the code is printed, and C129 is transmitted as printed.
Never padded, and the check digit is never recomputed or validated. A GTIN-12 stays 12 digits.
Upper-cased, which matters only for an ISBN-10 X check digit.
skuCode and nsCode are free-form references, so they are not upper-cased — whitespace is collapsed and nothing else. If a code would exceed its length limit, dropping a hyphen is the right way to shorten it.spCodeNotApplicable — exception code Y081#
Send true only when the product genuinely carries no standardised barcode. Customs is then given exception code Y081 in place of C129.The field has three states, recorded exactly as you send them:| Value | Meaning | Requires |
|---|
omitted / null | Not stated — you have said nothing either way | nothing |
true | No barcode; claims the Y081 exemption | spCode empty |
false | A barcode exists | spCode non-empty |
true and false are assertions, so each must agree with spCode. null asserts nothing and is never a contradiction — silence is allowed, a false statement is not.If you always populate spCode, always sending false is correct. If you cannot guarantee that, omit the field on the items you are unsure about rather than defaulting to false.originCountryCode — optional, ISO 3166-1 alpha-2#
Two-letter code, e.g. US; case is normalised, so us is accepted. Optional, and it stays optional after 1 November. Omit it (or send null) when you do not hold it — an empty string is stored as "not stated", never as a country.Validation and error handling#
Two layers, reporting differently.Layer 1 — request boundary, HTTP 400. The whole request is refused. Paths are zero-based and prefixed with Parcel.:Parcel.Items[0].OriginCountryCode: The length of 'Origin Country Code' must be 2 characters or fewer.
Over-length skuCode, nsCode and spCode are refused here too.Layer 2 — parcel errors, HTTP 200 with hasErrors: true. The parcel is stored but flagged, and handling is skipped until you fix it. Your value is kept so you can see what you sent. Paths are one-based, no prefix:| Error | Cause |
|---|
Items[1].SpCode: cannot be empty when SpCodeNotApplicable is false | Asserted a barcode exists but sent none |
Items[1].SpCode: must be empty when SpCodeNotApplicable is true | Claimed the Y081 exemption but also sent a barcode |
Items[1].OriginCountryCode: not valid | Two characters, but not a recognised country code |
📌 Watch the index bases. The same item is Items[0] in a boundary error and Items[1] in a parcel error. Different validators, not a typo.
A contradiction is reported rather than silently corrected: clearing your spCode because you also sent spCodeNotApplicable: true would make PostPlus file a Y081 exemption you never intended.Spreadsheet uploads differ on length. There is no request boundary, so an over-long identifier is truncated instead of rejecting the parcel. API callers get the 400 above; nothing is truncated.From 1 November, completeness is enforced in addition: a parcel missing a mandatory identifier is rejected at submission.Customs, after filing#
There is no real-time EU database check of PID values — no central product register. Accuracy is assessed on risk analysis: plausibility against the description, HS code and value.If customs raises a discrepancy, we email your operational contacts with the parcel number and reason and flag the parcel in tracking; you return corrected item data; we amend or re-lodge, and the parcel is released on acceptance. Each parcel travels under its own declaration, so a hold attaches to that parcel only — it does not block the consolidated manifest or the MAWB. Unresolved parcels stay in temporary storage for at most 90 days, then must be re-exported, returned or destroyed at the shipper's cost, though cases normally resolve well before that.There is no EU-level PID fine schedule; sanctions for incomplete declarations are national law. Until 31 October the no-sanction safe harbour applies; from 1 November the real risk is non-clearance — holds and delays — rather than a defined fine.Reading back what we stored#
The create/update response echoes the items as stored, so it is where you verify what we hold, including normalisation such as a stripped spCode space or an upper-cased originCountryCode.The read endpoints return them only when you ask for items: pass IncludeItems=true on GET /api/v1/parcels/{id}, the …-nrs lookups or /query. Without it the response gives parcelItems as a count and an empty items array — the default for every item field, not something specific to the identifiers.spCodeNotApplicable comes back as null when never stated, and as true/false when it was; the three states survive the round trip.Catalogue readiness#
The API change is small — sourcing the values is the real work.| Situation | What to do |
|---|
| Barcode known | spCode = barcode, spCodeNotApplicable = false |
| Product genuinely has no barcode (unbranded, bulk, custom-made) | spCode empty, spCodeNotApplicable = true |
| Barcode exists but you do not hold it | A data-sourcing task, not an exemption. Do not send true — that is a false Y081 claim. Omit both fields until you have the value. |
| No manufacturer part number | Ask the supplier to assign an internal reference. nsCode has no exemption, so it cannot be left empty after 1 November. |
Prioritise supplier outreach for the part of your catalogue with neither a barcode nor a manufacturer reference — the only group that needs someone else to act before you can comply.Manifest file uploads#
If you manifest by uploading a spreadsheet, the identifiers are available as columns NsCode, SpCode, SpCodeNotApplicable and OriginCountryCode.SpCodeNotApplicable is read as free text: true, yes or 1 claim the exemption. A blank or absent column parses to "not stated", not to false — consistent with the API. This section applies to spreadsheet upload only.Testing#
The fields are live on test — verify against the test Swagger before you build:Worth testing explicitly:an item with a barcode (spCodeNotApplicable: false)
an item without one (true, empty spCode)
an item with neither field set — confirm it is still accepted
a deliberate contradiction — confirm you can read the error out of hasErrors
Because all four fields stay optional until 1 November, you can begin populating them in production as soon as your side is ready.