First EU DPP deadline (Batteries): 130 days Get compliant now
Language
EN DE FR IT ES NL PL BG CS DA EL ET FI HR HU LT LV MT PT RO SK SL SV
Product
Features After-sales How It Works Demo Enterprise Pricing
Sectors
All Sectors Textiles Electronics Batteries
Resources
Blog Compare DPP Software Country Requirements Readiness Assessment Green claims checker Free Downloads Glossary
Company
Partners Contact
Sign in Get Started Free
Developers / REST API

API Reference

Everything in MyProductPassport is available over a key-authenticated REST API: create products, generate and publish Digital Product Passports, push live battery data, and check compliance, straight from your ERP, PIM or build pipeline.

v1 https://myproductpassport.eu/api/v1 Get started free OpenAPI spec ↓

01 / Getting startedAuthentication

Every request is authenticated with an API key passed as a Bearer token. Keys are created in the app under API Keys, are shown once, and are stored hashed; each key carries its own permission set, an optional IP allowlist, and an optional expiry date. Keys can be rotated without downtime.

Request header
Authorization: Bearer pk_your_api_key
On every plan. API keys can be created and used on every plan, Free included. What a plan changes is how many requests a key may make each minute (below) and how many products and passports the workspace may hold.
Test mode. A test key (pk_test_...) runs every call exactly as a live key would, with the same validation, ids and responses, and then undoes it: nothing is saved, no email or webhook is sent and no file is kept. Responses carry "test_mode": true and an X-MPP-Mode: test header. Build and test an integration against your real data, then swap in a live key.

Permissions are granted per key, so an integration only gets what it needs:

PermissionGrants
products:readRead products
products:writeCreate, update, and delete products
passports:readRead passports
passports:writeGenerate passports
passports:publishPublish and archive passports
webhooks:manageCreate, test and remove webhook endpoints
items:readRead individual items and their history
items:writeAdd, update and flag individual items
aftersales:readRead owner registrations and service requests
aftersales:writeUpdate service requests and reply to customers

02 / Getting startedRate limits

Limits are per key, per minute, and set by plan:

PlanRequests / minute
Free30
Micro60
E-Label60
Starter120
Professional300
Business600
Battery600

Every response carries X-RateLimit-Limit and X-RateLimit-Remaining headers. Exceeding the limit returns 429:

429 Too Many Requests
{ "error": "Rate limit exceeded.", "limit": 300, "retry_after": 60 }

03 / Getting startedErrors

Errors are JSON with a conventional status code. The common ones:

StatusMeaningExample body
401Missing, invalid, revoked or expired key{"error": "Invalid or revoked API key."}
403Key lacks a permission, IP not on the allowlist, or plan allowance reached{"message": "API key does not have 'products:write' permission."}
404The resource does not exist in your workspace{"message": "Not found."}
422Validation failed{"message": "…", "errors": {"gtin": ["…"]}}
429Rate limit exceeded{"error": "Rate limit exceeded."}

04 / Getting startedPagination

List endpoints return 25 items per page with standard links and meta objects. Pass ?page=2 to move through the collection.

05 / Getting startedAI assistant connector (MCP)

Connect Claude, Cursor or any assistant that speaks the Model Context Protocol, and ask it in plain words to set up products, fill in passports from what you tell it, and check what is still missing. It uses an API key with that key's permissions and rate limit. It can create drafts and fill in fields; it never publishes, archives or deletes anything: those stay with you in the app.

Server address (Streamable HTTP)
https://myproductpassport.eu/api/mcp
Claude Code
claude mcp add --transport http myproductpassport https://myproductpassport.eu/api/mcp \
  --header "Authorization: Bearer pk_live_your_key"
Cursor (mcp.json)
{
  "mcpServers": {
    "myproductpassport": {
      "url": "https://myproductpassport.eu/api/mcp",
      "headers": { "Authorization": "Bearer pk_live_your_key" }
    }
  }
}
Claude Desktop and other clients that start a local server
{
  "mcpServers": {
    "myproductpassport": {
      "command": "npx",
      "args": ["mcp-remote", "https://myproductpassport.eu/api/mcp", "--header", "Authorization: Bearer pk_live_your_key"]
    }
  }
}

The tools it offers:

ToolWhat it does
list_productsThe workspace's products, newest first, 25 a page, with their sector and latest passport.
get_productOne product with its details, sector, regulatory sections and latest passport.
list_sectorsThe sector templates a product can use (textiles, electronics, batteries, wine and more), with their ids. A product's sector decides which passport fields are required.
create_productAdds a product to the workspace. Use a sector_id from list_sectors. compliance_sections adds regulatory sections to its passport: gpsr, epr, france, chemicals, eudr.
list_passportsThe workspace's passports, newest first, 25 a page, with status (draft, published, archived), compliance percentage and public address.
get_passportOne passport: status, version, compliance percentage, public address and QR code links.
create_passportCreates a draft passport for a product. It is not published: a person publishes it in the app.
get_passport_fieldsWhat a passport asks for: required and optional field keys, what each means (label, description, example), the values it has, and which required ones are missing.
update_passport_fieldsWrites field values on a draft or published passport (a published one is updated in place: same address, same QR code). Keys from get_passport_fields; an empty string removes a value. Only write facts the user or their documents state.
get_complianceA passport's completeness: percentage, status and the required fields still missing.
Use a test key to try it. With a test key (pk_test_...) the assistant's changes are checked and then undone, so you can see what it would do before giving it a live key.

