The Vikka API — EU Battery Regulation 2023/1542 Digital Product Passports. Authenticated endpoints use an API key (Authorization: Bearer tk_...); the QR code and public passport view need no auth. Every response carries an `X-Request-Id` header. Errors share one shape: `{ "error": { "type", "code", "message", "param?", "request_id" } }`. Passport objects carry a `type` discriminator (`battery` today) so the platform can add more product types without breaking the contract.
Machine-readable spec: /openapi.json (OpenAPI 3.1.0). Import it into Postman, Insomnia, or a code generator.
Authenticated endpoints take an API key. Include it on every request:
curl https://vikka.io/api/v1/passports \ -H "Authorization: Bearer tk_your_api_key"
Keys are scoped to your organization and carry scopes, read and/or write: reads need read, creates and updates need write. Requests are rate-limited to 60/minute per key; every response carries X-RateLimit-* headers, and a 429 returns Retry-After. Public endpoints (the QR code and the public passport view) need no key.
Every response carries an X-Request-Id header (also echoed in error bodies as request_id). Quote it when contacting support. Passport objects carry a type discriminator ("battery" today) and a links block (self, public, qr, digital_link).
Create your first passport. One request, four languages, all equivalent to POST /api/v1/passports.
curl https://vikka.io/api/v1/passports \
-H "Authorization: Bearer $VIKKA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "PowerCell EV-200",
"battery_category": "EV",
"chemistry": "NMC 811",
"capacity": 82.5,
"serial_number": "BT-2027-004182"
}'const res = await fetch("https://vikka.io/api/v1/passports", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.VIKKA_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "PowerCell EV-200",
"battery_category": "EV",
"chemistry": "NMC 811",
"capacity": 82.5,
"serial_number": "BT-2027-004182"
}),
})
const passport = await res.json()import os, requests
passport = requests.post(
"https://vikka.io/api/v1/passports",
headers={"Authorization": "Bearer " + os.environ["VIKKA_API_KEY"]},
json={
"name": "PowerCell EV-200",
"battery_category": "EV",
"chemistry": "NMC 811",
"capacity": 82.5,
"serial_number": "BT-2027-004182"
},
).json()body, _ := json.Marshal(map[string]any{
"name": "PowerCell EV-200",
"battery_category": "EV",
"chemistry": "NMC 811",
"capacity": 82.5,
"serial_number": "BT-2027-004182"
})
req, _ := http.NewRequest("POST", "https://vikka.io/api/v1/passports", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("VIKKA_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req){
"object": "passport",
"type": "battery",
"id": "8c2f4a1e-93d7-4b6a-b1c4-2f0e7d9a51b3",
"status": "active",
"created": "2026-07-11T15:04:00.000Z",
"name": "PowerCell EV-200",
"battery_category": "EV",
"chemistry": "NMC 811",
"capacity": 82.5,
"serial_number": "BT-2027-004182",
"links": {
"self": "https://vikka.io/api/v1/passports/8c2f4a1e-93d7-4b6a-b1c4-2f0e7d9a51b3",
"public": "https://vikka.io/api/passports/8c2f4a1e-93d7-4b6a-b1c4-2f0e7d9a51b3/public",
"qr": "https://vikka.io/api/qr?id=8c2f4a1e-93d7-4b6a-b1c4-2f0e7d9a51b3",
"digital_link": "https://vikka.io/01/07312345678909/21/BT-2027-004182"
}
}Create, retrieve and list return the same object: a Stripe-style envelope (object, type, id, status, timestamps) around the Annex XIII fields, with a links block. The type discriminator is "battery" today; the platform will add more product types without changing this contract.
{
"object": "passport",
"type": "battery",
"id": "7e8e0d80-2f1a-4b6a-b1c4-2f0e7d9a51b3",
"status": "active",
"created": "2026-07-11T15:04:00.000Z",
"updated": "2026-07-11T15:04:00.000Z",
"name": "PowerCell EV-200",
"manufacturer": "Vossberg Power",
"battery_category": "EV",
"chemistry": "NMC 811",
"capacity": 82.5,
"carbon": 61.2,
"links": {
"self": "https://vikka.io/api/v1/passports/7e8e0d80-…",
"public": "https://vikka.io/api/passports/7e8e0d80-…/public",
"qr": "https://vikka.io/api/qr?id=7e8e0d80-…",
"digital_link": "https://vikka.io/01/07312345678909/10/LOT/21/SN"
}
}Programmatic passport access (API key).
List passports
List the authenticated org’s passports. Requires the `read` scope.
Create a passport
Create a passport in the authenticated org. Requires the `write` scope. Server-owned fields are ignored.
Get a passport
Update a passport
Update fields on a passport. Requires the `write` scope. Server-owned fields are ignored.
Compliance readiness for a passport
The EU Battery Regulation readiness of a passport, scored by the compliance engine. Requires the `read` scope.
Export the full org dataset (portability)
The portability / continuity exit route (EU 2023/1542 Art. 77(5), no vendor lock-in). Returns your ENTIRE org dataset in one self-describing JSON document — every passport in the owner-tier API shape, plus a field catalogue — as a downloadable attachment. Requires the `read` scope. A customer can trigger this at any time; their data can never be trapped.
Unauthenticated public reads.
Public passport view
The no-auth, field-scoped public read of a passport — only the public Annex XIII fields, the same data behind the QR code. No API key.
Look up a passport by product identifier
Resolve a unique product identifier — a GS1 Digital Link URI, a /01/… path, a bare GTIN, or a passport UUID — to its current passport. Responds 307 to the public passport JSON; standard HTTP clients follow it, so a GET returns the DPP. No auth; rate limited per IP; exact match only.
GS1 Digital Link resolver
Resolves a GS1 Digital Link — `/01/{gtin}[/10/{batch}][/21/{serial}]` — the URL a battery QR encodes. By default (a browser) it 307-redirects to the public passport page, forwarding the query string. With `Accept: application/linkset+json` or `?linkType=linkset` (or `all`) it returns an RFC 9264 linkset of the product’s links (default link + product-information page + machine-readable passport data). A malformed GTIN returns 400. No auth; CORS-open.
Passport QR code
A PNG QR code encoding the passport’s GS1 Digital Link. No auth; rate limited per IP.
Every passport is addressable by its GS1 Digital Link, the URL a battery QR encodes, and the same string the API returns as links.digital_link:
https://vikka.io/01/{gtin}[/10/{batch}][/21/{serial}]The resolver at /01/… is GS1-conformant: it passes the official GS1 Resolver Test Suite (verified 2026-07-16). It content-negotiates:
The linkset exposes public-tier links only: defaultLink and pip (the human passport page) and epil (the machine-readable public JSON read). Do not expect owner-tier data here. A valid qualifier with an unknown value (e.g. an unseen serial) gracefully walks up to the GTIN-level entity rather than 404-ing.
curl -H "Accept: application/linkset+json" https://vikka.io/01/07312345678909
The resolver describes its own capabilities at /.well-known/gs1resolver (supported keys and link types). To resolve any identifier form to a passport in one call (a Digital Link URI, a /01/… path, a bare GTIN, or a passport UUID), use GET /api/passports/lookup.
Fields tracked per EU Battery Regulation 2023/1542, Annex XIII. All are optional on write; only name is required to create.
Every error returns the same envelope, so you can branch on error.type and error.code. param names the offending field when there is one, and a validation error adds an issues array.
{
"error": {
"type": "invalid_request_error",
"code": "validation_error",
"message": "Request failed validation.",
"param": "name",
"request_id": "req_8f3c1e0a…"
}
}The type is one of: