Developers · Getting started

Quickstart

From an API key to a hundred published battery passports in nine steps, in cURL, Node.js, Python, PHP, Java, C# and Go.

Version
v1.0.0
Base URL
https://dppapi-production.up.railway.app/v1
Format
JSON · OpenAPI 3.1
FeedbackLast updated
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.

  1. 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 the read and write scopes. 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 ready
  2. Step 2 of 9

    Check the key

    GET/v1/me

    GET /me answers 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; the X-DPP-Mode response header repeats the mode.

    GET /v1/me
    curl "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",
  3. Step 3 of 9

    Create a battery model

    POST/v1/models

    A 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, LMT or INDUSTRIAL), name, capacityKwh and chemistry are required; data points go in fields, keyed by the names in the field catalogue.

    Industrial batteries are in passport scope only above 2 kWh, so an INDUSTRIAL model with capacityKwh of 2 or less is refused with a 400.

    POST /v1/models
    curl -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"}
  4. Step 4 of 9

    Check what is still missing

    GET/v1/models/{id}/completeness

    The missing-field analysis compares the model against the obligations of its category. missing lists required data points that are still empty — these block a compliant passport. conditionalUnset lists 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}/completeness
    curl "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 }}
  5. 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 until complete is true.

    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 Conformity
    curl -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"
  6. Step 6 of 9

    Create 100 batteries

    POST/v1/items/batch

    Every physical battery gets its own item and its own passport, with a unique identifier minted for it. POST /items/batch creates up to 1,000 in one request — either count identical batteries numbered from a serialPrefix, or explicit rows of serial numbers you already have. When both are sent, rows wins.

    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. Set placeOfManufacture now — 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/batch
    curl -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,
  7. Step 7 of 9

    Publish the passports

    POST/v1/passports/publish

    Publishing puts a passport on the public resolver, freezes a hashed version for the archive, and is the moment it counts as issued. POST /passports/publish takes up to 1,000 passport ids and answers per row: 200 when every row published, 207 when some did not, with the reason on each failed row.

    Publishing an already-published passport is harmless: the row comes back ok with alreadyPublished. Publishing needs a paid plan; on the free plan the request is answered with 402.

    POST /v1/passports/publish
    curl -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 }]}
  8. Step 8 of 9

    Record a state-of-health reading

    POST/v1/passports/{id}/events

    Dynamic 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}/events
    curl -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}
  9. Step 9 of 9

    Fetch the public JSON-LD

    GET/p/{uniqueIdentifier}.jsonld

    The 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 answers 410 Gone: its passport has ceased to exist.

    GET /p/{uniqueIdentifier}.jsonld
    curl "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.

Cookie preferences

You choose which cookies are used. You can change your choice at any time with “Cookie settings” in the footer. Privacy policy

Necessary

Always on

Needed for the site to work: your light/dark theme and this cookie choice are stored in your browser. They do not track you.

Analytics

Google Analytics 4 measures how pages are used through statistics, which helps us improve the site. If you accept, it sets cookies and measures your visit in full. If you decline, it sets no cookies, but Google may still receive technical data such as the page viewed, your browser type and IP address.

_ga, _ga_* · up to 2 years · Google