06 / EndpointsAccount

GET /me

Returns the authenticated account, its subscription tier, and how much of the product allowance is used. Useful as a connectivity check.

Request
curl https://myproductpassport.eu/api/v1/me \
  -H "Authorization: Bearer pk_your_key"
Response
{
  "data": {
    "id": 42,
    "name": "Jane Doe",
    "email": "[email protected]",
    "company_name": "Acme Ltd",
    "subscription_tier": "professional",
    "product_count": 128,
    "product_limit": 500,
    "passport_count": 96,
    "organization": {"id": 7, "name": "Acme Ltd"},
    "api_key": {"name": "ERP sync", "permissions": ["*"], "rate_limit_per_minute": 1000, "expires_at": null},
    "webhooks_available": true
  }
}

07 / EndpointsProducts

GET /products requires products:read

Lists the products in your workspace, newest first, 25 per page. Each product includes its sector and latest passport summary.

ParameterTypeDescription
pageintegerPage number.
Request
curl "https://myproductpassport.eu/api/v1/products?page=1" \
  -H "Authorization: Bearer pk_your_key"
Response
{
  "data": [
    {
      "id": 512,
      "name": "Alpine Merino Crew Sweater",
      "sku": "AM-CREW-01",
      "gtin": "05012345678900",
      "brand": "Northloom",
      "category": "textiles",
      "status": "active",
      "country_of_origin": "PT",
      "weight": 0.4,
      "weight_unit": "kg",
      "materials": [
        { "name": "Merino wool (RWS)", "percentage": 78 },
        { "name": "Recycled nylon (GRS)", "percentage": 20 },
        { "name": "Elastane", "percentage": 2 }
      ],
      "sector": { "id": 3, "name": "Textiles" },
      "latest_passport": {
        "id": 96, "version": 1,
        "status": "published", "compliance_percentage": 100
      },
      "created_at": "2026-08-14T09:21:00+00:00",
      "updated_at": "2026-09-01T16:02:11+00:00"
    }
  ],
  "links": { "next": null },
  "meta": { "current_page": 1, "per_page": 25, "total": 1 }
}
POST /products requires products:write

Creates one product. Fails with 403 if the plan's product allowance is used up.

ParameterTypeDescription
namestringrequiredProduct name, up to 255 characters.
skustringYour internal SKU, up to 100 characters.
gtinstringGTIN-8, 12, 13 or 14 (digits only).
brandstringBrand shown on the public passport.
descriptionstringUp to 2,000 characters.
categorystringFree-text category. Defaults to "general".
sector_idintegerA sector id, which selects the passport field template.
country_of_originstringCountry code, e.g. "PT".
weightnumberNet weight.
weight_unitstringOne of kg, g, lb, oz. Defaults to kg.
materialsarrayObjects of {name, percentage 0-100}.
has_packagingbooleanAdds the packaging template's fields (keys starting pkg_) to the passport, under the same QR code.
Request
curl -X POST https://myproductpassport.eu/api/v1/products \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Alpine Merino Crew Sweater",
    "sku": "AM-CREW-01",
    "gtin": "05012345678900",
    "sector_id": 3,
    "materials": [
      {"name": "Merino wool (RWS)", "percentage": 78},
      {"name": "Recycled nylon (GRS)", "percentage": 22}
    ]
  }'
Response
{
  "data": {
    "id": 513,
    "name": "Alpine Merino Crew Sweater",
    "sku": "AM-CREW-01",
    "status": "active",
    "sector": { "id": 3, "name": "Textiles" },
    "created_at": "2026-09-07T18:40:00+00:00"
  }
}
POST /products/bulk requires products:write

Creates up to 100 products in one call. The whole batch is refused with 403 if it would exceed your remaining product allowance. For catalogues in the thousands, loop this endpoint or use CSV import in the app.

ParameterTypeDescription
productsarrayrequired1-100 product objects using the same fields as Create a product (materials excluded).
Request
curl -X POST https://myproductpassport.eu/api/v1/products/bulk \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "products": [
      {"name": "PSL-BT-122000", "sku": "PSL-BT-122000", "sector_id": 5},
      {"name": "PSL-BT-123000", "sku": "PSL-BT-123000", "sector_id": 5}
    ]
  }'
Response
{
  "message": "2 product(s) created.",
  "products": [
    { "id": 514, "name": "PSL-BT-122000", "status": "active" },
    { "id": 515, "name": "PSL-BT-123000", "status": "active" }
  ]
}
GET /products/{id} requires products:read

Returns one product with its sector and latest passport summary. 404 if the id is not in your workspace.

Request
curl https://myproductpassport.eu/api/v1/products/512 \
  -H "Authorization: Bearer pk_your_key"
PUT /products/{id} requires products:write

Partially updates a product. Send only the fields you want to change; the field list matches Create a product.

Request
curl -X PUT https://myproductpassport.eu/api/v1/products/512 \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"brand": "Northloom"}'
DELETE /products/{id} requires products:write

Soft-deletes a product. Published passports for the product keep working, so printed QR codes are never orphaned.

Request
curl -X DELETE https://myproductpassport.eu/api/v1/products/512 \
  -H "Authorization: Bearer pk_your_key"
Response
{ "message": "Product deleted." }

08 / EndpointsPassports

GET /passports requires passports:read

Lists passports, newest first, 25 per page, with product summary and all passport fields.

