API Reference

Vikka API v1.0.0

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.

Authentication

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).

Quickstart

Create your first passport. One request, four languages, all equivalent to POST /api/v1/passports.

curl
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"
  }'
TypeScript
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()
Python
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()
Go
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)
201 Created
{
  "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"
  }
}

The passport object

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"
  }
}

Passports

Programmatic passport access (API key).

GET/api/v1/passportsAPI key

List passports

List the authenticated org’s passports. Requires the `read` scope.

Parameters
manufacturerstring? · queryFilter by manufacturer (contains, case-insensitive)
categorystring? · queryFilter by battery_category
limitinteger? · queryMax results (1–200)
offsetinteger? · queryPagination offset
Responses
200PassportListA page of passports
401ErrorMissing or invalid API key
403ErrorInsufficient scope
429ErrorRate limit exceeded
500ErrorQuery failed
POST/api/v1/passportsAPI key

Create a passport

Create a passport in the authenticated org. Requires the `write` scope. Server-owned fields are ignored.

Request body
name *stringProduct name (required)
manufacturerstring
manufacturer_addressstring
manufacturer_contactstring
countrystringCountry of origin
battery_category"EV" | "Industrial" | "LMT" | "Portable" | "SLI"Battery category: EV, Industrial, LMT, Portable or SLI
battery_modelstring
serial_numberstring
batch_idstring
gtinstringGTIN / barcode number
chemistrystringCell chemistry (free text; e.g. NMC, LFP, NCA, LTO)
weight_kgnumber | null
capacitynumber | nullEnergy capacity (kWh)
rated_capacity_ahnumber | null
nominal_voltagenumber | null
carbonnumber | nullCarbon footprint (kg CO2e/kWh)
carbon_methodstring
rc_cobaltnumber | null
rc_lithiumnumber | null
rc_nickelnumber | null
rc_leadnumber | null
warrantynumber | null
expected_lifetime_yearsnumber | null
expected_lifetime_cyclesnumber | null
sohnumber | nullState of health (%)
dismantling_infostring
recycling_instructionsstring
due_diligence_policystring
Responses
201PassportThe created passport
400ErrorInvalid JSON
401ErrorMissing or invalid API key
403ErrorInsufficient scope
422ErrorValidation error
429ErrorRate limit exceeded
500ErrorInsert failed
GET/api/v1/passports/{id}API key

Get a passport

Parameters
idstring · pathPassport id
Responses
200PassportThe passport
401ErrorMissing or invalid API key
403ErrorInsufficient scope
404ErrorNot found
429ErrorRate limit exceeded
PUT/api/v1/passports/{id}API key

Update a passport

Update fields on a passport. Requires the `write` scope. Server-owned fields are ignored.

Parameters
idstring · pathPassport id
Request body
namestringProduct name
manufacturerstring
manufacturer_addressstring
manufacturer_contactstring
countrystringCountry of origin
battery_category"EV" | "Industrial" | "LMT" | "Portable" | "SLI"Battery category: EV, Industrial, LMT, Portable or SLI
battery_modelstring
serial_numberstring
batch_idstring
gtinstringGTIN / barcode number
chemistrystringCell chemistry (free text; e.g. NMC, LFP, NCA, LTO)
weight_kgnumber | null
capacitynumber | nullEnergy capacity (kWh)
rated_capacity_ahnumber | null
nominal_voltagenumber | null
carbonnumber | nullCarbon footprint (kg CO2e/kWh)
carbon_methodstring
rc_cobaltnumber | null
rc_lithiumnumber | null
rc_nickelnumber | null
rc_leadnumber | null
warrantynumber | null
expected_lifetime_yearsnumber | null
expected_lifetime_cyclesnumber | null
sohnumber | nullState of health (%)
dismantling_infostring
recycling_instructionsstring
due_diligence_policystring
Responses
200PassportThe updated passport
400ErrorInvalid JSON
401ErrorMissing or invalid API key
403ErrorInsufficient scope
404ErrorNot found
422ErrorValidation error
429ErrorRate limit exceeded
500ErrorUpdate failed
GET/api/v1/passports/{id}/complianceAPI key

Compliance readiness for a passport

