---
title: Build an EU DPP Registry upload file for battery passports
description: Build the XML file the economic operator uploads on the EU DPP Registry website, for up to 100 battery passports. Only passing passports go in. Paid plans.
canonical: "https://www.tracepass.eu/docs/registry-upload-file"
locale: en
source: "https://www.tracepass.eu/docs/registry-upload-file"
---

# Build an EU DPP Registry upload file for battery passports

> Build the XML file the economic operator uploads on the EU DPP Registry website, for up to 100 battery passports. Only passing passports go in. Paid plans.

```http
POST /api/v1/registry/upload-file
```

Builds the XML file the economic operator uploads on the EU DPP Registry website (DPP Registration → upload a file) for the **battery** passports you name, up to 100 per call. Registration is the operator's own act under its own EU Login; TracePass prepares the file and never submits it.

**Only passports that would pass go in.** The Registry fails the whole file on one bad entry, so every other passport comes back in `excluded` with its `reason`: `not_found`, `not_battery`, `not_published`, `registered_in_production`, `registration_in_progress` (being sent to an EN 18222 registry), `readiness_blocked` (the blocking findings are in `details`), `invalid_upi`, `duplicate_upi` (the same address as another passport in the file) or `over_limit` (past 100). `included` lists the passports in the file, each with its address.

Each included passport's address is frozen on the passport (`registryUpi`) and the preparation recorded (`registrationPreparedVia: "file"`). Save `xml` under `filename` and upload it unchanged: the name uses only the characters the Registry accepts. A preparation is not a registration: record the Registry's answer for each passport with `POST /api/v1/passports/{id}/registration`.

The response is JSON rather than raw XML, so it replays under `Idempotency-Key` and carries the exclusions. **Paid plans only** (`402` on Free). OAuth callers must act for an admin or owner. Counts one v1 write per passport included.

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Authorization` | header | string | yes | `Bearer <token>` — either a `tp_` API key (Developer → API Keys; simplest, for server-to-server) or an OAuth 2.0 access token (Developer → OAuth Apps; for user-authorized apps, scoped + revocable). The Authentication page has the full OAuth flow and scope list. |
| `Idempotency-Key` | header | string | no | UUID v4. |
| `passportIds` | body | string\[\] (24 hex chars each, 1–100) | yes | The passports to put in the file. Passports that would not pass are returned in `excluded` rather than failing the call. |

## Examples

```bash
curl -sS -X POST \
  https://app.tracepass.eu/api/v1/registry/upload-file \
  -H "Authorization: Bearer tp_REDACTED_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5a8d2f17-9c4e-4b3a-8e61-0f2c7d9b4e35" \
  -d '{ "passportIds": ["6650b2c3d4e5f6a7b8c9d0e1", "6650b2c3d4e5f6a7b8c9d0e2"] }'
```

```typescript
import { writeFile } from "node:fs/promises";

const res = await fetch("https://app.tracepass.eu/api/v1/registry/upload-file", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TRACEPASS_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({ passportIds }),
});
const { xml, filename, included, excluded } = await res.json();

// Upload this file unchanged on the EU DPP Registry website.
await writeFile(filename, xml);
for (const e of excluded) console.log("left out:", e.id, e.reason, e.details ?? []);
console.log(`${included.length} passport(s) in ${filename}`);
```

```python
import os, uuid, requests
res = requests.post(
    "https://app.tracepass.eu/api/v1/registry/upload-file",
    headers={
        "Authorization": f"Bearer {os.environ['TRACEPASS_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"passportIds": passport_ids},
)
res.raise_for_status()
body = res.json()
with open(body["filename"], "w", encoding="utf-8") as f:
    f.write(body["xml"])  # upload this file unchanged
for e in body["excluded"]:
    print("left out:", e["id"], e["reason"], e.get("details", []))
```

## Responses

### 200 — Upload file

```json
{
  "xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n…",
  "filename": "acme-batteries-dpp-registry-batch-2026-11-06.xml",
  "included": [
    {
      "id": "6650b2c3d4e5f6a7b8c9d0e1",
      "upi": "https://id.tracepass.eu/01/04012345000016/21/A1B2C3"
    }
  ],
  "excluded": [
    {
      "id": "6650b2c3d4e5f6a7b8c9d0e2",
      "reason": "readiness_blocked",
      "details": [
        "Required field \"Battery Carbon Footprint — Total\" has no value."
      ]
    }
  ]
}
```

### 400 — Validation error

```json
{
  "error": "Validation failed",
  "details": { "passportIds": ["Too small: expected array to have >=1 items"] }
}
```

### 402 — Free plan

```json
{ "error": "Available on every paid plan. Upgrade to use it." }
```

## Related

- [Record the Registry result](https://www.tracepass.eu/docs/record-passport-registration.md)
- [Get the address for one passport](https://www.tracepass.eu/docs/passport-registry-upi.md)
- [Check registry readiness](https://www.tracepass.eu/docs/passport-registry-readiness.md)