ParameterTypeDescription
pageintegerPage number.
Request
curl https://myproductpassport.eu/api/v1/passports \
  -H "Authorization: Bearer pk_your_key"
Response
{
  "data": [
    {
      "id": 96,
      "uuid": "36ff3776-d517-4228-8cde-9208136ea84a",
      "version": 1,
      "status": "published",
      "compliance_status": "compliant",
      "compliance_percentage": 100,
      "public_url": "https://myproductpassport.co.uk/p/36ff3776-…",
      "gs1_url": "https://myproductpassport.co.uk/01/05012345678900",
      "product": { "id": 512, "name": "Alpine Merino Crew Sweater", "sku": "AM-CREW-01" },
      "fields": [
        { "key": "material_composition", "value": "…", "verification_status": "verified" }
      ],
      "published_at": "2026-08-16T10:00:00+00:00"
    }
  ],
  "meta": { "current_page": 1, "per_page": 25, "total": 1 }
}
POST /products/{productId}/passports requires passports:write

Generates a draft passport for a product, pre-filling fields from the product data and the sector template. Returns the draft with its compliance score. A product that already has a live passport returns 409 with that passport: correct it with PUT /passports/{id}/fields, which keeps its address and QR code.

Request
curl -X POST https://myproductpassport.eu/api/v1/products/512/passports \
  -H "Authorization: Bearer pk_your_key"
GET /passports/{id} requires passports:read

Returns one passport with product, fields, public URL and GS1 Digital Link.

Request
curl https://myproductpassport.eu/api/v1/passports/96 \
  -H "Authorization: Bearer pk_your_key"
POST /passports/{id}/publish requires passports:publish

Publishes a draft passport, making its public page and QR code live. Only drafts can be published; anything else returns 404.

Request
curl -X POST https://myproductpassport.eu/api/v1/passports/96/publish \
  -H "Authorization: Bearer pk_your_key"
POST /passports/{id}/archive requires passports:publish

Archives a passport and takes its public page out of circulation.

Request
curl -X POST https://myproductpassport.eu/api/v1/passports/96/archive \
  -H "Authorization: Bearer pk_your_key"
POST /passports/bulk-publish requires passports:publish

Publishes up to 50 draft passports in one call and reports per-passport results, so one failure never aborts the batch.

ParameterTypeDescription
passport_idsarrayrequired1-50 passport ids.
Request
curl -X POST https://myproductpassport.eu/api/v1/passports/bulk-publish \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"passport_ids": [96, 97, 98]}'
Response
{
  "published": 2,
  "failed": 1,
  "published_ids": [96, 97],
  "errors": [
    { "id": 98, "error": "Compliance below the publishing threshold." }
  ]
}
POST /passports/{id}/successor requires passports:write

For a product that has been prepared for re-use, repurposed or remanufactured, the Battery Regulation (Article 77(7)) requires a new passport linked to the original. This creates a new product record and a draft passport prefilled from the published one, linked back to it. The original stays live and both passports show the link. Publish the new one with POST /passports/{id}/publish.

ParameterTypeDescription
stagestringrequiredprepared_for_reuse, repurposed or remanufactured.
Request
curl -X POST https://myproductpassport.eu/api/v1/passports/96/successor \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "stage": "repurposed" }'
Response
{
  "data": {
    "id": 131, "status": "draft", "version": 1,
    "lineage": {
      "stage": "repurposed",
      "original": { "id": 96, "url": "https://myproductpassport.co.uk/p/..." },
      "originals": [{ "passport_id": 96, "product_id": 40, "stage": "repurposed", "status": "published", "url": "https://myproductpassport.co.uk/p/..." }],
      "successors": []
    }
  }
}
GET /passports/{id}/fields requires passports:read

Returns the field keys the passport's sector template asks for (required and optional), the values currently held, and which required keys are still missing. Use it to build the payload for PUT /passports/{id}/fields.

Request
curl https://myproductpassport.eu/api/v1/passports/96/fields \
  -H "Authorization: Bearer pk_your_key"
Response
{
  "passport_id": 96,
  "sector": "batteries",
  "compliance_percentage": 84,
  "required": ["battery_category", "carbon_footprint", "..."],
  "optional": ["energy_round_trip_efficiency", "..."],
  "missing_required": ["carbon_footprint_class", "recycled_content"],
  "values": { "battery_category": "Industrial", "...": "..." }
}
PUT /passports/{id}/fields requires passports:write

Writes any set of field values onto a draft or live passport: the full Annex XIII set for a battery, a sector template's fields, or your own keys. Send only the keys you want to change; a null or empty value removes that field. A live passport is updated in place, its address and QR code do not change, and its machine translations are regenerated. Compliance is re-scored. A draft is not published by this call; use POST /passports/{id}/publish.

ParameterTypeDescription
fieldsobjectrequiredMap of field key to value. Keys are lowercase letters, digits and underscores; values up to 5,000 characters. Up to 200 keys per call.
Request
curl -X PUT https://myproductpassport.eu/api/v1/passports/96/fields \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": {
      "carbon_footprint": "61.2 kg CO2e/kWh",
      "carbon_footprint_class": "C",
      "recycled_content": "Cobalt 16%, lithium 6%, nickel 6%, lead 0%"
    }
  }'
Response
{
  "message": "3 field(s) changed, 0 removed.",
  "changed": ["carbon_footprint", "carbon_footprint_class", "recycled_content"],
  "removed": [],
  "passport": { "id": 96, "status": "published", "compliance_percentage": 92 }
}
POST /passports/{id}/state-of-health requires passports:write