The EU Battery Regulation readiness of a passport, scored by the compliance engine. Requires the `read` scope.

Parameters
idstring · pathPassport id
Responses
200ComplianceThe compliance result
401ErrorMissing or invalid API key
403ErrorInsufficient scope
404ErrorNot found
422ErrorThe stored battery_category is not a known category (fix it via PUT)
429ErrorRate limit exceeded
500ErrorScoring failed
GET/api/v1/exportAPI key

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.

Responses
200A JSON export document (attachment)
401ErrorMissing or invalid API key
403ErrorInsufficient scope
429ErrorRate limit exceeded
500ErrorExport failed

Public

Unauthenticated public reads.

GET/api/passports/{id}/publicPublic

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.

Parameters
idstring · pathPassport id
Responses
200PublicPassportThe public passport fields
404ErrorNot found
GET/api/passports/lookupPublic

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.

Parameters
product_idstring · queryA unique product identifier: a GS1 Digital Link URI, a /01/… path, a bare GTIN, or a passport UUID.
Responses
307Redirect to the public passport JSON (/api/passports/{id}/public)
400ErrorUnrecognized product identifier
404ErrorNo passport matches this identifier
429ErrorRate limit exceeded
503ErrorLookup temporarily unavailable (DB outage)
GET/01/{gs1Path}Public

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.

Parameters
gs1Pathstring · pathGTIN, optionally followed by /10/{batch} and /21/{serial}
linkTypestring? · query`linkset` (or `all`) returns the full linkset; a specific GS1 term (`pip`, `epil`, `defaultLink`) 307-redirects to that link, or 404 if not exposed
Responses
200application/linkset+jsonThe RFC 9264 linkset (when a linkset is requested)
307Default (or a specific linkType): redirect to the target link
400Malformed Digital Link URI (invalid GTIN or qualifier)
404No passport matches this code, or the requested linkType is not exposed
503Lookup temporarily unavailable (DB outage)
GET/api/qrPublic

Passport QR code

A PNG QR code encoding the passport’s GS1 Digital Link. No auth; rate limited per IP.

Parameters
idstring · queryPassport id
Responses
200image/pngPNG image
400ErrorMissing id
404ErrorPassport not found
429ErrorRate limit exceeded

GS1 Digital Link resolver

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:

default (a browser)307Redirects to the public passport page; the incoming query string is forwarded
Accept: application/linkset+json200Returns an RFC 9264 linkset (no redirect)
?linkType=linkset (or all)200Same linkset, via the query parameter
?linkType=pip | epil | defaultLink307Redirects to that one link; a link type we do not expose returns 404

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.

Field reference (Annex XIII)

Fields tracked per EU Battery Regulation 2023/1542, Annex XIII. All are optional on write; only name is required to create.

General Information
name · battery_model · battery_category · chemistry · serial_number · batch_id · gtin · weight_kg · manufacturer · manufacturer_address · manufacturer_contact · country · manufacturing_date · manufacturing_place · hazardous_substances
Compliance
eu_declaration_of_conformity · conformity_body · test_report_ref
Carbon Footprint
carbon · carbon_method · carbon_performance_class · carbon_study_url
Recycled Content
rc_cobalt · rc_lithium · rc_nickel · rc_lead · renewable_content_pct
Performance & Durability
capacity · rated_capacity_ah · nominal_voltage · min_voltage · max_voltage · warranty · expected_lifetime_years · expected_lifetime_cycles · capacity_fade_pct · temp_range_min · temp_range_max · round_trip_efficiency · initial_internal_resistance · soh
Due Diligence
due_diligence_policy · third_party_audit_status · raw_material_sourcing
End-of-Life
collection_scheme · recycling_instructions · dismantling_info

Errors

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:

invalid_request_error400 / 404 / 422Bad JSON, unknown resource, or failed field validation (see param / issues)
authentication_error401Missing, malformed, unrecognized, or revoked API key
permission_error403Key lacks the scope (read for GET, write for POST/PUT)
rate_limit_error429Too many requests; see Retry-After / X-RateLimit-* headers
api_error500Something failed on our side; the request_id helps us trace it
Vikka API v1.0.0 · EU Battery Regulation 2023/1542