Developers · Getting started

TypeScript SDK

The TypeScript client for the Battery Passport API: retries, idempotency keys, pagination iterators and dry runs built in.

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

The official TypeScript client, @collecta/dpp-sdk: every request and response typed, an idempotency key on every write, automatic retries, auto-pagination and webhook verification — with no runtime dependencies.

Install

Shell
npm install @collecta/dpp-sdk# pnpm add @collecta/dpp-sdk · yarn add @collecta/dpp-sdk

It uses the global fetch, so it runs on Node 18+, Deno, Bun, Cloudflare Workers and Vercel Edge. It ships as ESM, with a second entry point, @collecta/dpp-sdk/webhooks, for receivers.

Quickstart

TypeScript SDK
import { CollectaDpp } from '@collecta/dpp-sdk';const dpp = new CollectaDpp({ apiKey: process.env.DPP_API_KEY! });// 1. A battery model: the data every battery of this type shares.const model = await dpp.models.create({category: 'EV',name: 'NV-EV-90',capacityKwh: 90,chemistry: 'NMC811',fields: { manufacturerName: 'Nordvolt Energy GmbH', modelDesignation: 'NV-EV-90' },});// 2. What is still missing for an EV passport?const report = await dpp.models.completeness(model.id);console.log(report.missing.map((gap) => gap.label));// 3. One battery = one passport (Art. 77(1)), with an identifier that never changes.const battery = await dpp.batteries.create({modelId: model.id,serialNumber: 'NV-0001',placeOfManufacture: 'Berlin, DE',});// 4. Publish it, and fetch its print-grade QR code.await dpp.passports.publish(battery.passport.id);const png = await dpp.passports.qrPng(battery.passport.id); // Uint8Array

The Quickstart shows every step with an SDK tab next to the raw HTTP samples.

Configuration