Pushes this battery's own data (Annex XIII point 4 of Regulation (EU) 2023/1542) onto a published passport, for BMS and fleet systems. Send any subset of the fields below; compliance is re-checked after the update. These values are shown only to viewers the passport owner has approved (Article 77(2)), never on the public page. capacity_fade, power_capability and internal_resistance are stored as this battery's current readings (current_*), separate from the model's original values.

ParameterTypeDescription
state_of_healthstringe.g. "97% at 412 cycles".
state_of_certified_energystringCertified energy remaining (EV batteries).
battery_statusstringoriginal, repurposed, re-used, remanufactured or waste.
capacity_fadestringCapacity fade to date. Stored as current_capacity_fade.
power_capabilitystringCurrent power capability. Stored as current_power_capability.
internal_resistancestringMeasured internal resistance. Stored as current_internal_resistance.
use_datastringCycles, negative events, temperature and state of charge, up to 5,000 characters.
Request
curl -X POST https://myproductpassport.eu/api/v1/passports/96/state-of-health \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "state_of_health": "97% at 412 cycles",
    "capacity_fade": "3% from initial rated capacity"
  }'
Response
{
  "message": "2 field(s) updated.",
  "updated_fields": ["state_of_health", "current_capacity_fade"],
  "passport": { "id": 96, "status": "published", "compliance_percentage": 100 }
}
POST /passports/{id}/units requires passports:write

Each battery needs its own passport (Regulation (EU) 2023/1542, Article 77(1)). Register up to 10,000 batteries of a battery model in one call, each with its own serial (up to 20 letters, digits, dots, hyphens or underscores), and optionally manufacturing_date, battery_status and its Annex XIII point 4 data. Existing serials are updated; values you leave out are kept. With a GTIN on the product, each battery's address is its GS1 Digital Link, /01/{gtin}/21/{serial}. Point 4 data is shown only to viewers the owner approves.

ParameterTypeDescription
units[].serialstringrequiredThe battery's serial number.
units[].manufacturing_datestringMonth and year, e.g. "2026-09".
units[].battery_statusstringoriginal or waste. A change of status needs the values measured at it (Annex XIII 4(a)); waste also needs responsible_party_type (producer, pro or waste_operator), responsible_party_name and responsible_party_contact (Article 77(7)). recycled, with recycled_at and recycler_name, ends the passport (Article 77(8)). A repurposed, re-used or remanufactured battery gets a new passport: POST /passports/{id}/units/{serial}/successor.
units[].state_of_healthstringAlso state_of_certified_energy, current_capacity_fade, current_power_capability, current_internal_resistance, remaining_capacity_ah, power_fade, internal_resistance_increase, round_trip_efficiency, round_trip_efficiency_fade, expected_lifetime_cycles_remaining, self_discharge_rate, ohmic_resistance and use_data.
Request
curl -X POST https://myproductpassport.eu/api/v1/passports/96/units \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"units": [{"serial": "KES-00001", "manufacturing_date": "2026-09", "state_of_health": "100%"}]}'
Response
{ "created": 1, "updated": 0, "skipped": 0, "errors": [] }
PATCH /passports/{id}/units/{serial} requires passports:write

For a battery management system pushing one battery's data: any of battery_status, state_of_health, state_of_certified_energy, current_capacity_fade, current_power_capability, current_internal_resistance and use_data. GET the same path to read it, or GET /passports/{id}/units for the list (per_page up to 500).

Request
curl -X PATCH https://myproductpassport.eu/api/v1/passports/96/units/KES-00001 \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"state_of_health": "96% at 412 cycles"}'
Response
{
  "data": { "serial": "KES-00001", "public_url": "https://myproductpassport.co.uk/01/05012345678900/21/KES-00001", "battery_status": "original", "data": { "state_of_health": "96% at 412 cycles" } }
}
GET /passports/{id}/units/{serial}/snapshots requires passports:read

Annex XIII 4(a) asks for the performance and durability values when the battery is placed on the market and when its status changes. Those readings are kept as they were, whatever is pushed later; send record_snapshot: true with a PATCH to keep a periodic reading as well.

Request
curl https://myproductpassport.eu/api/v1/passports/96/units/KES-00001/snapshots \
  -H "Authorization: Bearer pk_your_key"
Response
{
  "data": [
    { "event": "placed_on_market", "status": "original", "values": { "state_of_health": "100%" }, "measured_at": "2026-09-26T09:00:00+00:00", "source": "api" },
    { "event": "status_change", "status": "waste", "values": { "remaining_capacity_ah": "71 Ah", "power_fade": "14%", "internal_resistance_increase": "22%", "expected_lifetime_cycles_remaining": "0" }, "measured_at": "2029-03-02T10:00:00+00:00", "source": "bms" }
  ]
}
POST /passports/{id}/units/{serial}/successor requires passports:write

Article 77(7): a battery that has been prepared for re-use, repurposed or remanufactured has a new passport linked to its original. This registers the battery again, with a new serial and identifier, under a second-life battery model of your workspace (made from this model if there is none yet), links the two and keeps the values measured at the change. The original battery keeps its passport. Complete and publish the second-life model with POST /passports/{id}/publish.

