DDPP API: integration guide
Updated 24 October 2026: a passport card becomes the passport its product needs (section 2). On 14 October 2026: gift passport cards (section 2). On 11 October 2026: DDPP's checks of businesses, permanent passports, certificates and analysis reports (sections 1 and 2). It describes what DDPP does today, not what is planned; where something is not built, it says so.
Base address: https://ddppeu.com. Every link the API writes into a passport, a label or a Digital Link is built on it. https://dppeu.cloud, the address until 23 September 2026, still answers /api/ and sends everything else to the same path on ddppeu.com, so a code printed with it keeps working.
1. What works today with no credentials at all
This is the part an importer, a customer's IT department or a marketplace can use immediately. Reading is anonymous by design — the ESPR expects public passport data to be readable without registering, and the app follows the same rule.
Read a passport
GET /api/public/passports/{public_id}
public_id is a UUID v4. Returns the passport as everyone sees it: never a person, never a member, never an email address. The same payload renders the HTML page and the resolver, so they cannot disagree.
{
"passport": { "id": "…", "level": "batch", "lot": "…", "serial": null,
"status": "published", "published_at": "…", "recalled_at": null,
"recall_reason": null, "url": "https://ddppeu.com/p/…",
"digital_link": "https://ddppeu.com/01/…" },
"product": { "name": "…", "brand": "…", "model_number": "…", "gtin": "…",
"group": "…", "group_label": "…", "image_url": "…" },
"operator": { … the economic operator … },
"classification": { "is_espr_passport": false, "espr_status": "…", "regime": "…" },
"blocks": [ … the schema's blocks, for rendering … ],
"data": { … only what the product group publishes at the public tier … },
"events": [ { "kind": "…", "title": "…", "occurred_at": "…", "place_name": "…",
"latitude": …, "longitude": …,
"media": [ { "url": "…", "sha256": "…", "mime": "…" } ],
"review_state": null } ],
"certifications": [ { "scheme": "…", "number": "…", "issuer": "…",
"valid_from": "…", "valid_until": "…", "expired": false,
"document_url": "…", "declared_by": "manufacturer",
"review_state": "awaiting" } ],
"updated_at": "…"
}
Two things worth reading carefully, because they are deliberate:
- **
declared_by: "manufacturer"** on every certification: it is the manufacturer's claim. Since 11 October 2026 DDPP looks at each certificate's document, and at each analysis report (an event of kindquality_check):review_stateisawaitinguntil a person at DDPP has, thenchecked. **checkedmeans the document matches what is written beside it, never that the product meets the scheme or that the data is true.** Show it in DDPP's words, "checked by DDPP" or "awaiting DDPP review", and never as a verification. A certificate or report DDPP did not accept is left out of the payload (and of the page and the anchored digest) until the business corrects it. Other events carryreview_state: null: they need no tick. - **
classification.is_espr_passport** decides the words. When it is true the record may be called a Digital Product Passport; otherwise call it a product passport or a product record. The API is the only thing that decides this — do not infer it.
A withdrawn passport answers 410 Gone with code: "withdrawn" and nothing but its id and status — it existed, and it is gone. An unknown id answers 404 with code: "not_found". A draft is not served at all.
Resolve a GS1 Digital Link
GET /01/{gtin}/10/{lot} Accept: text/html → 303 to the passport page
GET /01/{gtin}/10/{lot} Accept: application/json → the payload above
GET /01/{gtin}/10/{lot} Accept: application/linkset+json → an RFC 9264 linkset
GET /01/{gtin}/10/{lot}?linkType=linkset → the same linkset
The code forms are /01/{gtin} (the model), /01/{gtin}/10/{lot} (a batch), /01/{gtin}/21/{serial} (an item) and /01/{gtin}/10/{lot}/21/{serial} (an item, with the lot it came from); each leads to the most specific published or recalled passport, item first, then batch, then model. An item made with its serial alone is found whether or not the label also carries the lot. When more than one organization has entered the same GTIN, the code answers for the one whose GS1 Company Prefix the GTIN falls under, then for the one that published a passport for that code first. Content negotiation follows the GS1 resolver conventions: ?linkType= first, then Accept, best quality first, and every answer carries Vary: Accept. A 303 also carries a Link: …; rel="linkset" header. A withdrawn passport answers 410 Gone, which is the correct answer for a code that was real and is no longer served. The path is parsed raw, so a lot containing an encoded / stays one value.
Search, without a code
GET /api/public/search?q=…
Finds published and recalled passports whose organization has listing switched on, as summaries: id, status, level, lot, serial, url, product name/brand/GTIN/image, the operator's name and country, and published_at. An exact code or one of our links always resolves to that one passport whether or not it is listed — a printed label must not stop working because a business left the search. Results come a page at a time: page and per_page (25 unless asked, at most 50) choose it, and meta gives current_page, last_page, per_page and total; an exact match is a single page of one. The app's Find screen asks for the next page on "Show more".
Being in a result is not a merit and the response says nothing evaluative. The same search backs the /find page and /sitemap-passports.xml.
Public files
GET /api/public/media/{id}
Serves a file only while public content of a published or recalled passport still points at it: evidence on a public event, the product photo, or a public certification's document. Every file carries a SHA-256 of its stored bytes in the payload, so a recipient can check that what they downloaded is what the passport refers to.
Limits
60 requests a minute per IP address on the public API, 300 a minute for public files. The client's real address comes from the proxy protocol; X-Forwarded-For is not trusted.
Signed in (section 2), the ceilings are per account or API client, so one integration cannot spend another's budget: 300 requests a minute (and 600 a minute per IP behind that), 60 writes a minute (every POST, PATCH, PUT and DELETE), 30 uploads a minute, 20 invitations an hour per organization, and 5 account mails an hour. Sign-in and registration have their own small limits. A refused request answers 429 with Retry-After.
2. The member API
Everything a business does — products, passports, data, events, evidence, publishing — is a REST API under /api, authenticated with a bearer token. It is the same API the mobile app uses; there is no second, private one.
Authorization: Bearer {token}
X-Language: de # de, en, tr or es — error messages come back in that language
?lang=de on the URL does the same, for a client that cannot set a header.
Credentials for a system, not a person
An integration authenticates as an API client of the organization: a member nobody signs in as, holding an ordinary membership with a role. The same policies decide what it may do as decide for a person, so its access is the organization's own rules, not a parallel set.
POST /api/organizations/{id}/tokens
{ "name": "SAP connector", "role": "editor" }
201 { "token": "1|xxxxxxxx…", "client": { "id": 7, "name": "SAP connector", "role": "editor",
"expires_at": "2027-09-22T…" } }
- Only an owner or an admin of the organization may create one, and only as
editor(reads and writes) orviewer(reads). A client can never manage the organization, and never mint another client. - The secret is shown once. What the database keeps is a hash; there is no way to display it again. Store it in your own secret store as you receive it.
- The token lasts a year.
GET /api/organizations/{id}/tokenslists every client withlast_used_atandexpires_at, so an owner can see whether an integration is still running and when it must be rotated. - Rotating is revoking and creating another.
DELETE /api/organizations/{id}/tokens/{client}removes the token, the membership and the member at once, and the secret stops working immediately. What the client wrote stays: passports belong to the organization, not to the credential that wrote them. - A client's token is refused by
/api/me, by organization creation and by the client routes themselves, withcode: "service_token"— a leaked integration secret is not a way into anybody's account.
An owner or admin can also do this in a browser: Desk → API clients (/desk/api-clients) makes a client, shows its token once on the page that follows, lists every client with last_used_at and expires_at, and revokes one. It is the same service the endpoints above call (App\Services\ApiClients), so a client made either way is the same thing.
Everything under /api/me, the members, invitations, exports and client routes, organization creation, checkout and in-app purchases are for people only: a client's token there answers 403 service_token.
| Area | Endpoints |
|---|---|
| Sign-in | POST /api/auth/register, …/login, …/google, …/apple (→ {token, user, created}), POST /api/auth/forgot-password (always 200), POST /api/auth/logout |
| Account | GET/PATCH/DELETE /api/me, POST /api/me/password (current_password, password; keeps this token, ends every other), POST /api/me/verification |
| Organizations | POST /api/organizations (name, country, optional details and referral_code), GET/PATCH /api/organizations/{id} |
| Members | GET /api/organizations/{id}/members, PATCH/DELETE …/members/{user} (role; remove), DELETE …/membership (leave) |
| Invitations | GET/POST /api/organizations/{id}/invitations, DELETE …/invitations/{invitation}, GET /api/invitations/{token}, POST /api/invitations/{token}/accept |
| API clients | GET/POST /api/organizations/{id}/tokens, DELETE …/tokens/{client} |
| Credits | GET /api/organizations/{id}/credits (balance, item_slots, pricing by level, the web price list, history with each spend's details, and store: what the app may sell), POST …/credits/checkout (pack → a Stripe Checkout url; owners and admins) |
| In-app purchases | POST /api/store/apple (signed_transaction, the StoreKit 2 JWS; optional organization_id), POST /api/store/google (product_id, purchase_token; optional organization_id) → {already_granted, product_id, credits, organization, balance} (owners and admins of the organization the purchase was made for) |
| Exports | GET/POST /api/organizations/{id}/exports, GET …/exports/{export}/download (owners and admins; a zip, 7 days) |
| Products | GET/POST /api/organizations/{id}/products, GET/PATCH/DELETE /api/products/{id} |
| Guide | GET /api/groups (every product group, each with guide: {level, basis, asks}), GET /api/organizations/{id}/guide ({cost}: what its permanent passport takes, as …/passports/cost?level=model gives for a product), POST /api/organizations/{id}/guide (a product and its permanent passport together: product_group, name, brand?, model_number?, gtin?, expected_credits → {product, passport}; both refused together) |
| Product data | GET /api/products/{id}/editor, PUT /api/products/{id}/data, GET …/revisions |
| Certifications | POST /api/products/{id}/certifications, PATCH/DELETE /api/certifications/{id} |
| Business check | GET /api/organizations/{id}/verification (any member: state, reason, requirements, missing, document_kinds, the VIES answer in vat; owners and admins also get documents and can_submit), POST …/verification/documents (multipart kind, file), GET/DELETE …/verification/documents/{document}, POST …/verification/submit (owners and admins; people only) |
| Passports | GET /api/organizations/{id}/passports, GET /api/products/{id}/passports/cost?level= (what making one takes: a permanent passport's credits; &card= with a gift passport card), POST /api/products/{id}/passports (level, lot, serial, a batch's quantity and unit, and expected_credits for a permanent passport; card and card_uid to set a gift passport card up with it), GET/PATCH/DELETE /api/passports/{id} (DELETE answers refunded: the credits given back) |
| Gift passport cards | GET /api/cards/{code} (→ {card: {code, state, gift, gifted, gift_until, setup_until, passport}}), GET /api/cards/{code}/plan?product= (what it becomes on a product), POST /api/cards/{code}/setup; see below |
| Review | GET /api/passports/{id}/review (a permanent passport: state, reason, can_submit, blocked_by, missing, and the cost of submitting), POST /api/passports/{id}/submit (expected_credits?) |
| Batches | GET /api/passports/{model}/batches/new (what the form starts from: the next lot, the last batch's quantity, unit and destination, the fields a batch asks for), POST /api/passports/{model}/batches (lot, quantity, unit, values? → a batch draft under the permanent passport) |
| Passport data | GET /api/passports/{id}/editor, PUT /api/passports/{id}/data, GET …/revisions, GET …/readiness |
| Item passports by the run | Under a published batch: POST /api/passports/{batch}/items (serials: [...], or range: {prefix?, from, to, width?} → {created, first, last}: drafts, all or none, at most 1,000 a run), POST …/items/import (multipart file: a CSV whose first row names serial and any of the passport's own fields by key; comma, semicolon or tab; UTF-8), GET …/items/cost ({drafts, published, room, next_run, credits, balance, item_slots, charging}), POST …/items/publish (expected_credits from the cost → {published, remaining, stopped}; 250 a run; the run stops at the first refusal and says why) |
| Access to what is not public | Owners and admins, people only: GET/POST /api/organizations/{id}/access (email, audience: interested or authority, purpose, product_id?, expires_at? as YYYY-MM-DD), POST …/access/{grant}/revoke (making grants answers 403 access_closed until DDPP opens it). The person given access, signed in with that confirmed address: GET /api/shared (what they hold), GET /api/shared/passports/{public_id} (a published passport's public fields and those their audience may read, each with public and its Annex XIII access group; 404 for anyone else; every reading is recorded) |
| Lifecycle | GET /api/passports/{id}/cost (what the draft's next step takes now — submitting a permanent passport, publishing a batch or an item: step, credits, balance_after, and why), POST /api/passports/{id}/publish (batches and items; expected_credits?: the cost shown, which is then what is taken or nothing; 402 not_enough_credits when charging is on; a permanent passport answers 409 review_required), …/recall (reason?), …/withdraw (owners and admins) |
| Events | GET/POST /api/passports/{id}/events, PATCH/DELETE /api/events/{id}, POST /api/events/{id}/media |
| NFC chips | GET /api/passports/{id}/chips, POST …/chips/check (uid → {already, locked}), POST …/chips (uid, kind: tag or card, locked? → 201 new, 200 the same chip again) |
| Files | POST /api/organizations/{id}/media, GET /api/media/{id} |
| Public | GET /api/public/passports/{public_id}, GET /api/public/search?q=, GET /api/public/media/{id} (section 1) |
| Stripe | POST /api/stripe/webhook (Stripe alone, by its signature) |
| App Store | POST /api/store/apple/notifications (App Store Server Notifications V2: Apple alone, by its signature) |
| Health | GET /api/health |
How the data model behaves
- An organization is the economic operator a passport names. Members have roles: owners and admins manage it, editors write, viewers read.
- An organization can record the DDPP partner that introduced it:
referral_codeonPOST /api/organizations, the partner's code in any case (one no partner has is a 422 onreferral_code). It is recorded once, as the organization is set up, and never returned or changed afterwards; aPATCHthat sends it changes nothing. In a browser, the partner's linkhttps://ddppeu.com/desk/register?ref=CODEfills it in on the new-organization form. - A product carries what every one of its passports inherits: name, brand, model, GTIN, product group, image, certifications. A GTIN is unique within an organization. Its
gtin_statusisownwhen the GTIN falls under the organization'sgs1_company_prefix(6 to 12 digits, set on the organization; read after the GTIN's indicator digit),claimedwhen it does not, and null without a GTIN. It is the organization's own word, not a check by GS1. - A passport covers a level — model, batch or item — and a batch by default. A product's model passport is its permanent passport: it holds what every lot shares. A batch is issued under it (the permanent passport must be published first: 409
model_required), is named by its lot, says how much it is (quantity, withunitkg,t,L,pcs,morm2, needed before it is published and fixed after, so a correction is a new batch), and shows the permanent passport's data together with its own. An item is issued under the published batch of its lot (409batch_requiredotherwise), named by its serial, and shows the batch's and the permanent passport's data too. One passport per product, level, lot and serial. - Data is append-only. Every save writes a whole new revision and the latest one is current; a
PUTchanges only the keys it sends (nullor""clears one), and a save that changes nothing writes no revision. Nothing is ever updated in place or deleted. - The schema is server-side.
/editorreturns the fields for the product's group, so new EU rules do not need a new app or a new integration. - **
readiness** lists what is still missing before a passport may be submitted or published. - DDPP checks the business before its passports go live (owner's spec, 2026-10-11). An organization starts
unverified; its owners and admins send its papers — in Türkiye its vergi levhası (tax_certificate) and its ticaret sicil gazetesi (trade_registry_gazette) or a faaliyet belgesi (activity_certificate); elsewhere a registry extract (registry_extract) or another paper (other) — and its legal name, then ask (pending). An EU business's VAT number is checked in the EU's VIES; an answer VIES cannot give leaves only that check pending, never the request, and is asked again. A reviewer at DDPP then approves (verified), asks for more (back tounverified, with thereason), or rejects (rejected, with the reason; the business may send better papers and ask again); DDPP may also suspend a business (suspended), and a suspension lifted returns it to where it stood (topendinginstead ofverifiedwhen its legal name, country or VAT number changed while it was suspended). The owners and admins are told each decision by email. While a business is suspended, nothing new goes live on its published passports (403organization_suspended): no event, certificate or data change on a public passport or its product, and no change to the organization details its passports print or to itsgs1_company_prefix. What is published stays visible, recalls and withdrawals still work, and drafts stay editable. **Drafting is open in every state; submitting a passport for review and publishing anything needverified** (403organization_unverified). Papers are seen only by the owners and admins (never an API client, whatever its role:documentsis null and thecan_*flags false for one) and DDPP's reviewers, and are never public; they stay as sent while the business is being checked and after. Changing the legal name, the country or the VAT number of a checked business sends it back topending. - A product's permanent passport is reviewed by DDPP and goes live when approved. It is paid for when it is made (owner's decision, 2026-10-11):
GET /api/products/{id}/passports/cost?level=modelsays what that takes,expected_creditson thePOSTconfirms it, and the rules are publishing's (cost_changed,cost_unconfirmed,not_enough_credits). One made while charging was off pays when it is submitted. Its review:draft→submitted(POST …/submit, once the business is checked and the passport is ready) →approved(it is published at once, with no further charge),changes_requested(with thereason; edit and submit again) orrejected(final, with the reason: its credits come back as a new ledger entry,passport_refund, naming the charge it returns, never twice). Deleting a draft that was never submitted gives its credits back too; one deleted after it was submitted keeps them.passport.reviewcarries the state. Batches and items are not reviewed: they are issued under a passport that was, and published by the business. - After approval, edits go live at once. A new certificate, or a new analysis report (an event of kind
quality_check), showsreview_state: awaitinguntil a reviewer ticks it (checked); one a reviewer does not accept (rejected, withreview_reason) leaves the public page until the business corrects it, and its owners and admins are told why. Changing what was ticked puts it back toawaiting, unless DDPP has already decided on exactly the new content for the same product (a certificate) or the same passport (a report): then its latest decision stands (checked, orrejectedwith its reason), also for a certificate or report added again there. The same certificate on another product, or the same report on another passport, waits for its own review. A certificate is corrected where it is; a report on a published passport is corrected by adding the corrected report as a newquality_checkevent, since a published passport's events are only added to. - The guide says how a product group's passports are issued:
basisislaw(an adopted act sets the level),expected(an act still to come; the level is DDPP's assumption until then) orchoice(no act:asksis true, and the level is a suggestion the answer to "what changes from one unit to the next?" replaces:unit→ item,run→ batch,none→ model). Whatever the level, the guide makes the product and its permanent passport, as one: a draft paid for as it is made, like any permanent passport (GET /api/organizations/{id}/guidegives itscost,expected_creditson thePOSTconfirms it, and a refusal leaves no product behind). Its batches, or its items, are issued under it once DDPP has approved it. - An NFC chip is used once. Its ID (7, 8 or 10 bytes of hex; a 4-byte ID is random or shared and is refused) is recorded for one passport, whichever organization asks, and for any passport but a withdrawn one; a gift passport card's chip is taken too. A draft's chip carries its page link (
/p/…), which stays its own through publishing, since a draft's Digital Link may still change with its GTIN or lot; a published or recalled passport's carries its Digital Link, or else its page. The record keeps the link the server computes, never one the client sends; recording the same chip again only raises its lock. Writing the chip itself is the client's: the app checks, writes, then records. - Lifecycle: draft → published → recalled → withdrawn. Only a draft can change its lot, serial or GTIN, or be deleted. After publishing, events can only be added and data changes become new revisions. A recall keeps the passport public with a notice, and any editor may make one; a withdrawal ends it, and only owners and admins may. Credits pay for passports once charging is on: a permanent passport 40 credits once, when it is made (given back if DDPP rejects it, see above); a batch one credit per 20 tonnes, 20,000 litres or 10,000 pieces (or other units) begun and at least one, and an item one of the organization's item passports (one credit buys 100), both when they are published; everything else is free.
GET /api/passports/{id}/costsays what a draft's next step will take; send itscreditsback asexpected_creditsonpublish, and a different cost is refused with 409cost_changed(nothing spent, the freshcostattached). Without it, an API client's publication is refused above 100 credits and a person's above one credit (409cost_unconfirmed, with thecost): an integration that publishes large batches reads the cost first and sends it. A batch counted in pieces takes no more item passports than its quantity (409batch_full), and a batch'squantityandunitare in its public JSON. Send the quantity as a JSON number or a plain decimal with a dot ("20000","25.5"): a string like"40.000"reads two ways and is refused (422 onquantity), unless it is the quantity DDPP gave you, sent back unchanged (the draft's own onPATCH, the template's onPOST …/batches). A recalled permanent passport or batch takes no new batches or items (409model_required,batch_required).
Gift passport cards
Since 14 October 2026 DDPP hands out NFC cards whose chip and printed QR code carry https://ddppeu.com/c/{code}. The code is ten characters of Crockford base32, read in any case, with or without spaces and hyphens, and with I or L for 1 and O for 0. A business sets a card up on one of its products, and the card becomes the passport that product needs; from then on the card's link leads to that passport.
GET /api/cards/{code}
Any signed-in member may ask:
{ "card": { "code": "7KQ2M9XD10",
"state": "available", // available | yours | taken | expired
"gift": true, // would setting it up now make the permanent passport free
"gifted": null, // yours only: whether it was set up as a gift
"gift_until": "2026-11-22", // days in Europe/Istanbul, inclusive; null: no gift
"setup_until": "2026-12-31", // null: no end
"passport": null } } // yours only: { id, public_id, product_id, organization_id, level }
yoursis a card set up in one of your organizations;takenis someone else's and says nothing more about it;expiredis one nobody set up before itssetup_until. An unknown code answers 404card_unknown.- Looking codes up is limited: 20 a minute and 100 an hour per account (429 with
Retry-After), and the same budget covers every request that names a card. - What it becomes (since 24 October 2026):
GET /api/cards/{code}/plan?product={id}, for a product you may edit:
``jsonc { "plan": { "becomes": "batch", // model | batch | item | existing "level": "batch", // the product's level (below) "product": { "id": 12, "name": "Linen shirt" }, "passport": { "id": 40, "public_id": "…", "status": "published" }, // its permanent passport, once it has one "lot": "LS-02", "quantity": "200", "unit": "pcs", // a lot: what it starts from (the next after the last) "lots": [], // a unit: the published lots to choose from, the latest first ("lot" is the latest) "cost": null } } // a permanent passport: its quote (as …/passports/cost); null where nothing is charged now ``
- model: the product has no permanent passport, and the card makes it (the gift applies). - existing: the product's level is model (its permanent passport covers every unit), so the card leads to that passport, whatever its state. Nothing is made or charged, and a passport can have several cards this way. - batch or item: a new lot's passport, or one unit's under a published lot, made as a draft with the card under the approved permanent passport. It costs what any lot or unit costs when it is published; it is never a gift. - Refused: card_wait_for_review (409: the permanent passport is a draft or in review), card_batch_needed (409: a unit needs a published lot), card_passport_rejected (409: DDPP rejected it; delete its draft first), card_passport_withdrawn (409), model_required (409: it is recalled), and card_taken or card_expired as above. - The product's level is the law's where its product group's law sets one; otherwise the guide's answer (varies on POST /api/organizations/{id}/guide: unit, run or none), then what the product already has (unit passports, then lots), then the group's suggestion. - Setting it up: POST /api/cards/{code}/setup with product, the becomes the plan said, optionally card_uid (the chip's UID in hex as the phone read it, 4 to 10 bytes, colons allowed) and what it becomes asks for: expected_credits (model), lot, quantity and unit (batch), or lot and serial (item). It answers 201 with {passport, card}. A plan that changed meanwhile (a permanent passport made or approved) is refused with 409 card_plan_changed, carrying the fresh plan. The card is set up in the same transaction as the passport, so two people tapping one card set it up once, and any refusal leaves the card as it was. - Before 24 October a card was set up only by making a permanent passport with it, and that still works: POST /api/products/{id}/passports with level: "model", card and optionally card_uid (422 card_level for any other level). - The gift: a card set up on or before its gift_until makes its permanent passport free for good: nothing is charged when it is made, nor when it is submitted, whenever that is. Read the cost first with GET /api/products/{id}/passports/cost?level=model&card={code}: credits is 0 and basis.gift_card true (with basis.gift_until) while the gift is open, the normal price after it, and the same refusals as setting it up; send credits back as expected_credits. The credit history shows the gift as an entry of reason gift_card that moves nothing (delta 0), and a draft's GET /api/passports/{id}/cost shows basis.gift_card too. A card set up after gift_until and on or before setup_until costs the normal price. - Deleting the passport's draft, or DDPP rejecting it, releases its cards: each can be set up again while it is open, and is a gift again only if that happens on or before its gift_until. - The card's own page, GET /c/{code}, needs no account: it leads to the passport once that is public or withdrawn (303 to /p/{public_id}), names the organization preparing it until then, explains how to set an available card up, and answers 410 once a card nobody set up has expired.
Errors
HTTP status plus a machine-readable code and a message already translated into the request's language. Validation errors arrive as Laravel's errors map (422). Branch on code, never on message. The codes, by area:
- Signing in and the account:
invalid_credentials(401 on sign-in; 422 on a password change with the wrong current password),no_password(409: the account signs in with Google or Apple),token_not_proven(401),provider_not_configuredandprovider_unreachable(503),email_unverified(401),unverified(403: confirm the address first),nothing_to_verify(409),mail_unavailable(503),service_token(403: an API client's token on a person's route). - Organizations and people:
last_owner(409),needs_owner(403),yourself(409),already_member(409),not_a_member(404),invitation_invalid(404),invitation_not_yours(403),too_many_invitations(429),export_expired(410). - Billing:
not_enough_credits(402, withbalanceandcost),billing_unavailableandbilling_unreachable(503). - The business check:
organization_unverified(403, withverification_state: submitting for review and publishing need a checked business),organization_suspended(403: nothing new goes live on a suspended business's published passports),verification_not_open(409: it is being checked, was checked, or is suspended),verification_incomplete(422, withmissing),documents_locked(409: papers stay as sent while the business is checked and after). A paper of the wrong kind for the business's country, or a file that is no photo or PDF, is a 422 onkindorfile. - Passport reviews:
review_required(409: a permanent passport is submitted, not published),not_reviewed(409: batches and items are not reviewed),review_not_open(409, withreview_state: already submitted, approved, or rejected for good). - Gift passport cards:
card_unknown(404: no card has this code),card_taken(409: it is set up already),card_expired(409: its setup period is over),card_level(422: making a passport withcardmakes a permanent one,level: "model"),card_wait_for_review,card_batch_needed,card_passport_rejected,card_passport_withdrawnandcard_plan_changed(409, withplan; see section 2). Acard_uidthat is no hex UID is a 422 oncard_uid. - In-app purchases:
billing_unavailable(503, withcharging: false: nothing is sold while charging is off),store_not_verifiedandstore_refunded(422: final, the purchase adds nothing),store_pending(409: the store waits for the money),store_other_organization(409: bought for an organization the sender does not manage),store_unavailableandstore_unreachable(503: try again later). Only a 200 means the credits are there; the app finishes a store purchase after that and never before. - Products and passports:
has_passports(409),identifier_locked(409),published(409),not_draft(409),model_required(409: a batch needs the product's published permanent passport),batch_required(409: an item needs the published batch of its lot),not_model(409: batches are issued under a model passport),batch_full(409: a batch counted in pieces has as many item passports as pieces),cost_changedandcost_unconfirmed(409: see publishing),identity_changed(409: the lot changed while the passport was being published; open it again),not_ready(422, withmissing; a batch without its quantity listsquantity),not_published(409),withdrawn(409 on a recall or withdrawal of a withdrawn passport, or a chip for one; 410 when reading it),chip_in_use(409: the chip is recorded for another passport, or is a passport card's, which the answer does not name),not_found(404).
3. What is not built yet
These are on the roadmap, not in the product.
- No webhooks and no event subscriptions. A client that wants to know when a passport was published has to poll.
- Bulk import for item passports only. A batch's serials and each item's own data come in by range or CSV (section 2, "Item passports by the run"); products and their data do not. An export of a whole organization exists (
/api/organizations/{id}/exports: owners and admins, never an API client; a zip of JSON and files, downloadable for 7 days), which the Terms promise free of charge. - No signed passports yet. W3C Verifiable Credentials, EPCIS and EU registry submission are planned, not built.
- No sandbox. There is one environment; a client integrating today works against live data.
4. Worked example: read a passport from a scanned code
# A scanned GS1 Digital Link, asked for as data rather than a page
curl -sH 'Accept: application/json' \
'https://ddppeu.com/01/09506000134352/10/LOT-2026-07' | jq '.passport.status, .product.name'
# The same passport by its public id
curl -s 'https://ddppeu.com/api/public/passports/2b1e…' | jq '.certifications[].declared_by'
# What links exist for this code, for a client that speaks GS1
curl -sH 'Accept: application/linkset+json' \
'https://ddppeu.com/01/09506000134352/10/LOT-2026-07'
No key, no account, no rate-limit negotiation. That is the part of DDPP a partner can integrate against this afternoon.