TypeScript SDK
const dpp = new CollectaDpp({apiKey: 'dpp_live_…',                                   // requiredbaseUrl: 'https://dppapi-production.up.railway.app/v1', // default; include /v1maxRetries: 3,                                          // retries after the first attempttimeoutMs: 30_000,                                      // per attemptfetch: customFetch,                                     // defaults to globalThis.fetchuserAgent: 'acme-erp/4.2',                              // appended to the SDK's user agentonResponse: (r) => log(r.requestId, r.status, r.rateLimit?.remaining),});dpp.mode; // "live" | "test", read from the key prefix

Every method also takes a final options argument:

TypeScript SDK
{ idempotencyKey?: string; dryRun?: boolean; signal?: AbortSignal; timeoutMs?: number; maxRetries?: number }

Resources

ResourceMethods
dpp.modelslist, listAll, get, create, update, validate, completeness, uploadDocument
dpp.batteriescreate, createMany, get, listForModel
dpp.passportslist, listAll, get, publish, publishMany, recordEvent, events, eventsAll, changeStatus, recycle, qrSvg, qrPng, versions
dpp.fleetbulkLifecycle, importUpdates
dpp.documentslist, listAll, get, download
dpp.auditlist, listAll
dpp.webhookslist, create, get, update, rotateSecret, test, deliveries, deliveriesAll, resend, eventTypes
dpp.exportjson, archive
Clientme(), fieldCatalog(), request() for any other endpoint

Models

TypeScript SDK
import { readFile } from 'node:fs/promises';const page = await dpp.models.list({ category: 'EV', complete: false, updatedSince: '2027-01-01', limit: 50 });page.data[0].requiredFilled; // completeness is part of every list rowconst model = await dpp.models.get(id);                         // with conformity, documents, declarationsawait dpp.models.update(id, { fields: { ratedCapacity: 225 } }); // fields merge; omitted keys are keptconst check = await dpp.models.validate({category: 'EV',name: 'X',capacityKwh: 90,chemistry: 'LFP',documentTypes: ['EU Declaration of Conformity'],});check.complete; check.missing; check.unknownFields;              // nothing storedawait dpp.models.uploadDocument(id, {file: await readFile('DoC.pdf'),                               // Buffer, Uint8Array, ArrayBuffer, Blob or Filefilename: 'DoC.pdf',type: 'EU Declaration of Conformity',point: 1,                                                      // 1 public (default), 3 authority only});

Batteries

TypeScript SDK
const battery = await dpp.batteries.create({modelId,serialNumber: 'NV-0001',dateOfManufacture: new Date('2027-01-15'),placeOfManufacture: 'Berlin, DE',});battery.uniqueIdentifier; // never changesbattery.passport.id;      // the draft passport// Up to 1,000 in one transaction: explicit serials …await dpp.batteries.createMany({ modelId, rows: [{ serialNumber: 'A-1' }, { serialNumber: 'A-2' }] });// … or a count with a prefix (NV-0001, NV-0002, …)await dpp.batteries.createMany({ modelId, count: 500, serialPrefix: 'NV-', placeOfManufacture: 'Berlin, DE' });await dpp.batteries.get(itemId); // with model, passport, registrationawait dpp.batteries.listForModel(modelId, { status: 'draft', q: 'NV-00', page: 1, pageSize: 100 });

Passports

TypeScript SDK
await dpp.passports.list({ modelId, status: 'attention', published: true, updatedSince: lastSync });const passport = await dpp.passports.get(id);               // every field, events, lineageawait dpp.passports.publish(id);                            // 422 publish_blocked if not readyconst batch = await dpp.passports.publishMany(ids);         // per-row resultsbatch.results.filter((r) => !r.ok);await dpp.passports.recordEvent(id, { eventType: 'SOH_READING', payload: { stateOfCertifiedEnergy: 96.4 } });for await (const e of dpp.passports.eventsAll(id, { eventType: 'CYCLE_COUNT' })) { /* … */ }const result = await dpp.passports.changeStatus(id, { status: 'REPURPOSED', note: 'Second life: storage' });if ('forked' in result) console.log('new passport', result.childUid);await dpp.passports.recycle(id, { note: 'Shredded at plant 3' }); // the public page answers 410 from now onconst svg = await dpp.passports.qrSvg(id);                  // stringconst versions = await dpp.passports.versions(id);          // the published archive, newest first

Webhook endpoints

TypeScript SDK
const endpoint = await dpp.webhooks.create({url: 'https://erp.example.com/dpp-webhooks',events: ['passport.published', 'passport.recycled'], // omit for every event});saveSecret(endpoint.secret); // shown once: store it nowawait dpp.webhooks.update(endpoint.id, { enabled: false });       // paused, not deletedconst { secret } = await dpp.webhooks.rotateSecret(endpoint.id);  // the old secret stops verifying at onceawait dpp.webhooks.test(endpoint.id);                             // sends a ping, answers with your receiver's replyfor await (const d of dpp.webhooks.deliveriesAll(endpoint.id, { status: 'failed' })) {await dpp.webhooks.resend(d.id);}

Anything else

Endpoints without a dedicated method — facilities, supplier requests — go through dpp.request() with the same authentication, retries, idempotency and errors:

TypeScript SDK
const plants = await dpp.request<Array<{ id: string; name: string }>>({ method: 'GET', path: '/plants' });

Pagination

TypeScript SDK
// Every item, fetched lazily, page by pagefor await (const passport of dpp.passports.listAll({ limit: 200 })) { /* … */ }// Collect, with an optional capconst first500 = await dpp.passports.listAll().toArray({ max: 500 });// Whole pagesfor await (const page of dpp.audit.listAll().pages()) console.log(page.data.length);// Or by handlet page = await dpp.passports.list({ limit: 200 });while (page.has_more) page = await dpp.passports.list({ limit: 200, cursor: page.next_cursor! });

listAll is available on models, passports (plus eventsAll), documents, audit and webhooks.deliveriesAll.

Idempotency and retries

Every write is sent with an Idempotency-Key. When you pass none, the SDK generates a UUID and reuses it for each retry of that call, so a timeout after the server created 500 batteries returns the stored answer instead of creating 500 more. Pass your own key to stay safe across processes, such as a job queue re-running a task:

TypeScript SDK
await dpp.batteries.createMany({ modelId, count: 500 }, { idempotencyKey: `order-${orderId}-batteries` });
  • Retried automatically: network errors, timeouts, 429, 5xx and 409 idempotency_in_progress. Other 4xx are never retried.
  • Backoff: 500 ms · 2^attempt, capped at 30 s, with 50–100 % jitter; retry-after is honoured up to the cap.
  • maxRetries (default 3) and timeoutMs (default 30 s per attempt) on the client or per call; an AbortSignal cancels the call and any pending retry wait.

Dry runs

With a live key, dryRun: true validates a write and stores nothing, sends no webhook and counts no usage. The return type changes to DryRunResult. A test key dry-runs every write; isDryRunResult() handles both in one code path.

TypeScript SDK
import { isDryRunResult } from '@collecta/dpp-sdk';const check = await dpp.passports.publish(id, { dryRun: true });if (!check.would_publish) console.log(check.blockers); // e.g. missing_place_of_manufactureconst result = await dpp.batteries.create(input);if (isDryRunResult(result)) console.log('validated only:', result.checks);

Typed errors

ClassStatusRetried
BadRequestError400 — details maps fields to messagesNo
AuthenticationError401No
PaymentRequiredError402 — requiredPlan names the planNo
PermissionDeniedError403No
NotFoundError404No
ConflictError409Only idempotency_in_progress
GoneError410No
UnprocessableEntityError422 — publish_blocked, idempotency_key_reused, …No
RateLimitError429 — retryAfterSecondsYes
InternalServerError5xxYes
DppConnectionError, DppTimeoutErrorNo answerYes

All API errors extend DppApiError and carry status, code (the error field), message, details, requestId and the raw body. For publish_blocked, details holds the blockers.

TypeScript SDK
import { DppApiError, UnprocessableEntityError } from '@collecta/dpp-sdk';try {await dpp.passports.publish(id);} catch (error) {if (error instanceof UnprocessableEntityError && error.code === 'publish_blocked') {for (const blocker of error.details as Array<{ reason: string; message: string }>) console.log(blocker.message);} else if (error instanceof DppApiError) {console.error(error.status, error.code, error.requestId);} else {throw error;}}

Response metadata

TypeScript SDK
const { data, response } = await dpp.passports.publish(id).withResponse();response.requestId;          // x-request-idresponse.mode;               // "live" | "test"response.dryRun;             // x-dpp-dry-runresponse.idempotentReplayed; // idempotent-replayedresponse.idempotencyKey;     // the key that was sentresponse.rateLimit;          // { limit, remaining, resetSeconds }response.attempt;            // 0 on the first try, 1 after one retry, …

Verifying webhooks

verifyWebhook checks DPP-Signature in constant time, refuses deliveries outside toleranceSeconds (default 300) to block replays, and returns the typed event. It uses node:crypto on Node and WebCrypto elsewhere. Pass the raw body — parsing and re-serialising JSON changes the bytes and the signature will not match.

Express
import express from 'express';import { verifyWebhook, WebhookVerificationError } from '@collecta/dpp-sdk/webhooks';const app = express();// Raw body for this route only; do not put express.json() in front of it.app.post('/dpp-webhooks', express.raw({ type: 'application/json' }), async (req, res) => {try {const event = await verifyWebhook({payload: req.body, // a Bufferheader: req.headers['dpp-signature'],secret: process.env.DPP_WEBHOOK_SECRET!,});res.sendStatus(200);await handle(event);} catch (error) {if (error instanceof WebhookVerificationError) return res.status(400).send(error.reason);throw error;}});
Next.js (App Router) · app/api/dpp-webhooks/route.ts
import { verifyWebhook, WebhookVerificationError } from '@collecta/dpp-sdk/webhooks';export async function POST(request: Request) {const payload = await request.text(); // the raw bodytry {const event = await verifyWebhook({payload,header: request.headers.get('dpp-signature'),secret: process.env.DPP_WEBHOOK_SECRET!,});await handle(event);return new Response(null, { status: 200 });} catch (error) {if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });throw error;}}

Next.js Pages Router: export config = { api: { bodyParser: false } } and read the raw body from the request stream before verifying.

Typed events

TypeScript SDK
import type { WebhookEvent } from '@collecta/dpp-sdk/webhooks';async function handle(event: WebhookEvent) {switch (event.type) {case 'passport.published':await markLive(event.data.passport.uniqueIdentifier, event.data.passport.publicUrl);break;case 'passport.event_recorded':console.log(event.data.event.eventType, event.data.event.payload);break;case 'passport.status_changed':if (event.data.forked) console.log('new passport', event.data.forked.uniqueIdentifier);break;case 'supplier.submitted':console.log(event.data.request.supplierName, event.data.fields ?? event.data.file);break;case 'ping':break;}}
  • Secret rotation: pass both secrets while you deploy the new one — secret: [newSecret, oldSecret].
  • Testing a receiver without the API: generateWebhookSignatureHeader({ payload, secret }) from @collecta/dpp-sdk/webhooks produces a valid header.
  • Respond 2xx quickly and work afterwards; de-duplicate on event.id.

TypeScript types

TypeScript SDK
import type {ModelCreateInput, BatteryModel, ModelSummary, CompletenessReport,BatteryCreateInput, CreatedBattery, PassportListRow, PassportDetail,DynamicEventInput, DynamicEventType, DryRunResult, Page, ResponseMeta,} from '@collecta/dpp-sdk';import type { WebhookEvent, WebhookEventOf } from '@collecta/dpp-sdk/webhooks';type Published = WebhookEventOf<'passport.published'>;

Response dates are ISO 8601 strings; dates you send may be Date objects or strings. Within v1 the API only adds fields, so expect extra properties on responses.

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