ParameterTypeDescription
stagestringrequiredprepared_for_reuse, repurposed or remanufactured.
valuesobjectrequiredThe values measured at the change: at least one of remaining_capacity_ah or current_capacity_fade, of current_power_capability or power_fade, of current_internal_resistance or internal_resistance_increase, and expected_lifetime_cycles_remaining (Annex IV Part A).
serialstringThe new serial. Default: the original serial plus -RU, -RP or -RM.
measured_atstringWhen the values were measured.
member_statestringTwo-letter code of the Member State where the state of health was tested (Article 73(1)(a)).
target_passport_idintegerAn existing second-life model of this battery model to register it under.
Request
curl -X POST https://myproductpassport.eu/api/v1/passports/96/units/KES-00001/successor \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"stage": "repurposed", "values": {"remaining_capacity_ah": "82 Ah", "power_fade": "9%", "internal_resistance_increase": "15%", "expected_lifetime_cycles_remaining": "2000"}}'
Response
{
  "data": {
    "serial": "KES-00001-RP", "battery_status": "repurposed", "passport_id": 131,
    "lineage": { "originals": [{ "serial": "KES-00001", "passport_id": 96, "stage": "repurposed" }], "successors": [] }
  }
}
POST /passports/{id}/registry requires passports:write

Stores the EU DPP Registry submission and result on a passport: status, correlation ID, the registration identifier the Registry returns, and an optional link to a back-up copy hosted by a DPP service provider. Registration itself is performed in the Commission Registry (web form or file upload); this keeps the outcome with the passport. GET /passports/{id} returns the same block under registry.

ParameterTypeDescription
statusstringnot_submitted, submitted, registered or failed.
product_groupstringRegistry product group, for example batteries.
granularitystringmodel, batch or item.
upistringUnique product identifier as an https URL. Defaults to the public passport URL.
correlation_idstringCorrelation ID assigned by the Registry on submission.
uristringRegistration identifier returned by the Registry.
backup_urlstringhttps link to the back-up copy hosted by a DPP service provider.
submitted_atdateSubmission date.
registered_atdateRegistration date.
errorstringError description for a failed submission.
Request
curl -X POST https://myproductpassport.eu/api/v1/passports/96/registry \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "registered",
    "correlation_id": "7f3a2c1e-0b4d-4f8e-9a6b-2d1c3e4f5a6b",
    "uri": "urn:eu:dpp:reg:00000001",
    "registered_at": "2027-02-18"
  }'
Response
{
  "message": "Registry details updated.",
  "passport": {
    "id": 96,
    "registry": {
      "status": "registered",
      "upi": "https://myproductpassport.co.uk/p/552fbb4e-5d9a-41da-9050-cc9005d8b66b",
      "correlation_id": "7f3a2c1e-0b4d-4f8e-9a6b-2d1c3e4f5a6b",
      "uri": "urn:eu:dpp:reg:00000001",
      "registered_at": "2027-02-18T00:00:00+00:00"
    }
  }
}
GET /passports/{id}/qr requires passports:read

The passport's QR code as a file: svg (default), png (branded, print-ready) or pdf (label sheet).

ParameterTypeDescription
formatstringsvg | png | pdf
Request
curl "https://myproductpassport.eu/api/v1/passports/96/qr?format=png" \
  -H "Authorization: Bearer pk_your_key" -o passport-96.png

09 / EndpointsCompliance

GET /passports/{passportId}/compliance requires passports:read

Runs a live compliance check for a passport against its sector's required fields and reports exactly what is missing.

Request
curl https://myproductpassport.eu/api/v1/passports/96/compliance \
  -H "Authorization: Bearer pk_your_key"
Response
{
  "data": {
    "passport_id": 96,
    "product_name": "Alpine Merino Crew Sweater",
    "status": "compliant",
    "percentage": 100,
    "completed_fields": 12,
    "total_fields": 12,
    "missing_fields": []
  }
}

10 / EndpointsSectors

GET /sectors

The sectors a product can be filed under: the platform's system sectors plus any your workspace defined. Use the id when creating products, and the field keys when writing passport fields.

Request
curl https://myproductpassport.eu/api/v1/sectors \
  -H "Authorization: Bearer pk_your_key"
Response
{
  "data": [
    {
      "id": 4,
      "slug": "batteries",
      "name": "Batteries",
      "system": true,
      "regulations_finalised": true,
      "deadline": "2027-02-18",
      "deadline_label": "18 Feb 2027",
      "required_fields": ["battery_category", "battery_weight", "..."],
      "optional_fields": ["energy_round_trip_efficiency", "..."]
    }
  ]
}

11 / EndpointsDocuments

GET /products/{productId}/documents requires products:read

Declarations of conformity, test reports and certificates attached to the product. A public document is linked from every published passport of the product.

Request
curl https://myproductpassport.eu/api/v1/products/512/documents \
  -H "Authorization: Bearer pk_your_key"
Response
{
  "data": [
    {
      "id": 31,
      "product_id": 512,
      "filename": "declaration-of-conformity.pdf",
      "mime_type": "application/pdf",
      "file_size": 184322,
      "is_public": true,
      "processing_status": "completed",
      "public_url": "https://myproductpassport.co.uk/p/3f0c.../document/31",
      "created_at": "2026-09-22T10:12:00+00:00"
    }
  ]
}
POST /products/{productId}/documents requires products:write

Multipart upload. PDF, Word, Excel, CSV, text, PNG and JPEG up to 10 MB, at most 20 per product. Text is extracted in the background.

