Este artículo aún no está disponible en español; se muestra a continuación en inglés.
On this page1. Create an API key
This walkthrough takes a new LMT battery model to a hundred published passports and a first state-of-health reading. Every request is shown in cURL, Node.js, Python, PHP, Java, C# and Go, plus the TypeScript SDK; pick a language once and every block follows. Samples read the key from DPP_API_KEY and use example ids — replace them with the ids your previous step returned.
Personalise the samples. Paste a test key and every snippet uses it — and your own model and passport ids.
Step 1 of 9
Create an API key
Sign in to the app and open Organisation → API keys. Create a test key (
dpp_test_…) with thereadandwritescopes. A test key runs every write as a dry run: fully validated against your real data, never stored. You can follow this whole page with it and change nothing.The key is shown exactly once. Put it in an environment variable straight away — every sample below reads
DPP_API_KEY. When you are ready to create real passports, create a live key (dpp_live_…) and swap it in; nothing else changes.Terminal# macOS / Linux. Keep the key out of source code and shell history.export DPP_API_KEY="dpp_test_…" # or dpp_live_… once you are readyStep 2 of 9
Check the key
GET/v1/meGET /meanswers with the key, its scopes and mode, your organisation, your plan, this year’s usage and your rate limit. It is the cheapest way to confirm a key works before wiring it into anything; theX-DPP-Moderesponse header repeats the mode.GET /v1/mecurl "https://dppapi-production.up.railway.app/v1/me" \-H "Authorization: Bearer $DPP_API_KEY"Expected response · 200 OK{"apiKey": {"name": "SAP production","prefix": "dpp_test_3f9a","scopes": ["read", "write"],"mode": "test"},"organisation": {"id": "cm1q7v0aa0000l70f1a2b3c4d","legalName": "Example Batteries GmbH","country": "Germany","esprOperatorIdentifier": null},"plan": {"key": "CONNECTED","label": "Connected",Step 3 of 9
Create a battery model
POST/v1/modelsA model carries the data every battery of that design shares: manufacturer, weight, capacity, voltages and the other model-level data points of Annex VI and Annex XIII.
category(EV,LMTorINDUSTRIAL),name,capacityKwhandchemistryare required; data points go infields, keyed by the names in the field catalogue.Industrial batteries are in passport scope only above 2 kWh, so an
INDUSTRIALmodel withcapacityKwhof 2 or less is refused with a 400.POST /v1/modelscurl -X POST "https://dppapi-production.up.railway.app/v1/models" \-H "Authorization: Bearer $DPP_API_KEY" \-H "Content-Type: application/json" \-d '{"category": "LMT","name": "Cargo 48V 15Ah","capacityKwh": 0.72,"chemistry": "Lithium-ion NMC","fields": {"manufacturerName": "Example Batteries GmbH","manufacturerAddress": "Musterstraße 1, 70173 Stuttgart, Germany","modelDesignation": "C48-15","weightKg": 4.2,"ratedCapacity": 15,"nominalVoltage": 48,"minVoltage": 39,"maxVoltage": 54.6}}'Expected response · 201 Created{"id": "cm1q8x2k40003l70f9a8s7d6e","templateKey": "battery","category": "LMT","industrialExternalStorage": false,"name": "Cargo 48V 15Ah","capacityKwh": 0.72,"chemistry": "Lithium-ion NMC","fields": {"manufacturerName": "Example Batteries GmbH","ratedCapacity": 15},"createdAt": "2027-01-15T09:12:44.120Z","updatedAt": "2027-01-15T09:12:44.120Z"}Step 4 of 9
Check what is still missing
GET/v1/models/{id}/completenessThe missing-field analysis compares the model against the obligations of its category.
missinglists required data points that are still empty — these block a compliant passport.conditionalUnsetlists the "if applicable" ones, as prompts rather than blockers.Required documents count too: until an EU Declaration of Conformity is uploaded with
POST /models/{id}/documents, it is reported as missing.GET /v1/models/{id}/completenesscurl "https://dppapi-production.up.railway.app/v1/models/cm1q8x2k40003l70f9a8s7d6e/completeness" \-H "Authorization: Bearer $DPP_API_KEY"Expected response · 200 OK{"category": "LMT","requiredTotal": 41,"requiredFilled": 9,"complete": false,"missing": [{"name": "hazardousSubstances","label": "Hazardous substances (other than Hg/Cd/Pb)","point": 1,"annexRef": "Annex VI A","granularity": "model"}],"conditionalUnset": [],"missingByPoint": { "1": 18, "2": 5, "3": 1, "4": 8 }}Step 5 of 9
Fill the gaps
PATCH/v1/models/{id}Send the missing values with
PATCH /models/{id}. Fields are merged into what is stored, so send only what you are adding. Upload the EU Declaration of Conformity as a document. Repeat the completeness check untilcompleteistrue.This step is not optional: the server refuses to publish passports of a model that is incomplete for its category, in the app and over the API alike.
PATCH /v1/models/{id}curl -X PATCH "https://dppapi-production.up.railway.app/v1/models/cm1q8x2k40003l70f9a8s7d6e" \-H "Authorization: Bearer $DPP_API_KEY" \-H "Content-Type: application/json" \-d '{"fields": {"hazardousSubstances": "None above reporting thresholds","extinguishingAgent": "Water, foam","criticalRawMaterials": "Cobalt, lithium, natural graphite","renewableContentShare": 0}}'Upload the EU Declaration of Conformitycurl -X POST "https://dppapi-production.up.railway.app/v1/models/cm1q8x2k40003l70f9a8s7d6e/documents" \-H "Authorization: Bearer $DPP_API_KEY" \-F "type=EU Declaration of Conformity" \-F "file=@declaration.pdf;type=application/pdf"Step 6 of 9
Create 100 batteries
POST/v1/items/batchEvery physical battery gets its own item and its own passport, with a unique identifier minted for it.
POST /items/batchcreates up to 1,000 in one request — eithercountidentical batteries numbered from aserialPrefix, or explicitrowsof serial numbers you already have. When both are sent,rowswins.Send an
Idempotency-Key: if the connection drops and you send the same request again, you get the first response back instead of a second hundred batteries. SetplaceOfManufacturenow — publishing refuses a passport without one, because the EU Registry does. New passports are drafts: nothing is public or billed until you publish.POST /v1/items/batchcurl -X POST "https://dppapi-production.up.railway.app/v1/items/batch" \-H "Authorization: Bearer $DPP_API_KEY" \-H "Content-Type: application/json" \-H "Idempotency-Key: 7b3e0c52-1f7d-4b8a-9a52-2f0e6d1c9a44" \-d '{"modelId": "cm1q8x2k40003l70f9a8s7d6e","count": 100,"serialPrefix": "C48-2027-","placeOfManufacture": "Stuttgart, Germany","dateOfManufacture": "2027-01-15"}'Expected response · 201 Created{"count": 100,"items": [{"id": "cm1q8y0bq0005l70fq2r3t4u5","modelId": "cm1q8x2k40003l70f9a8s7d6e","uniqueIdentifier": "C-7KQ2M9XHT4R","identifierOrigin": "MINTED","serialNumber": "C48-2027-0001","placeOfManufacture": "Stuttgart, Germany","dateOfManufacture": "2027-01-15T00:00:00.000Z","passport": {"id": "cm1q8y0bq0006l70fx9y8z7w6","status": "ORIGINAL","lifecycleState": "ACTIVE","publishedAt": null,Step 7 of 9
Publish the passports
POST/v1/passports/publishPublishing puts a passport on the public resolver, freezes a hashed version for the archive, and is the moment it counts as issued.
POST /passports/publishtakes up to 1,000 passport ids and answers per row:200when every row published,207when some did not, with the reason on each failed row.Publishing an already-published passport is harmless: the row comes back
okwithalreadyPublished. Publishing needs a paid plan; on the free plan the request is answered with402.POST /v1/passports/publishcurl -X POST "https://dppapi-production.up.railway.app/v1/passports/publish" \-H "Authorization: Bearer $DPP_API_KEY" \-H "Content-Type: application/json" \-d '{"ids": ["cm1q8y0bq0006l70fx9y8z7w6","cm1q8y0bq0008l70fa1b2c3d4"]}'Expected response · 200 OK{"requested": 2,"published": 2,"failed": 0,"results": [{ "id": "cm1q8y0bq0006l70fx9y8z7w6", "ok": true },{ "id": "cm1q8y0bq0008l70fa1b2c3d4", "ok": true }]}Step 8 of 9
Record a state-of-health reading
POST/v1/passports/{id}/eventsDynamic data is appended as events, never overwritten, so the passport keeps its history. The payload is keyed by the field names of the dynamic data points. For an LMT battery, state of health is remaining capacity, remaining power capability and remaining round-trip efficiency; for an EV, state of certified energy (
stateOfCertifiedEnergy).POST /v1/passports/{id}/eventscurl -X POST "https://dppapi-production.up.railway.app/v1/passports/cm1q8y0bq0006l70fx9y8z7w6/events" \-H "Authorization: Bearer $DPP_API_KEY" \-H "Content-Type: application/json" \-d '{"eventType": "SOH_READING","payload": {"remainingCapacity": 14.6,"capacityFade": 2.7}}'Expected response · 201 Created{"id": "cm1qa1d7e0009l70f1k2j3h4g","passportId": "cm1q8y0bq0006l70fx9y8z7w6","eventType": "SOH_READING","payload": { "remainingCapacity": 14.6, "capacityFade": 2.7 },"recordedAt": "2027-06-02T07:30:00.000Z","sourceOrgId": "cm1q7v0aa0000l70f1a2b3c4d","bmsHistoryInvalidatedByReset": false}Step 9 of 9
Fetch the public JSON-LD
GET/p/{uniqueIdentifier}.jsonldThe QR code on the battery resolves to the public resolver. It needs no key and serves the public tier only — the data Annex XIII makes available to anyone. Restricted tiers never leave the server on this path.
An unpublished or unknown identifier answers
404. A recycled battery answers410 Gone: its passport has ceased to exist.GET /p/{uniqueIdentifier}.jsonldcurl "https://dppapi-production.up.railway.app/p/C-7KQ2M9XHT4R.jsonld"Expected response · 200 OK{"@context": {"@vocab": "https://dpp.eu/vocab/battery#","schema": "https://schema.org/"},"@type": "BatteryPassport","@id": "https://dppapi-production.up.railway.app/p/C-7KQ2M9XHT4R","uniqueIdentifier": "C-7KQ2M9XHT4R","category": "LMT","status": "ORIGINAL","lifecycleState": "ACTIVE","datePublished": "2027-01-15T10:02:11.000Z","manufacturer": {"@type": "schema:Organization","name": "Example Batteries GmbH","addressCountry": "Germany",
Where to go next
Wire it into production with the ERP guide, make every write retry-safe with idempotency keys, get told about changes with webhooks, and map the rest of your data with the field catalogue.