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.
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.
Authorization: Bearer pk_your_api_keyPermissions are granted per key, so an integration only gets what it needs:
| Permission | Grants |
|---|---|
| products:read | Read products |
| products:write | Create, update, and delete products |
| passports:read | Read passports |
| passports:write | Generate passports |
| passports:publish | Publish and archive passports |
| webhooks:manage | Create, test and remove webhook endpoints |
| items:read | Read individual items and their history |
| items:write | Add, update and flag individual items |
| aftersales:read | Read owner registrations and service requests |
| aftersales:write | Update service requests and reply to customers |
02 / Getting startedRate limits
Limits are per key, per minute, and set by plan:
| Plan | Requests / minute |
|---|---|
| Free | 30 |
| Micro | 60 |
| E-Label | 60 |
| Starter | 120 |
| Professional | 300 |
| Business | 600 |
| Battery | 600 |
Every response carries X-RateLimit-Limit and X-RateLimit-Remaining headers. Exceeding the limit returns 429:
{ "error": "Rate limit exceeded.", "limit": 300, "retry_after": 60 }03 / Getting startedErrors
Errors are JSON with a conventional status code. The common ones:
| Status | Meaning | Example body |
|---|---|---|
| 401 | Missing, invalid, revoked or expired key | {"error": "Invalid or revoked API key."} |
| 403 | Key lacks a permission, IP not on the allowlist, or plan allowance reached | {"message": "API key does not have 'products:write' permission."} |
| 404 | The resource does not exist in your workspace | {"message": "Not found."} |
| 422 | Validation failed | {"message": "…", "errors": {"gtin": ["…"]}} |
| 429 | Rate 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.
https://myproductpassport.eu/api/mcpclaude mcp add --transport http myproductpassport https://myproductpassport.eu/api/mcp \
--header "Authorization: Bearer pk_live_your_key"{
"mcpServers": {
"myproductpassport": {
"url": "https://myproductpassport.eu/api/mcp",
"headers": { "Authorization": "Bearer pk_live_your_key" }
}
}
}{
"mcpServers": {
"myproductpassport": {
"command": "npx",
"args": ["mcp-remote", "https://myproductpassport.eu/api/mcp", "--header", "Authorization: Bearer pk_live_your_key"]
}
}
}The tools it offers:
| Tool | What it does |
|---|---|
| list_products | The workspace's products, newest first, 25 a page, with their sector and latest passport. |
| get_product | One product with its details, sector, regulatory sections and latest passport. |
| list_sectors | The 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_product | Adds 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_passports | The workspace's passports, newest first, 25 a page, with status (draft, published, archived), compliance percentage and public address. |
| get_passport | One passport: status, version, compliance percentage, public address and QR code links. |
| create_passport | Creates a draft passport for a product. It is not published: a person publishes it in the app. |
| get_passport_fields | What 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_fields | Writes 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_compliance | A passport's completeness: percentage, status and the required fields still missing. |
06 / EndpointsAccount
/me
Returns the authenticated account, its subscription tier, and how much of the product allowance is used. Useful as a connectivity check.
curl https://myproductpassport.eu/api/v1/me \
-H "Authorization: Bearer pk_your_key"{
"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
/products
requires products:read Lists the products in your workspace, newest first, 25 per page. Each product includes its sector and latest passport summary.
| Parameter | Type | Description | |
|---|---|---|---|
| page | integer | Page number. |
curl "https://myproductpassport.eu/api/v1/products?page=1" \
-H "Authorization: Bearer pk_your_key"{
"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 }
}/products
requires products:write Creates one product. Fails with 403 if the plan's product allowance is used up.
| Parameter | Type | Description | |
|---|---|---|---|
| name | string | required | Product name, up to 255 characters. |
| sku | string | Your internal SKU, up to 100 characters. | |
| gtin | string | GTIN-8, 12, 13 or 14 (digits only). | |
| brand | string | Brand shown on the public passport. | |
| description | string | Up to 2,000 characters. | |
| category | string | Free-text category. Defaults to "general". | |
| sector_id | integer | A sector id, which selects the passport field template. | |
| country_of_origin | string | Country code, e.g. "PT". | |
| weight | number | Net weight. | |
| weight_unit | string | One of kg, g, lb, oz. Defaults to kg. | |
| materials | array | Objects of {name, percentage 0-100}. | |
| has_packaging | boolean | Adds the packaging template's fields (keys starting pkg_) to the passport, under the same QR code. |
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}
]
}'{
"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"
}
}/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.
| Parameter | Type | Description | |
|---|---|---|---|
| products | array | required | 1-100 product objects using the same fields as Create a product (materials excluded). |
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}
]
}'{
"message": "2 product(s) created.",
"products": [
{ "id": 514, "name": "PSL-BT-122000", "status": "active" },
{ "id": 515, "name": "PSL-BT-123000", "status": "active" }
]
}/products/{id}
requires products:read Returns one product with its sector and latest passport summary. 404 if the id is not in your workspace.
curl https://myproductpassport.eu/api/v1/products/512 \
-H "Authorization: Bearer pk_your_key"/products/{id}
requires products:write Partially updates a product. Send only the fields you want to change; the field list matches Create a product.
curl -X PUT https://myproductpassport.eu/api/v1/products/512 \
-H "Authorization: Bearer pk_your_key" \
-H "Content-Type: application/json" \
-d '{"brand": "Northloom"}'/products/{id}
requires products:write Soft-deletes a product. Published passports for the product keep working, so printed QR codes are never orphaned.
curl -X DELETE https://myproductpassport.eu/api/v1/products/512 \
-H "Authorization: Bearer pk_your_key"{ "message": "Product deleted." }08 / EndpointsPassports
/passports
requires passports:read Lists passports, newest first, 25 per page, with product summary and all passport fields.
| Parameter | Type | Description | |
|---|---|---|---|
| page | integer | Page number. |
curl https://myproductpassport.eu/api/v1/passports \
-H "Authorization: Bearer pk_your_key"{
"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 }
}/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.
curl -X POST https://myproductpassport.eu/api/v1/products/512/passports \
-H "Authorization: Bearer pk_your_key"/passports/{id}
requires passports:read Returns one passport with product, fields, public URL and GS1 Digital Link.
curl https://myproductpassport.eu/api/v1/passports/96 \
-H "Authorization: Bearer pk_your_key"/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.
curl -X POST https://myproductpassport.eu/api/v1/passports/96/publish \
-H "Authorization: Bearer pk_your_key"/passports/{id}/archive
requires passports:publish Archives a passport and takes its public page out of circulation.
curl -X POST https://myproductpassport.eu/api/v1/passports/96/archive \
-H "Authorization: Bearer pk_your_key"/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.
| Parameter | Type | Description | |
|---|---|---|---|
| passport_ids | array | required | 1-50 passport ids. |
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]}'{
"published": 2,
"failed": 1,
"published_ids": [96, 97],
"errors": [
{ "id": 98, "error": "Compliance below the publishing threshold." }
]
}/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.
| Parameter | Type | Description | |
|---|---|---|---|
| stage | string | required | prepared_for_reuse, repurposed or remanufactured. |
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" }'{
"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": []
}
}
}/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.
curl https://myproductpassport.eu/api/v1/passports/96/fields \
-H "Authorization: Bearer pk_your_key"{
"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", "...": "..." }
}/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.
| Parameter | Type | Description | |
|---|---|---|---|
| fields | object | required | Map of field key to value. Keys are lowercase letters, digits and underscores; values up to 5,000 characters. Up to 200 keys per call. |
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%"
}
}'{
"message": "3 field(s) changed, 0 removed.",
"changed": ["carbon_footprint", "carbon_footprint_class", "recycled_content"],
"removed": [],
"passport": { "id": 96, "status": "published", "compliance_percentage": 92 }
}/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.
| Parameter | Type | Description | |
|---|---|---|---|
| state_of_health | string | e.g. "97% at 412 cycles". | |
| state_of_certified_energy | string | Certified energy remaining (EV batteries). | |
| battery_status | string | original, repurposed, re-used, remanufactured or waste. | |
| capacity_fade | string | Capacity fade to date. Stored as current_capacity_fade. | |
| power_capability | string | Current power capability. Stored as current_power_capability. | |
| internal_resistance | string | Measured internal resistance. Stored as current_internal_resistance. | |
| use_data | string | Cycles, negative events, temperature and state of charge, up to 5,000 characters. |
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"
}'{
"message": "2 field(s) updated.",
"updated_fields": ["state_of_health", "current_capacity_fade"],
"passport": { "id": 96, "status": "published", "compliance_percentage": 100 }
}/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.
| Parameter | Type | Description | |
|---|---|---|---|
| units[].serial | string | required | The battery's serial number. |
| units[].manufacturing_date | string | Month and year, e.g. "2026-09". | |
| units[].battery_status | string | original 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_health | string | Also 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. |
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%"}]}'{ "created": 1, "updated": 0, "skipped": 0, "errors": [] }/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).
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"}'{
"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" } }
}/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.
curl https://myproductpassport.eu/api/v1/passports/96/units/KES-00001/snapshots \
-H "Authorization: Bearer pk_your_key"{
"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" }
]
}/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.
| Parameter | Type | Description | |
|---|---|---|---|
| stage | string | required | prepared_for_reuse, repurposed or remanufactured. |
| values | object | required | The 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). |
| serial | string | The new serial. Default: the original serial plus -RU, -RP or -RM. | |
| measured_at | string | When the values were measured. | |
| member_state | string | Two-letter code of the Member State where the state of health was tested (Article 73(1)(a)). | |
| target_passport_id | integer | An existing second-life model of this battery model to register it under. |
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"}}'{
"data": {
"serial": "KES-00001-RP", "battery_status": "repurposed", "passport_id": 131,
"lineage": { "originals": [{ "serial": "KES-00001", "passport_id": 96, "stage": "repurposed" }], "successors": [] }
}
}/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.
| Parameter | Type | Description | |
|---|---|---|---|
| status | string | not_submitted, submitted, registered or failed. | |
| product_group | string | Registry product group, for example batteries. | |
| granularity | string | model, batch or item. | |
| upi | string | Unique product identifier as an https URL. Defaults to the public passport URL. | |
| correlation_id | string | Correlation ID assigned by the Registry on submission. | |
| uri | string | Registration identifier returned by the Registry. | |
| backup_url | string | https link to the back-up copy hosted by a DPP service provider. | |
| submitted_at | date | Submission date. | |
| registered_at | date | Registration date. | |
| error | string | Error description for a failed submission. |
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"
}'{
"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"
}
}
}/passports/{id}/qr
requires passports:read The passport's QR code as a file: svg (default), png (branded, print-ready) or pdf (label sheet).
| Parameter | Type | Description | |
|---|---|---|---|
| format | string | svg | png | pdf |
curl "https://myproductpassport.eu/api/v1/passports/96/qr?format=png" \
-H "Authorization: Bearer pk_your_key" -o passport-96.png09 / EndpointsCompliance
/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.
curl https://myproductpassport.eu/api/v1/passports/96/compliance \
-H "Authorization: Bearer pk_your_key"{
"data": {
"passport_id": 96,
"product_name": "Alpine Merino Crew Sweater",
"status": "compliant",
"percentage": 100,
"completed_fields": 12,
"total_fields": 12,
"missing_fields": []
}
}10 / EndpointsSectors
/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.
curl https://myproductpassport.eu/api/v1/sectors \
-H "Authorization: Bearer pk_your_key"{
"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
/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.
curl https://myproductpassport.eu/api/v1/products/512/documents \
-H "Authorization: Bearer pk_your_key"{
"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"
}
]
}/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.
| Parameter | Type | Description | |
|---|---|---|---|
| file | file | required | The document. |
| is_public | boolean | Show it on the public passport (default false). |
curl -X POST https://myproductpassport.eu/api/v1/products/512/documents \
-H "Authorization: Bearer pk_your_key" \
-F "[email protected]" \
-F "is_public=1"/products/{productId}/documents/{id}
requires products:write Toggles whether the document is linked from the public passport.
| Parameter | Type | Description | |
|---|---|---|---|
| is_public | boolean | required |
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}'/products/{productId}/documents/{id}
requires products:write Removes the file and its passport link.
curl -X DELETE https://myproductpassport.eu/api/v1/products/512/documents/31 \
-H "Authorization: Bearer pk_your_key"{ "message": "Document deleted." }12 / EndpointsSupply chain
/products/{productId}/supply-chain
requires products:read The steps shown on the product's passport, in order.
curl https://myproductpassport.eu/api/v1/products/512/supply-chain \
-H "Authorization: Bearer pk_your_key"/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.
| Parameter | Type | Description | |
|---|---|---|---|
| steps | array | required | Up to 50 steps. |
| steps[].step_type | string | required | |
| steps[].supplier_name | string | required | |
| steps[].supplier_location | string | ||
| steps[].supplier_country | string | ISO 3166-1 alpha-2. | |
| steps[].lat / lng | number | ||
| steps[].description | string | ||
| steps[].certifications | array | ||
| steps[].date | date |
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
/passports/{id}/translations
requires passports:read Every translated field value by locale, with its source: auto (machine, made on publish) or manual.
curl https://myproductpassport.eu/api/v1/passports/96/translations \
-H "Authorization: Bearer pk_your_key"{
"data": {
"passport_id": 96,
"available_locales": ["bg", "cs", "da", "de", "..."],
"translations": {
"de": {"care_instructions": {"value": "Kalt waschen", "source": "manual"}}
}
}
}/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.
| Parameter | Type | Description | |
|---|---|---|---|
| fields | object | required | field_key: translated value |
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"}}'{ "message": "1 translation(s) written, 0 removed.", "locale": "de", "written": 1, "removed": 0 }14 / EndpointsIntegrity and exports
/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.
curl https://myproductpassport.eu/api/v1/passports/96/integrity \
-H "Authorization: Bearer pk_your_key"{ "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": [] } }/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.
curl https://myproductpassport.eu/api/v1/passports/96/integrity/1 \
-H "Authorization: Bearer pk_your_key"/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.
curl https://myproductpassport.eu/api/v1/passports/96/epcis \
-H "Authorization: Bearer pk_your_key"15 / EndpointsSupplier requests
/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.
| Parameter | Type | Description | |
|---|---|---|---|
| supplier_name | string | required | |
| supplier_email | string | required | |
| field_keys | array | required | Field keys from the passport's template. |
| locale | string | Language of the email and form, e.g. de. | |
| due_at | date | Reply-by date; 14 days if omitted. | |
| message | string |
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"}'/passports/{id}/supplier-requests
requires passports:read Every request for the passport with its status.
curl https://myproductpassport.eu/api/v1/passports/96/supplier-requests \
-H "Authorization: Bearer pk_your_key"/supplier-requests/{id}
requires passports:read One request with the supplier's answers, note and attached file names.
curl https://myproductpassport.eu/api/v1/supplier-requests/12 \
-H "Authorization: Bearer pk_your_key"16 / EndpointsAfter-sales
/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}.
| Parameter | Type | Description | |
|---|---|---|---|
| items[].serial | string | required | Up to 20 letters, digits, dots, hyphens or underscores. |
| items[].batch | string | Batch or lot, up to 20 characters. | |
| items[].manufactured_on | string | Date of manufacture, YYYY-MM-DD. | |
| range | object | Instead of items: prefix, start, count, digits and batch, for example SN-00001 to SN-00500. |
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"}}'{ "created": 500, "updated": 0, "skipped": 0, "over_limit": 0, "errors": [] }/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).
curl https://myproductpassport.eu/api/v1/passports/96/items/SN-00001 \
-H "Authorization: Bearer pk_your_key"{
"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" } ] }
}/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.
| Parameter | Type | Description | |
|---|---|---|---|
| status | string | active, stolen, recalled or retired. | |
| status_note | string | Shown on the item's page, up to 500 characters. |
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."}'/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.
curl "https://myproductpassport.eu/api/v1/owner-registrations?marketing_consent=1&since=2026-10-01" \
-H "Authorization: Bearer pk_your_key"/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.
curl "https://myproductpassport.eu/api/v1/service-requests?status=new" \
-H "Authorization: Bearer pk_your_key"/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).
| Parameter | Type | Description | |
|---|---|---|---|
| body | string | required | The reply, up to 5,000 characters. |
| internal | boolean | A note for your team instead of a reply. | |
| status | string | new, in_progress, waiting, resolved or closed. |
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
/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).
| Parameter | Type | Description | |
|---|---|---|---|
| product_ids | array | required | |
| target | string | required | product | field |
| key | string | required | |
| value | string | ||
| mode | string | set | fill | clear |
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"}'{ "data": { "changed": 2, "skipped": 1, "errors": [] } }/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.
| Parameter | Type | Description | |
|---|---|---|---|
| image | file | required | |
| alt | string | Description of the image. |
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"/products/{id}/images/order
requires products:write Pass image_ids in the order you want; the first becomes the cover.
| Parameter | Type | Description | |
|---|---|---|---|
| image_ids | array | required |
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]}'/products/{id}/images/{imageId}
requires products:write Removes the image and its file.
curl -X DELETE https://myproductpassport.eu/api/v1/products/12/images/31 \
-H "Authorization: Bearer pk_your_key"18 / EndpointsWebhooks
/webhooks
requires webhooks:manage The endpoints registered for your workspace. Secrets are not included here; fetch one endpoint to see its secret.
curl https://myproductpassport.eu/api/v1/webhooks \
-H "Authorization: Bearer pk_your_key"/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.
| Parameter | Type | Description | |
|---|---|---|---|
| url | string | required | |
| events | array | required | Any of the events listed below. |
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"]}'{
"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"
}
}/webhooks/{id}
requires webhooks:manage curl https://myproductpassport.eu/api/v1/webhooks/3 \
-H "Authorization: Bearer pk_your_key"/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.
| Parameter | Type | Description | |
|---|---|---|---|
| url | string | ||
| events | array | ||
| is_active | boolean |
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}'/webhooks/{id}/test
requires webhooks:manage Queues a webhook.test delivery so you can check your receiver and signature code.
curl -X POST https://myproductpassport.eu/api/v1/webhooks/3/test \
-H "Authorization: Bearer pk_your_key"{ "message": "Test delivery queued." }/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.
| Parameter | Type | Description | |
|---|---|---|---|
| per_page | integer | Up to 100. |
curl https://myproductpassport.eu/api/v1/webhooks/3/deliveries \
-H "Authorization: Bearer pk_your_key"/webhooks/{id}
requires webhooks:manage curl -X DELETE https://myproductpassport.eu/api/v1/webhooks/3 \
-H "Authorization: Bearer pk_your_key"{ "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:
| Event | Fires when |
|---|---|
| product.created | A product is created (app, API or CSV import) |
| product.updated | A product is updated |
| product.deleted | A product is deleted |
| passport.created | A passport is generated |
| passport.published | A passport goes live |
| passport.archived | A passport is archived |
| supplier_request.submitted | A supplier sends the information you asked for |
| owner.registered | An owner confirms their registration |
| owner.transferred | An item passes to a new registered owner |
| service_request.created | A customer sends a service request from a passport |
| service_request.updated | A service request gets a reply or a new status |
| item.status_changed | An item is flagged stolen, recalled or retired, or cleared |
| item.verification_failed | A secure tag tap fails, wrong codes pile up, or one serial is seen in too many places |
| webhook.test | You 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:
{
"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:
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.
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $endpointSecret);
abort_unless(hash_equals($expected, $request->header('X-MPP-Signature')), 401);