ParameterTypeDescription
filefilerequiredThe document.
is_publicbooleanShow it on the public passport (default false).
Request
curl -X POST https://myproductpassport.eu/api/v1/products/512/documents \
  -H "Authorization: Bearer pk_your_key" \
  -F "[email protected]" \
  -F "is_public=1"
PATCH /products/{productId}/documents/{id} requires products:write

Toggles whether the document is linked from the public passport.

ParameterTypeDescription
is_publicbooleanrequired
Request
curl -X PATCH https://myproductpassport.eu/api/v1/products/512/documents/31 \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"is_public": false}'
DELETE /products/{productId}/documents/{id} requires products:write

Removes the file and its passport link.

Request
curl -X DELETE https://myproductpassport.eu/api/v1/products/512/documents/31 \
  -H "Authorization: Bearer pk_your_key"
Response
{ "message": "Document deleted." }

12 / EndpointsSupply chain

GET /products/{productId}/supply-chain requires products:read

The steps shown on the product's passport, in order.

Request
curl https://myproductpassport.eu/api/v1/products/512/supply-chain \
  -H "Authorization: Bearer pk_your_key"
PUT /products/{productId}/supply-chain requires products:write

Replaces every step with the list you send, in the order sent. Send an empty list to clear it. Step types: raw_material, processing, manufacturing, assembly, packaging, distribution.

ParameterTypeDescription
stepsarrayrequiredUp to 50 steps.
steps[].step_typestringrequired
steps[].supplier_namestringrequired
steps[].supplier_locationstring
steps[].supplier_countrystringISO 3166-1 alpha-2.
steps[].lat / lngnumber
steps[].descriptionstring
steps[].certificationsarray
steps[].datedate
Request
curl -X PUT https://myproductpassport.eu/api/v1/products/512/supply-chain \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"steps": [{"step_type": "raw_material", "supplier_name": "Merino Growers Co-op", "supplier_country": "NZ", "certifications": ["RWS"]}, {"step_type": "manufacturing", "supplier_name": "Northloom Mill", "supplier_location": "Biella", "supplier_country": "IT"}]}'

13 / EndpointsTranslations

GET /passports/{id}/translations requires passports:read

Every translated field value by locale, with its source: auto (machine, made on publish) or manual.

Request
curl https://myproductpassport.eu/api/v1/passports/96/translations \
  -H "Authorization: Bearer pk_your_key"
Response
{
  "data": {
    "passport_id": 96,
    "available_locales": ["bg", "cs", "da", "de", "..."],
    "translations": {
      "de": {"care_instructions": {"value": "Kalt waschen", "source": "manual"}}
    }
  }
}
PUT /passports/{id}/translations/{locale} requires passports:write

Sets translations for one locale, keyed by field key. A manual translation is never overwritten by the machine. Send an empty string to remove one.

ParameterTypeDescription
fieldsobjectrequiredfield_key: translated value
Request
curl -X PUT https://myproductpassport.eu/api/v1/passports/96/translations/de \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"fields": {"care_instructions": "Kalt waschen, nicht bleichen"}}'
Response
{ "message": "1 translation(s) written, 0 removed.", "locale": "de", "written": 1, "removed": 0 }

14 / EndpointsIntegrity and exports

GET /passports/{id}/integrity requires passports:read

Every published state of the passport as a hash-chained, Ed25519-signed record: fingerprint (SHA-256), chain value and signature. The public keys are at /.well-known/mpp-integrity-keys.json, so anyone can verify the chain.

Request
curl https://myproductpassport.eu/api/v1/passports/96/integrity \
  -H "Authorization: Bearer pk_your_key"
Response
{ "data": { "state": "intact", "hash_algorithm": "SHA-256", "signature_algorithm": "Ed25519", "records": [ { "sequence": 1, "event": "published", "content_hash": "8f3a...", "chain_hash": "c21e...", "signature": "MEUC...", "key_id": "3fa9c21e0b7d4a11" } ], "problems": [] } }
GET /passports/{id}/integrity/{sequence} requires passports:read

The complete record as it was signed, snapshot included: what the passport said at that moment, down to a hash of every attached file.

Request
curl https://myproductpassport.eu/api/v1/passports/96/integrity/1 \
  -H "Authorization: Bearer pk_your_key"
GET /passports/{id}/epcis requires passports:read

The passport as a GS1 EPCIS 2.0 JSON-LD document: the item commissioned, each supply-chain step as an observation, individual batteries, and a second-life passport as a transformation. Identifiers are GS1 Digital Link URIs when the product has a GTIN.

Request
curl https://myproductpassport.eu/api/v1/passports/96/epcis \
  -H "Authorization: Bearer pk_your_key"

15 / EndpointsSupplier requests

POST /passports/{id}/supplier-requests requires passports:write

Emails a supplier a private link to fill in the fields you choose, in their language. Nothing reaches the passport until someone in your workspace approves it.

ParameterTypeDescription
supplier_namestringrequired
supplier_emailstringrequired
field_keysarrayrequiredField keys from the passport's template.
localestringLanguage of the email and form, e.g. de.
due_atdateReply-by date; 14 days if omitted.
messagestring
Request
curl -X POST https://myproductpassport.eu/api/v1/passports/96/supplier-requests \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"supplier_name": "Mei Chen", "supplier_email": "[email protected]", "field_keys": ["recycled_content_cobalt", "carbon_footprint"], "locale": "de"}'
GET /passports/{id}/supplier-requests requires passports:read

