DPP Agent docs

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 — prefix plus a number from start (default 1), zero-padded to padding digits (default 5).
  • sgtin — numeric serials for an SGTIN; needs companyPrefix (6 to 12 digits) and itemRef (digits).
  • custom_pattern — a pattern with {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 @context and @type
  • anything else, including no Accept header — 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.