From 1 November 2026, every item on a B2C parcel up to 150 EUR imported into the EU 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. An item line that stands for several products lists one code per product (see Several products on one item line below).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 while a product is left without a barcode, and never send true with a barcode for every product. Both are contradictions and both are returned as parcel errors. Omitting spCodeNotApplicable is never a contradiction: it records "not stated", not false. From 1 November, though, an in-scope item with neither spCode nor spCodeNotApplicable is reported as missing, and the parcel is not handled until you send one.The identifiers#
| Field | Customs element | What to send | Max length |
|---|
skuCode | C127 — Merchant PID | Your own product code | 70 per code |
nsCode | C128 — Non-standardised manufacturer PID | The manufacturer's model or part number | 70 per code |
spCode | C129 — Standardised PID | The barcode: GTIN-8/12/13/14, EAN, UPC or ISBN | 70 per code |
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 B2C parcels up to 150 EUR imported into the EU. Above 150 EUR the fields stay optional. A declaration missing a mandatory PID is incomplete and will not be accepted for release. A missing identifier comes back as a parcel error, and the parcel is not handled until you send the value. |
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 of a B2C parcel up to 150 EUR imported into the EU.nsCode — Non-standardised manufacturer PID (C128)#
The manufacturer's own model or part number, e.g. NOW-0674. Mandatory on the same parcels as skuCode, 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, and so are invisible characters such as zero-width spaces. 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. Spaces, tabs and line breaks are trimmed from both ends, and each run inside becomes one space. Invisible characters (zero-width, text-direction marks) are removed. Nothing else changes, apart from placeholders (below). We never shorten skuCode, nsCode or spCode for you. If your own skuCode is over 70 characters, switch to a shorter code that still names exactly one variant. Never shorten the manufacturer's nsCode or spCode.Placeholders are stored empty. 0, -, --, ., N/A, NA, NONE, NULL, EMPTY or CLEAR (case-insensitive) in skuCode, nsCode or spCode is not a code, so we store it empty and never send it to a carrier or to customs.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, or empty at the position of each product without one |
false | A barcode exists | spCode non-empty, for every product on the line |
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.Several products on one item line#
If you declare goods per HS code rather than per product, one item line can stand for up to 33 products. List one code per product, separated by , or ;, in the same order in all three fields: position 1 in each field is the first product, position 2 the second, and so on."skuCode": "CGN-02333,CGN-02334,CGN-02335",
"nsCode": "NOW-0674,NOW-0675,NOW-0676",
"spCode": "733739006745,,733739006769",
"spCodeNotApplicable": true
Here the second product has no barcode: its spCode position is empty, and spCodeNotApplicable is true.| Rule | |
|---|
| Separator | , or ;, also full-width , ;. Spaces around it are ignored. Stored and returned with , |
| Limits | 70 characters per code, 33 codes per field |
One skuCode or nsCode | Covers every product on the line, e.g. one skuCode for all variants of a listing |
One spCode | Is one product's barcode, never all of them: list one per product |
Empty position in skuCode or nsCode | That product's code is missing |
Empty position in spCode | Needs spCodeNotApplicable: true (Y081 for that product) |
| Placeholder | A placeholder (N/A, 0, … see spCode above) in a position counts as an empty position |
spCodeNotApplicable | One per line. true: the products at the empty spCode positions have no barcode. false: every product has one |
A code that contains a separator is split in two, so replace that character in your own codes. A carrier that takes one product code per item gets the first skuCode.Validation and error handling#
Two layers, reporting differently. Both count items from 0, like the JSON array: Items[0] is the first item. Parcel errors used to count from 1; if you match on them, update the position.Layer 1 — request boundary, HTTP 400. The whole request is refused. Each entry in errorDetails names the field in property, prefixed with Parcel. (or Parcels[n].Parcel. on create-or-update-many):{ "property": "Parcel.Items[0].SpCode", "detail": "max 2343 characters", "code": "MaximumLengthValidator" }
An over-length skuCode, nsCode, spCode or originCountryCode is refused here. For the identifiers this checks the whole field, 2343 characters (33 codes of 70 with their separators); a code over 70 or a 34th code is a Layer 2 error. Match on code, not on the detail text. Every length limit is also in the Swagger schema, as maxLength.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. Two exceptions are stored empty: an identifier sent as a placeholder such as 0 (see Field-by-field rules), and an over-long identifier (a code over 70 characters, or over 33 codes). Paths have no prefix:| Error | Cause |
|---|
Items[0].SpCode: required when SpCodeNotApplicable is false | Asserted a barcode exists but sent none |
Items[0].SpCode: one per product when SpCodeNotApplicable is false | Listed products, but left one without a barcode |
Items[0].SpCode: must be empty when SpCodeNotApplicable is true | Claimed the Y081 exemption but also sent a barcode |
Items[0].SpCode: needs an empty position when SpCodeNotApplicable is true | Listed products with a barcode for every one, so none is exempt |
Items[0].SkuCode: max 70 characters per code, 33 codes | A code over 70 characters, or more than 33 codes. The field is not stored |
Items[0].OriginCountryCode: not valid | Not a recognised two-letter country code |
Items[0,2].NsCode: required for EU customs (C128) | From 1 November: the first and third items have no nsCode |
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 skip the Layer 1 check, so an over-long identifier comes back as the Layer 2 parcel error Items[0].SpCode: max 70 characters per code, 33 codes. The code is not stored. Correct it and upload again.From 1 November, completeness is checked on B2C parcels up to 150 EUR imported into the EU and reported like the Layer 2 errors above: in errorDetails (hasErrors: true), with handling skipped until you send the missing value. The 150 EUR limit is tested on the parcelValue you send; if it is 0 or omitted, on the sum of quantity × valuePerItem over all items. Other currencies are converted at the current ECB rate. If no rate is available, the parcel is treated as up to 150 EUR.Placeholders count as missing (N/A, 0, … see spCode above), whether sent by API or spreadsheet.
One message per field, naming every item that lacks it — the first 10, then .... skuCode is C127 and nsCode is C128.
spCode (C129) is reported when it is missing or a placeholder, unless spCodeNotApplicable is true: Items[0].SpCode: required for EU customs (C129) unless SpCodeNotApplicable is true. false with an empty or placeholder spCode is the contradiction error above instead.
On a line listing several products, each product needs its codes: one skuCode or nsCode covers them all, otherwise every position must hold one; spCode always needs one per product (an empty spCode position only with spCodeNotApplicable: true). A product without one is reported with the same messages, ending in for every product: Items[0].NsCode: required for EU customs (C128) for every product.
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, a placeholder stored empty, 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. From 1 November, on a B2C parcel up to 150 EUR imported into the EU, this is a parcel error and the parcel is not handled until you do. |
| No manufacturer part number | Ask the supplier to assign an internal reference. nsCode has no exemption, so after 1 November it cannot be left empty on a B2C parcel up to 150 EUR imported into the EU. |
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, next to the existing SkuCode. In all five, a cell holding -, NULL, EMPTY or CLEAR (case-insensitive) counts as blank, and in SkuCode, NsCode and SpCode so does any other placeholder (0, N/A, … see spCode above): an identifier is stored empty and never sent to customs as a code, and an origin is "not stated". A cell can list several products, as in the API. In a CSV file, quote a cell whose list uses the file's own separator, or separate the codes with the other character.SpCodeNotApplicable accepts true, yes or 1 (claims the exemption) and false, no or 0, case-insensitive. A blank or absent column parses to "not stated", not to false — consistent with the API. Any other value, such as n/a or y, is a parcel error and is read as "not stated": File parser: SpCodeNotApplicable: not valid, use true or false. When a parcel spans several rows, one per item, an error in the item columns of any of them is a parcel error. If every row has an error, the whole file is refused.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
if you list several products on one line: the response echoes the lists comma-separated, inner empty positions kept (S1,,S3), trailing ones dropped
Because all four fields stay optional until 1 November, you can begin populating them in production as soon as your side is ready.