Every request for the passport with its status.

Request
curl https://myproductpassport.eu/api/v1/passports/96/supplier-requests \
  -H "Authorization: Bearer pk_your_key"
GET /supplier-requests/{id} requires passports:read

One request with the supplier's answers, note and attached file names.

Request
curl https://myproductpassport.eu/api/v1/supplier-requests/12 \
  -H "Authorization: Bearer pk_your_key"

16 / EndpointsAfter-sales

POST /passports/{id}/items requires items:write

Give items of a product (any sector except batteries, which use /units) their own serial number: each gets its own address and QR code, a hidden authenticity code for its label, and can be registered by its owner. Send up to 10,000 items, or a range. With a GTIN the address is the GS1 Digital Link /01/{gtin}/21/{serial}.

ParameterTypeDescription
items[].serialstringrequiredUp to 20 letters, digits, dots, hyphens or underscores.
items[].batchstringBatch or lot, up to 20 characters.
items[].manufactured_onstringDate of manufacture, YYYY-MM-DD.
rangeobjectInstead of items: prefix, start, count, digits and batch, for example SN-00001 to SN-00500.
Request
curl -X POST https://myproductpassport.eu/api/v1/passports/96/items \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"range": {"prefix": "SN-", "start": 1, "count": 500, "digits": 5, "batch": "B2026-10"}}'
Response
{ "created": 500, "updated": 0, "skipped": 0, "over_limit": 0, "errors": [] }
GET /passports/{id}/items/{serial} requires items:read

An item with its address, status, whether it is registered and until when its warranty runs, its authenticity checks and its history. A key with items:write also gets its hidden code, for printing. GET /passports/{id}/items lists them (status and batch filters, per_page up to 500).

Request
curl https://myproductpassport.eu/api/v1/passports/96/items/SN-00001 \
  -H "Authorization: Bearer pk_your_key"
Response
{
  "data": { "serial": "SN-00001", "status": "active", "url": "https://myproductpassport.co.uk/01/05012345678900/21/SN-00001", "registered": true, "warranty_ends_on": "2028-10-02", "events": [ { "type": "registered", "country": "IE", "at": "2026-10-03T09:12:00+00:00" } ] }
}
PATCH /passports/{id}/items/{serial} requires items:write

Set status to stolen or recalled and its page warns whoever scans it, with your status_note; a stolen item cannot be registered. retired takes its page offline. batch and manufactured_on can be changed too.

ParameterTypeDescription
statusstringactive, stolen, recalled or retired.
status_notestringShown on the item's page, up to 500 characters.
Request
curl -X PATCH https://myproductpassport.eu/api/v1/passports/96/items/SN-00001 \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"status": "stolen", "status_note": "Reported stolen on 2 October 2026."}'
GET /owner-registrations requires aftersales:read

People who registered your products: name, email, country, purchase date, retailer, warranty dates, and whether they agreed to news and offers (marketing_consent=1 lists only those). Active registrations by default; status=pending, transferred or ended for others; since= for changes after a date.

Request
curl "https://myproductpassport.eu/api/v1/owner-registrations?marketing_consent=1&since=2026-10-01" \
  -H "Authorization: Bearer pk_your_key"
GET /service-requests requires aftersales:read

Repairs, warranty claims, take-backs, spare parts and questions sent from your passports, newest first, with status and type filters. GET /service-requests/{reference} adds the conversation.

Request
curl "https://myproductpassport.eu/api/v1/service-requests?status=new" \
  -H "Authorization: Bearer pk_your_key"
POST /service-requests/{reference}/replies requires aftersales:write

Your reply is emailed to the customer with a link to their request page, in their language. internal: true adds a note only your team sees. PATCH /service-requests/{reference} with a status moves it along (the customer is told when it is in progress, resolved or closed).

ParameterTypeDescription
bodystringrequiredThe reply, up to 5,000 characters.
internalbooleanA note for your team instead of a reply.
statusstringnew, in_progress, waiting, resolved or closed.
Request
curl -X POST https://myproductpassport.eu/api/v1/service-requests/SR-7K3F9Q/replies \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"body": "A new lid is on its way today.", "status": "waiting"}'

17 / EndpointsBulk edits and images

PATCH /products/bulk-fields requires products:write

Sets a product detail or a passport field on up to 5,000 products. mode is set, fill (only where empty) or clear. Up to 100 products are updated in the request; larger sets are queued (202).

ParameterTypeDescription
product_idsarrayrequired
targetstringrequiredproduct | field
keystringrequired
valuestring
modestringset | fill | clear
Request
curl -X PATCH https://myproductpassport.eu/api/v1/products/bulk-fields \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"product_ids": [12, 13, 14], "target": "field", "key": "warranty_period", "value": "10 years", "mode": "fill"}'
Response
{ "data": { "changed": 2, "skipped": 1, "errors": [] } }
POST /products/{id}/images requires products:write

Multipart upload of a JPG, PNG or WebP (up to 15MB, 8 per product). The first image is the cover on the public passport.

ParameterTypeDescription
imagefilerequired
altstringDescription of the image.
Request
curl -X POST https://myproductpassport.eu/api/v1/products/12/images \
  -H "Authorization: Bearer pk_your_key" \
  -F [email protected] -F alt="Front of the battery"
PUT /products/{id}/images/order requires products:write

Pass image_ids in the order you want; the first becomes the cover.

