DPPs — create, read, update, delete
The DPP is the central object: one row per product (model or variant granularity), per batch, or per individual item.
The endpoints on this page accept an API key
(Authorization: Bearer dpp_live_…) as well as a signed-in dashboard
session. With a key, the tenant comes from the key: tenant_id can be
left out, and a tenant_id naming another tenant is refused with
403 tenant_mismatch. See Authentication.
Object shape
{
"id": "3f2a9b1c-e0d8-4f8e-b308-d2541886e331",
"tenant_id": "8d3a4f91-427f-4d5d-b527-c0b28363676c",
"passport_id": "https://example.dpp.dppagent.com/01/09501101530003/21/AX-001",
"gtin": "09501101530003",
"batch": null,
"serial": "AX-001",
"cpv": null,
"category": "electronics",
"granularity": "item",
"dpp_status": "active",
"link_to_previous_dpp": "https://example.dpp.dppagent.com/01/09501101530003",
"previous_dpp": null,
"dpp_data": { "espr.pid.product_name": "Kompas Portable Speaker" },
"last_updated": "2026-06-26T13:42:05.000Z",
"created_at": "2026-05-10T09:18:21.000Z"
}
id is a UUID. passport_id is the passport's public address, built
from the tenant's host and the identifiers. link_to_previous_dpp is
the parent: the passport this one inherits its data from.
previous_dpp is the predecessor: the passport this one replaces.
The two are separate; see Parent and predecessor. dpp_data holds the
product data as flat keys (espr.pid.product_name, bat.chemistry,
…); the category guides list the keys per category.
Create one DPP
POST /api/v1/dpps
Authorization: Bearer dpp_live_…
Content-Type: application/json
{
"gtin": "09501101530003",
"category": "electronics",
"granularity": "model",
"dpp_status": "draft",
"dpp_data": {
"espr.pid.product_name": "Kompas Portable Speaker",
"espr.mfr.name": "Example Audio A/S",
"espr.eo.country": "DK"
}
}
| Field | Notes |
|---|---|
gtin |
8 to 14 digits. A draft can be saved without one. |
category |
One of textile, ppe, ppe_textile, footwear, battery, electronics, ict, cosmetics, detergents, chemicals, furniture, mattresses, construction, tyres, iron_steel, aluminium, general. |
granularity |
model, variant, batch or item. variant needs cpv, batch needs batch, item needs serial. |
cpv, batch, serial |
The GS1 Digital Link qualifiers /22/, /10/ and /21/. |
dpp_status |
Defaults to active: without "draft" the passport is published at once. |
link_to_previous_dpp |
The parent: address of the passport this one inherits from (a product, model or batch). Every empty field is filled from it when the passport is shown. Despite the name, not the passport it replaces. |
previous_dpp |
The predecessor: absolute http(s) address of the passport this one replaces, which may be another provider's passport. Nothing is inherited from it. |
facility_id, content_specification_ids, dictionary_references |
Optional passport header values. |
dpp_data |
The product data. |
Publishing (any status other than draft) needs a valid economic
operator identifier on the tenant (an LEI, GS1 company prefix or
PEPPOL identifier). A battery passport also needs
dpp_data["bat.category"] (PORTABLE, SLI, LMT, INDUSTRIAL or
EV) unless it inherits it from a parent.
Response — 201 Created:
{
"dpp": { "id": "3f2a9b1c-…", "passport_id": "https://example.dpp.dppagent.com/01/09501101530003", "dpp_status": "draft", "…": "…" },
"public_url": "https://example.dpp.dppagent.com/01/09501101530003",
"tier": "light",
"blippa": null
}
dpp is the stored row. public_url is the address to print or link
to: the brand's own domain when one is verified. blippa reports the
push to a connected page builder for tenants that publish there, and
is null otherwise.
Errors: 400 with { "error": "Validation failed", "details": [ … ] }
listing each problem; 402 when publishing would take the tenant over
its plan's number of active passports (drafts are always allowed).
One batch, many items
Send items with a batch to create a batch passport and one item
passport per serial:
{
"gtin": "09501101530003",
"category": "battery",
"batch": "B-2026-17",
"dpp_data": { "bat.category": "LMT" },
"items": { "quantity": 250, "format": "sequential", "prefix": "SN-", "padding": 6 }
}
format is one of:
sequential—prefixplus a number fromstart(default 1), zero-padded topaddingdigits (default 5).sgtin— numeric serials for an SGTIN; needscompanyPrefix(6 to 12 digits) anditemRef(digits).custom_pattern— apatternwith{YEAR},{WEEK},{MONTH},{DAY}and{SEQ}tokens; a width can follow a colon, as in{SEQ:6}.
The batch passport carries the product data; each item passport carries its serial and inherits the rest.
Up to 500 items are created in the request, and the response adds
items_created, first_item and last_item. Above 500 the batch
passport is created at once and the items are queued: the response is
202 Accepted:
{
"dpp": { "id": "…", "passport_id": "…" },
"job": { "id": "c1d2…", "total_items": 20000, "completed_items": 0, "status": "pending" }
}
Poll the job:
GET /api/v1/dpps/jobs/{job_id}
It returns the job with status, total_items, completed_items,
progress_pct and last_error. Stop when status is completed,
failed or cancelled.
Create many
POST /api/v1/dpps-batch
Authorization: Bearer dpp_live_…
{
"stop_on_error": false,
"items": [
{ "gtin": "09501101530003", "category": "battery", "granularity": "model", "dpp_data": { "bat.category": "LMT" } },
{ "gtin": "09501101530010", "category": "battery", "granularity": "model", "dpp_data": { "bat.category": "LMT" } }
]
}
Each item takes the same fields as a single create. Up to 1 000 items
per request (more gives 413). The response is 201 when every item
was created and 207 when some failed:
{
"tenant_id": "8d3a4f91-…",
"total": 2,
"created": 1,
"failed": 1,
"stopped_early": false,
"results": [
{ "index": 0, "status": "created", "dpp": { "…": "…" } },
{ "index": 1, "status": "failed", "error": "Validation failed", "details": ["gtin: must be 8-14 digits"] }
]
}
With "stop_on_error": true the batch stops at the first failure.
Read one DPP
GET /api/v1/dpps/{id}
Authorization: Bearer dpp_live_…
Accept: application/json
{id} is the UUID or the passport's address (URL-encoded). The
response depends on Accept:
application/json—{ "@id", "dppId", "productIdentification", "dppMetadata", "productData" }application/ld+json— the same with@contextand@type- anything else, including no
Acceptheader — a plain HTML page
So send Accept: application/json from code. Drafts are readable here
by their own tenant. A withdrawn or archived passport answers 410.
Update a DPP
PATCH /api/v1/dpps/{id}
Authorization: Bearer dpp_live_…
Content-Type: application/json
{
"dpp_data": {
"espr.pid.product_name": "Kompas Portable Speaker II",
"espr.mfr.name": "Example Audio A/S",
"espr.eo.country": "DK"
}
}
{id} must be the UUID. dpp_data replaces the stored map: send
every key you want to keep. To change a few keys, read the passport,
change them, and send the whole map back.
Status changes are a PATCH too:
{ "dpp_status": "active" }
Values: draft, active, replaced, withdrawn, archived,
recalled. Publishing a draft this way counts against the plan's
number of active passports (402 when it is reached). The category
cannot change once the passport is published (409). A withdrawn
or archived passport keeps its address, which then answers 410 with
an explanation. A recalled passport still resolves, with the recall
shown and a gs1:recallStatus link.
The response has the same shape as a create.
The fields a PATCH may change are dpp_data, dpp_status, category,
granularity, granularity_policy, link_to_previous_dpp,
previous_dpp, identifier_scheme and template_id; any other key is
refused with 400. Send "previous_dpp": null to remove a
predecessor.
Parent and predecessor
A passport can point at two other passports, for two different reasons.
- Parent (
link_to_previous_dpp). The passport inherits from it. An item passport carries its serial number and reads the rest from its batch; a batch reads from its model or product. Changing the parent changes what the passport shows. The field kept its old name so that existing integrations keep working; it has always meant the parent. - Predecessor (
previous_dpp). The passport this one replaces: a new identifier after a change of granularity (EN 18219 §4.4.2(3)), or a repurposed or remanufactured battery (Regulation (EU) 2023/1542, art. 77(7)). It is a link only; nothing is inherited. It may be another provider's passport, so it is stored as an address. A passport cannot name itself.
The successor is not stored. A passport is "replaced by" whichever
published passport names it as its previous_dpp.
When you read a passport, dppMetadata carries all of them:
{
"linkToPreviousDPP": "https://example.dpp.dppagent.com/01/09501101530003",
"parentDPP": "https://example.dpp.dppagent.com/01/09501101530003",
"previousDPP": "https://other-provider.example/dpp/8f2c",
"nextDPP": null
}
linkToPreviousDPP and parentDPP are the parent (the first is the
old name, kept as it was). previousDPP and nextDPP are the
predecessor and the successor. In JSON-LD, previousDPP and nextDPP
are the CIRPASS-2 properties eudpp:hasPreviousDPP and
eudpp:hasNextDPP; the parent is not mapped to either.
Delete a draft
DELETE /api/v1/dpps/{id}
Authorization: Bearer dpp_live_…
Only drafts can be deleted; anything else answers 409. To take a
published passport off the market, set dpp_status to withdrawn or
archived, which keeps its history.
Public lookups
These need no key. On a brand's own host they only see that brand's
passports; on dppagent.com they see every published passport.
GET /api/v1/dppsByProductId/{gtin}
The most recently updated active passport for a GTIN, as JSON (default) or JSON-LD; a browser asking for HTML is redirected to the passport. 60 requests a minute per address.
POST /api/v1/dppsByProductIds
Content-Type: application/json
{ "productIds": ["09501101530003", "09501101530010"] }
Every active passport per GTIN, up to 1 000 GTINs per request:
{ "mappings": [ { "productId", "dppIds": [ … ], "passportIds": [ … ] } ], "count" }.
30 requests a minute per address.
GET /api/v1/dpps/{id}/versions?limit=50&offset=0
The version history of a published passport, newest first (limit up
to 500). Drafts answer 404. 60 requests a minute per address.
In the dashboard only
Per-passport scan analytics, AI Suggest and re-syncing a passport from its integration are dashboard features. Their endpoints need a signed-in session and are not part of the API an integration can call.