# AGENTS.md — Collecta DPP API integration rules

Drop this file into any repository that integrates with the Collecta Digital Product Passport (DPP) API. Coding agents (Claude Code, Codex, Cursor and others) read it before they write code. The rules are not style preferences: breaking one creates duplicate batteries, lost updates, forged webhooks or passports that cannot be published.

Source of truth: https://www.collectadpp.com/developers · OpenAPI 3.1: https://dppapi-production.up.railway.app/v1/openapi.json · LLM index: https://www.collectadpp.com/llms.txt (full corpus: /llms-full.txt)

## The domain in one paragraph

EU battery passports under Regulation (EU) 2023/1542. A **model** holds the data every battery of one design shares. Each **physical battery** has exactly one **passport** (Art. 77(1)) with a unique identifier that never changes and must resolve for ten years. Data point names come from the field catalogue (`GET /v1/field-catalog`, 71 EU data points with the obligation per category: `EV`, `LMT`, `INDUSTRIAL`). Never invent a field name: look it up.

## Base URL and authentication

- Base URL: `https://dppapi-production.up.railway.app/v1`. Read it from configuration (e.g. `DPP_BASE_URL`), never hard-code it in business logic.
- Every request: `Authorization: Bearer <key>`. Keys are created in the app under Organisation → API keys and shown once.
- Keys come from the environment (`DPP_API_KEY`) or a secret manager. Never commit a key, log it, or put it in client-side code.
- Scopes: `read` and `write`. Give each system the smallest scope it needs.
- API access is included on every plan. A `402 plan_upgrade_required` means the plan does not include that operation (e.g. publishing on Free) — a plan problem, not a bug.

## Always start with a test key

- `dpp_test_…` keys run **every write as a dry run**: validated against your real data (auth, scope, plan, schema, ownership, publish gate) and nothing is stored, sent or counted. Develop and run CI with a test key.
- Switch to `dpp_live_…` only when the integration is reviewed. The code must not change — only the key.
- With a live key, send `X-DPP-Dry-Run: true` (SDK: `{ dryRun: true }`) to validate a single write first.
- Check the mode on startup with `GET /v1/me` (also returns plan, usage and rate limit). The `X-DPP-Mode` response header repeats it.

## Writes

1. **Idempotency-Key on every write** (POST, PATCH). Generate it once per business operation (a UUID, or a stable key such as `erp-order-4711-line-3`) and reuse the same key on every retry of that operation. Keys are kept 24 hours per API key. Same key + different body → `422 idempotency_key_reused`; same key still running → `409 idempotency_in_progress` (retry shortly).
2. **Validate before create.** `POST /v1/models/validate` takes the same body as `POST /v1/models` and returns `missing`, `conditionalUnset` and `unknownFields` without storing anything (read scope is enough). Fix `unknownFields` — they are almost always typos.
3. **Completeness before publish.** `GET /v1/models/{id}/completeness` lists what is still missing for the model's category. Publishing is refused with `422 publish_blocked` until the model is complete **and** the battery has a `placeOfManufacture`. Dry-run the publish (`POST /v1/passports/publish` with dry run) and read `would_publish` / `blockers` per passport.
4. **Batch, and read per-row results.** `POST /v1/items/batch`, `POST /v1/passports/publish` and the fleet endpoints take up to 1000 rows and answer per row. A 200 does not mean every row succeeded: check `results[].ok` and route failures to a work queue.
5. **Never DELETE.** The API has no delete: passports must stay available for ten years. To end a battery's life use status changes (`PATCH /v1/passports/{id}/status`: `REPURPOSED`, `REUSED`, `REMANUFACTURED`, `WASTE`) or recycling (`POST /v1/passports/{id}/recycle`, after which the public page answers 410). Removing data is done by a person in the app, with an audit trail. Dynamic events are append-only.
6. Unique identifiers are issued by the API. Only pass `uniqueIdentifier` when migrating batteries whose identifier is already printed on them.

## Reading and sync

- Every list is **cursor-paginated**: `limit` (1–200, default 50) and `cursor`; keep requesting while `has_more` is true, passing `next_cursor`. Never compute offsets.
- For incremental sync pass `updated_since` = the start time of your previous successful run (store it only after the run completes). Overlap is fine; dedupe on `id`.
- Prefer webhooks for change notification and use `updated_since` polling as the safety net.

## Webhooks

- Every delivery carries `DPP-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint secret (`whsec_…`, shown once on creation or rotation).
- Verify against the **raw request body bytes**, before any JSON parsing — re-serialised JSON will not match.
- Compare signatures in **constant time** (`crypto.timingSafeEqual`, `hmac.compare_digest`, `hash_equals`, …).
- Reject timestamps more than **300 seconds** from now (replay protection).
- Answer 2xx quickly and process asynchronously. Deliveries are retried (1 min, 5 min, 30 min, 2 h, 6 h), so handlers must be idempotent: dedupe on the event `id`.
- With the TypeScript SDK: `verifyWebhook` from `@collecta/dpp-sdk/webhooks`.

## Errors

- Errors are JSON: `{ "error": "<code>", "message": "…", "details": …, "request_id": "…" }`. Switch on `error` (the code), never on `message`.
- **Log `request_id`** (also in the `X-Request-Id` header) with every failure; quote it to support.
- Retry only network errors, `429`, `5xx` and `409 idempotency_in_progress` — with the **same** Idempotency-Key, exponential backoff with jitter, and `Retry-After` honoured. Never retry other 4xx unchanged: fix the request (`400 validation_failed` → read `details`).
- Full list: https://www.collectadpp.com/developers/error-codes

## Rate limits

- 600 requests per minute per live key, 60 per test key. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`.
- On `429`, wait for `Retry-After` seconds. Use batch endpoints instead of loops of single calls.

## Tools

- TypeScript SDK `@collecta/dpp-sdk` (coming soon to npm): typed, automatic idempotency keys, retries, pagination iterators, dry runs, webhook verification. Prefer it in TypeScript/JavaScript code over hand-written HTTP.
- MCP server `@collecta/dpp-mcp` (coming soon to npm): lets a coding agent read models, completeness, passports and the field catalogue directly, read-only by default. Setup: https://www.collectadpp.com/developers/ai-agents
- Docs pages can be copied as Markdown (the "Copy page" button) for pasting into an agent.

## Checklist before merging

- [ ] Key from environment/secret manager; test key in development and CI.
- [ ] Idempotency-Key on every POST/PATCH, stable across retries.
- [ ] Models validated before create; completeness checked before publish.
- [ ] Per-row results of batch calls checked; failures handled.
- [ ] No DELETE anywhere; end of life via status change or recycle.
- [ ] Lists follow `next_cursor`; sync uses `updated_since`.
- [ ] Webhooks verified on the raw body, constant-time, 300 s tolerance, deduped on event id.
- [ ] `request_id` logged on every error; only retryable errors retried.