ParameterTypeDescription
image_idsarrayrequired
Request
curl -X PUT https://myproductpassport.eu/api/v1/products/12/images/order \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"image_ids": [31, 30]}'
DELETE /products/{id}/images/{imageId} requires products:write

Removes the image and its file.

Request
curl -X DELETE https://myproductpassport.eu/api/v1/products/12/images/31 \
  -H "Authorization: Bearer pk_your_key"

18 / EndpointsWebhooks

GET /webhooks requires webhooks:manage

The endpoints registered for your workspace. Secrets are not included here; fetch one endpoint to see its secret.

Request
curl https://myproductpassport.eu/api/v1/webhooks \
  -H "Authorization: Bearer pk_your_key"
POST /webhooks requires webhooks:manage

Registers a public HTTPS URL for one or more events. The response includes the signing secret. Private or internal hosts are refused.

ParameterTypeDescription
urlstringrequired
eventsarrayrequiredAny of the events listed below.
Request
curl -X POST https://myproductpassport.eu/api/v1/webhooks \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://erp.acme.example/mpp", "events": ["passport.published", "product.updated"]}'
Response
{
  "data": {
    "id": 3,
    "url": "https://erp.acme.example/mpp",
    "events": ["passport.published", "product.updated"],
    "is_active": true,
    "failure_count": 0,
    "secret": "9f1c...64 hex characters...",
    "created_at": "2026-09-22T10:12:00+00:00"
  }
}
GET /webhooks/{id} requires webhooks:manage

Request
curl https://myproductpassport.eu/api/v1/webhooks/3 \
  -H "Authorization: Bearer pk_your_key"
PUT /webhooks/{id} requires webhooks:manage

Change the URL or events, or pause and resume it. Resuming resets the failure count; an endpoint is paused automatically after 10 consecutive failed deliveries.

ParameterTypeDescription
urlstring
eventsarray
is_activeboolean
Request
curl -X PUT https://myproductpassport.eu/api/v1/webhooks/3 \
  -H "Authorization: Bearer pk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"is_active": true}'
POST /webhooks/{id}/test requires webhooks:manage

Queues a webhook.test delivery so you can check your receiver and signature code.

Request
curl -X POST https://myproductpassport.eu/api/v1/webhooks/3/test \
  -H "Authorization: Bearer pk_your_key"
Response
{ "message": "Test delivery queued." }
GET /webhooks/{id}/deliveries requires webhooks:manage

Recent deliveries, newest first, with the HTTP status your endpoint returned and the first 500 characters of its body.

ParameterTypeDescription
per_pageintegerUp to 100.
Request
curl https://myproductpassport.eu/api/v1/webhooks/3/deliveries \
  -H "Authorization: Bearer pk_your_key"
DELETE /webhooks/{id} requires webhooks:manage

Request
curl -X DELETE https://myproductpassport.eu/api/v1/webhooks/3 \
  -H "Authorization: Bearer pk_your_key"
Response
{ "message": "Webhook endpoint deleted." }

19 / EventsWebhooks

Instead of polling, register a webhook endpoint in the app (Webhooks page, available on Starter plans and above) and MyProductPassport will POST JSON to your URL as things happen:

EventFires when
product.createdA product is created (app, API or CSV import)
product.updatedA product is updated
product.deletedA product is deleted
passport.createdA passport is generated
passport.publishedA passport goes live
passport.archivedA passport is archived
supplier_request.submittedA supplier sends the information you asked for
owner.registeredAn owner confirms their registration
owner.transferredAn item passes to a new registered owner
service_request.createdA customer sends a service request from a passport
service_request.updatedA service request gets a reply or a new status
item.status_changedAn item is flagged stolen, recalled or retired, or cleared
item.verification_failedA secure tag tap fails, wrong codes pile up, or one serial is seen in too many places
webhook.testYou send a test delivery from the app

Every delivery is a JSON envelope: a delivery id (the same id is re-sent on retries, so you can de-duplicate), the event, the attempt number, a timestamp and the event data. Passport events carry the passport uuid and public URL, so you rarely need a second request:

Delivery body
{
  "id": 8123,
  "event": "passport.published",
  "attempt": 1,
  "created_at": "2026-09-22T10:12:00+00:00",
  "data": {
    "passport_id": 96,
    "uuid": "3f0c8f1e-...",
    "version": 2,
    "status": "published",
    "public_url": "https://myproductpassport.co.uk/p/3f0c8f1e-...",
    "product_id": 512,
    "product": {"id": 512, "name": "Alpine Merino Crew Sweater", "sku": "NL-AMC-001", "gtin": "5012345678900"}
  }
}

The headers carry the event name, the delivery id, a Unix timestamp and an HMAC signature of the raw body, so you can verify it came from us and reject replays:

Delivery headers
Content-Type: application/json
User-Agent: MyProductPassport-Webhooks/1.0
X-MPP-Event: passport.published
X-MPP-Delivery: 8123
X-MPP-Timestamp: 1790084320
X-MPP-Signature: sha256=<hex hmac of the raw body, keyed with your endpoint secret>

Answer with any 2xx within 10 seconds. Anything else is retried five times over about 90 minutes (30 s, 1 min, 5 min, 15 min, 1 h); after 10 consecutive failures the endpoint is paused until you re-enable it.

Verify (PHP)
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $endpointSecret);
abort_unless(hash_equals($expected, $request->header('X-MPP-Signature')), 401);