Developers · Core concepts

Webhooks

Signed, retried event notifications for passports, models and supplier submissions, with signature verification in Node.js, Python, PHP, Java, C#, Go and the TypeScript SDK.

Version
v1.0.0
Base URL
https://dppapi-production.up.railway.app/v1
Format
JSON · OpenAPI 3.1
FeedbackLast updated
On this pageSet up an endpoint

Instead of polling, let us tell you. Every change worth knowing about becomes a signed HTTPS POST to your endpoint, retried until it gets through.

Set up an endpoint

Register an endpoint in the app under Organisation → API → Webhooks, or over the API with `POST /webhooks`. Choose the events it receives — none selected means all; `GET /webhooks/event-types` lists them. Copy its signing secret (whsec_…) into your secret store, then send a signed ping with `POST /webhooks/{id}/test` — or from the app — and check your receiver answers 2xx.

  • The URL must use https and resolve to a public address; private, loopback and link-local addresses are refused.
  • Credentials in the URL are not allowed — the signature is the authentication.

Over the API you can also update or pause an endpoint (enabled: false), rotate its secret, read its delivery log and resend a delivery. Endpoints are paused rather than deleted over the API, so their history survives.

Events

TypeWhendata contains
passport.createdA battery and its draft passport were created (once per battery in a batch).passport
passport.publishedA passport was published.passport
passport.status_changedA passport’s status changed (PATCH /passports/{id}/status).passport, status; forked (passportId, uniqueIdentifier of the new linked passport) for a repurpose, re-use or remanufacture — none for WASTE
passport.recycledA passport ceased because the battery was recycled.passport
passport.event_recordedDynamic data was recorded.passport, event (id, eventType, payload, recordedAt)
model.createdA model was created.model
model.updatedA model was changed.model
document.uploadedA document was uploaded to a model.document (id, modelId, name, type, point, createdAt)
supplier.submittedA supplier answered a data request — values or a file.request (id, supplierName, modelId, kind), and fields or file

passport is { id, uniqueIdentifier, serialNumber, itemId, modelId, status, lifecycleState, publishedAt, publicUrl }; model is { id, name, category, capacityKwh, chemistry, updatedAt }. Payloads are deliberately small: fetch the full record with its id when you need more. The test event is ping, with data.message. Each event type is also described in the webhooks section of the OpenAPI document.

The delivery

POST to your endpoint
POST /hooks/dpp HTTP/1.1Content-Type: application/jsonUser-Agent: Collecta-DPP-Webhooks/1.0DPP-Event: passport.publishedDPP-Event-Id: evt_5f0c1d2e3a4b4c6d8e9f0a1b2c3d4e5fDPP-Delivery: cm1qf3g4h0060l70fm5n6b7v8DPP-Signature: t=1799920931,v1=6c1f0e…9b2a
Body
{"id": "evt_5f0c1d2e3a4b4c6d8e9f0a1b2c3d4e5f","type": "passport.published","created_at": "2027-01-15T10:02:11.214Z","data": {"passport": {"id": "cm1q8y0bq0006l70fx9y8z7w6","uniqueIdentifier": "C-7KQ2M9XHT4R","serialNumber": "C48-2027-0001","itemId": "cm1q8y0bq0005l70fq2r3t4u5","modelId": "cm1q8x2k40003l70f9a8s7d6e","status": "ORIGINAL","lifecycleState": "ACTIVE","publishedAt": "2027-01-15T10:02:11.000Z","publicUrl": "https://dppapi-production.up.railway.app/p/C-7KQ2M9XHT4R"}}}

Verify the signature

DPP-Signature is t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of the string "<t>.<raw body>" keyed with your endpoint secret. To verify a delivery:

  1. 1Read the raw request body, byte for byte. Parsing and re-serialising JSON changes it and breaks the signature.
  2. 2Split the header on , and each part on the first =; take t and v1.
  3. 3Reject the delivery if t is more than five minutes from your clock — that blocks replays of captured requests.
  4. 4Compute HMAC-SHA256 of t + "." + body with the secret, as hex, and compare it to v1 in constant time.
// Node.js 18+, no dependencies. Verify against the RAW body — not re-serialised JSON.import { createHmac, timingSafeEqual } from 'node:crypto';import { createServer } from 'node:http';const SECRET = process.env.DPP_WEBHOOK_SECRET; // whsec_…const TOLERANCE_SECONDS = 300;export function verifyDppSignature(rawBody, header, secret = SECRET) {const parts = Object.fromEntries(String(header ?? '').split(',').map((pair) => pair.trim().split('=', 2)),);const timestamp = Number(parts.t);if (!Number.isInteger(timestamp) || !parts.v1) return false;if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest();const received = Buffer.from(parts.v1, 'hex');return received.length === expected.length && timingSafeEqual(received, expected);}createServer((req, res) => {const chunks = [];req.on('data', (chunk) => chunks.push(chunk));req.on('end', () => {const raw = Buffer.concat(chunks).toString('utf8');if (!verifyDppSignature(raw, req.headers['dpp-signature'])) {res.writeHead(400).end();return;}const event = JSON.parse(raw);res.writeHead(200).end(); // acknowledge first, then do the workconsole.log(event.id, event.type, event.data);});}).listen(8080);

Responding and retries

  • Answer with any 2xx within 10 seconds. Acknowledge first and do the work afterwards, on a queue.
  • Anything else — another status, a timeout, a connection error, or a redirect (redirects are not followed) — is a failed attempt.
  • Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours. After the sixth attempt the delivery is marked failed.
  • The delivery log in the app shows each attempt, the status and body your endpoint returned, and lets you resend.

At least once, in any order

A delivery can arrive more than once — a retry after your endpoint processed it but timed out — and deliveries can arrive out of order. Make the handler idempotent: store the event id (also in DPP-Event-Id) and skip ids you have seen. Where order matters, compare created_at, or fetch the current state of the record by its id.

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