← veridyn.eu
HU EN DE

Developer API v1

veridyn

Programmatic access to product passports — for webshop/ERP sync and bulk automation.

From the Pro plan

During the open beta the API is available to every beta account — the Pro requirement applies after the beta.

Authentication

Every request authenticates with an API key, in the Authorization header:

Authorization: Bearer vk_<your key>

You create a key under Settings → Developer API. The plaintext key is shown only once, at creation — store it safely. The key grants read/write access to all products in your account, so keep it confidential.

Quick test in the browser

The key goes only in the Authorization header — not as a URL parameter, for security (it would end up in browser history and server logs). Try it for example with curl:

curl -H "Authorization: Bearer vk_<your key>" https://veridyn.eu/api/v1/categories

Key scope

When creating a key you choose a scope: Full (read + write) or Read-only. A read-only key may only call GET — POST/PATCH/DELETE return 403 read_only. Use a read-only key for ERP or display integrations.

Rate limit

Per key, 120–600 requests / minute depending on plan (Pro 120 · Business 300 · Enterprise 600; 300 during the open beta). Every response includes the X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix time) headers. Exceeding the limit returns 429 rate_limited with a Retry-After header — wait the indicated time, then retry.

Idempotency

You can add a unique Idempotency-Key header to POST requests. If you resend it with the same key (e.g. after a network error), you get back the original response — no duplicate is created. A replayed response is marked with the Idempotency-Replayed: true header.

OpenAPI

Machine-readable API description (OpenAPI 3.1) — for Postman import and SDK/code generation:

https://veridyn.eu/api/v1/openapi.json

⚡ Interactive API reference

Browsable, searchable endpoint reference — rendered directly from the spec above.

DPP vocabulary (JSON-LD namespace)

The machine-readable passport is JSON-LD: standard fields come from schema.org, DPP-specific fields from the dpp: namespace. That namespace is resolvable — open it to see what every field means, or send Accept: application/ld+json to get the machine-readable @context document.

https://veridyn.eu/ns/dpp/v1/

A single passport as data: its public URL with ?format=jsonld (or an Accept: application/ld+json header).

Postman collection

A ready-to-import collection with pre-set variables and example requests. After importing, just fill in the base_url and api_key collection variables.

⬇ Download Postman collection

Generate your own SDK

From the OpenAPI spec you can generate an official client in any language with openapi-generator — no hand-maintained SDK, always up to date. E.g. PHP:

npx @openapitools/openapi-generator-cli generate \
  -i https://veridyn.eu/api/v1/openapi.json \
  -g php -o ./veridyn-sdk

The -g value can be typescript-fetch, python, java and many more.

Quickstart (code)

PHP

$ch = curl_init('https://veridyn.eu/api/v1/products?limit=5');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER     => ['Authorization: Bearer vk_...'],
]);
$products = json_decode(curl_exec($ch), true)['data'];

JavaScript (fetch)

const res = await fetch('https://veridyn.eu/api/v1/products?limit=5', {
  headers: { 'Authorization': 'Bearer vk_...' }
});
const { data } = await res.json();

Base URL

https://veridyn.eu/api/v1

Responses are application/json, UTF-8 encoded.

Sandbox (test account)

Integrations are not built against a live account. Under Settings → Sandbox you can request a separate test account with its own API key: same API, same schemas, fully isolated from your live data.

Endpoints

MethodPathDescription
GET/categoriesAvailable product categories.
GET/schema/{category}The category's full field list — required + optional, type, enum, localized fields.
GET/productsList of products (summary). Parameters: q, status, page, limit — or updated_since and cursor for changes-only sync.
POST/productsNew product passport. Body: { category, data }.
POST/products:batchBulk create — up to 500 products per request. Body: { category, products: [ … ], on_duplicate? }. Returns a per-row report.
GET/products/{id}A product's full data + passport URL.
PATCH/products/{id}Update (partial) — creates a new version. Body: { data, change_type? }. change_type is optional: correction (the model held wrong data → already-issued batch/unit passports become out of date and can be refreshed) or change (the product changed from a given date → earlier units keep their correct data and are not overwritten).
GET/products/{id}/qrQR code (SVG) for the public passport.
GET/eventsHistory of outgoing webhook events (?after_id= & limit=) — for de-duplication and catching up on missed events.
GET/products/{id}/childrenList of the batch/unit child passports.
POST/products/{id}/childrenBatch/unit passport. Single: { level, lot|serial, data } · bulk: { level, items:[…] } (max 500). Inherits the parent's data.
PATCH/products/{id}/childrenBulk update of child passports (max 500): { items:[{ serial|lot|id, data }] }. For live BMS data — with a per-row report.
POST/products/{id}/archive
/products/{id}/restore
Archive and restore. Deliberately not a PATCH: a visibility change is not a data change, so it creates no empty version in the change history. No body needed.
GET/products/{id}/versions
/products/{id}/versions/{no}
The change history: the list of versions, or a given version's field-level diff (what changed, from what to what).
GET
POST
DELETE
/products/{id}/grants
/products/{id}/grants/{gid}
Expiring access links for an authority / a party with legitimate interest — reaching confidential fields without a login. DELETE revokes one.
GET
POST
DELETE
/products/{id}/documents
/products/{id}/documents/{docId}
Certificates and documents. Upload in base64 (PDF, PNG, JPEG, WebP), download the file, delete. The GET with a document id returns the file, not JSON.
GET
POST
/supplier-requestsSupplier data requests: list and create. The response contains the supplier link.
GET
POST
DELETE
/supplier-requests/{rid}
/supplier-requests/{rid}/merge
One request with its submitted values, merging the submission (new product version), or cancelling it.
POST/eo/tokenA short-lived JWT for the standards-based economic operator interface. Body: { ttl? }.
GET/products/{id}/scansPer-product scan summary: total, unique, byCountry, byDevice. Parameter: days=7|30|90|365.
GET/products/{id}/batterypassThe battery passport in CEN-CENELEC JTC24 (BatteryPass) form. Parameter: profile=feb27|full — feb27 is the narrower set binding from 2027-02-18, full is the complete data model. Battery category only.
GET/scansTenant-wide scan summary: the above + byCategory, byLevel.

What to send? (required vs. optional)

Every category has a required core — without it, schema validation rejects the request (422). All other fields are optional, but you can send them in the same data object anytime (more data = a better passport and more future-proof compliance). The exact, machine-readable field list is returned by /schema/{category} — with required: true/false, type, enum and localized markers.

curl https://veridyn.eu/api/v1/schema/textile \
  -H "Authorization: Bearer vk_<key>"

All fields by category

✓ = required · — = optional · ↳ nested sub-field · 🌐 per-language (localized) · 🔒 visible only at authority level. This list is generated from the schema — always up to date.

textile Textile & apparel20 field

FieldTypeReq.Description
productNamestring✓Product name — The product name as the buyer will see it on the passport.
skustring✓SKU — Internal item number / identifier — from your own system.
brandNamestring✓Brand
gtinstring—GTIN (optional) — The barcode number (GTIN-8/12/13/14). The system validates the check digit. Leave empty if you don't have one.
commodityCodestring—Commodity code (CN/HS, optional) — Customs/tariff classification: Combined Nomenclature (CN, 8 digits) or HS code. Mandatory metadata of the EU DPP register at registration — it appears on your supplier/customs documents.
economicOperatorobject✓Economic operator
  ↳ rolestring (enum)✓Role — Who places the product on the EU market and is responsible for it. For most manufacturers: Manufacturer. (manufacturer · importer · authorized_representative · distributor · dealer · fulfilment_service_provider)
  ↳ legalNamestring✓Legal name
  ↳ addressstring✓Address
  ↳ countrystring✓Country (ISO 3166-1 alpha-2) — Two-letter ISO country code, in capitals — e.g. HU, DE, IT
  ↳ contactEmailstring✓Contact email — This is where the buyer or an authority can turn.
  ↳ operatorId 🔒string—Unique operator ID (GS1 GLN) — 13-digit GS1 GLN — the EU DPP unique economic operator identifier. Visible at the authorities access level only. Leave empty if you do not have one.
  ↳ eori 🔒string—EORI number — The economic operator's EORI number (customs / EU DPP registry identifier) — the registry identifies you by this. Visible only at authority access level. Leave empty if you don't have one.
languagesarray[string]✓Languages (BCP-47) — Which languages the passport should appear in. The localised fields (care, life cycle) must be filled in for every language listed here. e.g. hu, en
fiberCompositionarray[object]✓Fibre composition — What the product is made of. The percentages must add up to exactly 100. e.g. cotton 95 + elastane 5
  ↳ fiberstring✓Fibre
  ↳ percentagenumber✓Percentage
recycledContentPercentagenumber—Recycled content — The share of recycled content in the product. Leave empty if not relevant.
countryOfManufacturestring✓Country of manufacture (ISO) — Where the finished product was made. Two-letter ISO code, e.g. PT, TR, HU
supplyChainStagesarray[object]—Supply chain stages — Where each production step took place (spinning, weaving, dyeing, assembly…). Optional, but it builds trust. The step and the country are public; the facility name and ID are protected — only a legitimate interest party (and authorities) can see them, via a token link.
  ↳ stagestring (enum)✓Stage (spinning · weaving · knitting · dyeing · finishing · assembly)
  ↳ countrystring✓Country (ISO)
  ↳ facilityName 🔒string—Facility name
  ↳ facilityId 🔒string—Unique facility ID (GS1 GLN)
careInstructions 🌐object✓Care instructions — Washing, drying, ironing — one text per language.
repairobject✓Repairability — Whether and how it can be repaired. The “repairable” and “spare parts available” fields are mandatory.
  ↳ repairableboolean✓Repairable
  ↳ instructions 🌐object—Repair instructions — How it can be repaired — per language (optional).
  ↳ sparePartsAvailableboolean✓Spare parts available (e.g. button, zip)
substancesOfConcernarray[object]—Substances of concern — Substances of concern (e.g. REACH SVHC), if present in the product. Can be left empty for most products.
  ↳ namestring✓Name
  ↳ casNumberstring—CAS number
  ↳ concentrationRangestring—Concentration range
durabilityobject—Durability — Durability data, if you have measurements (optional).
  ↳ testResultsstring—Test results
  ↳ pefScorenumber—PEF score — Product Environmental Footprint score, if available. (optional)
carbonFootprintnumber—Carbon footprint (kg CO₂e) — The product's total carbon footprint in kg CO₂ equivalent. Shown in the “Impact” theme as an “≈ km by car” comparison.
waterFootprintnumber—Water footprint (litres) — The amount of water used in manufacturing, in litres. Optional.
weightGramsnumber—Weight (grams) — The product's weight in grams. Optional.
endOfLifeobject✓End of life — What should happen to the product after use — filled in for each language.
  ↳ recyclingInstructions 🌐object✓Recycling instructions — How it can be recycled — per language.
  ↳ disposalInstructions 🌐object✓Disposal instructions — If it is not recyclable — per language.
complianceDocumentsarray[object]—Compliance documents — Links to certificates and conformity documents (optional). e.g. OEKO-TEX, GOTS.
  ↳ typestring✓Type
  ↳ urlstring✓URL

battery Battery105 field

FieldTypeReq.Description
productNamestring✓Product name — The battery name as the buyer will see it on the passport.
skustring✓SKU — Internal item number / identifier — from your own system.
brandNamestring✓Brand
gtinstring—GTIN (optional) — The barcode number (GTIN-8/12/13/14). The system validates the check digit. Leave empty if you don't have one.
commodityCodestring—Commodity code (CN/HS, optional) — Customs/tariff classification: Combined Nomenclature (CN, 8 digits) or HS code. Mandatory metadata of the EU DPP register at registration — it appears on your supplier/customs documents.
economicOperatorobject✓Economic operator
  ↳ rolestring (enum)✓Role — Who places the product on the EU market and is responsible for it. For most manufacturers: Manufacturer. (manufacturer · importer · authorized_representative · distributor · dealer · fulfilment_service_provider)
  ↳ legalNamestring✓Legal name
  ↳ addressstring✓Address
  ↳ countrystring✓Country (ISO 3166-1 alpha-2) — Two-letter ISO country code, in capitals — e.g. HU, DE, IT
  ↳ contactEmailstring✓Contact email — This is where the buyer or an authority can turn.
  ↳ operatorId 🔒string—Unique operator ID (GS1 GLN) — 13-digit GS1 GLN — the EU DPP unique economic operator identifier. Leave empty if you do not have one.
  ↳ eori 🔒string—EORI number — The economic operator's EORI number (customs / EU DPP registry identifier) — the registry identifies you by this. Visible only at authority access level. Leave empty if you don't have one.
languagesarray[string]✓Languages (BCP-47) — Which languages the passport should appear in. The localised fields (safety, life cycle) must be filled in for every language listed here. e.g. hu, en
batteryCategorystring (enum)✓Battery category — The category under (EU) 2023/1542. The mandatory passport applies first to EV, industrial (>2 kWh) and LMT batteries (18 February 2027). (portable · lmt · ev · industrial · sli)
cellChemistrystring (enum)✓Cell chemistry — Cell chemistry. The list follows the CEN-CENELEC JTC24 code list; "LTO" and "Other" are not in the official list, so a BatteryPass registration may fail with them. (nmc · nca · lfp · lmo · lto · nimh · lead_acid · sodium_ion …)
weightKgnumber✓Weight (kg) — The battery's weight in kilograms.
countryOfManufacturestring✓Country of manufacture (ISO) — Where the battery was made. Two-letter ISO code, e.g. DE, HU, CN
manufacturingDatestring✓Manufacturing date — Exact manufacturing date in ISO format (YYYY-MM-DD). The JTC24 data model requires a full date — a year or year-month value will fail BatteryPass registration.
ratedCapacitynumber✓Rated capacity (Ah) — Nominal capacity in ampere-hours (Ah).
energyWhnumber—Energy (Wh) — Total energy content in watt-hours (Wh). Optional.
nominalVoltagenumber—Nominal voltage (V) — Nominal voltage in volts. Optional.
expectedLifetimeCyclesnumber—Expected lifetime (charge cycles) — The guaranteed / expected total number of charging cycles. BatteryPass expects an integer, so a fractional value appears rounded in the JTC24 export. Optional.
stateOfHealth 🔒number—State of Health — The battery's state of health as a % of the original capacity (100 for a new battery). UNIT-SPECIFIC and dynamic — its ideal place is the unit level (not the model template). Optional.
remainingCapacity 🔒number—Remaining capacity (Ah) — Current capacity of the individual unit — unlike rated capacity, this declines over time. Required by the LMT profile, not by EV.
remainingPowerAt80Soc 🔒number—Remaining power capability at 80% SoC (W) — Counterpart of original power: the CURRENT value at 80% state of charge.
remainingPowerAt20Soc 🔒number—Remaining power capability at 20% SoC (W) — The same at 20% state of charge. JTC24 requires both together.
minimumVoltagenumber—Minimum voltage (V) — Lowest voltage at which the battery still operates.
maximumVoltagenumber—Maximum voltage (V) — Highest permitted operating voltage.
originalPowerAt80Socnumber—Original power capability at 80% SoC (W) — JTC24 requires original power at TWO states of charge; this is the 80% value.
originalPowerAt20Socnumber—Original power capability at 20% SoC (W) — Original power at 20% state of charge. Required together with the 80% value in JTC24.
maximumPermittedBatteryPowernumber—Maximum permitted battery power (W) — Peak power permitted by the manufacturer.
initialRoundTripEfficiencynumber—Initial round-trip energy efficiency (%) — Ratio of energy recovered to energy stored, when new.
roundTripEfficiencyAt50PctCycleLifenumber—Round-trip energy efficiency at 50% of cycle life (%) — The same efficiency at half the rated cycle count — it shows the rate of degradation.
initialInternalResistanceCellnumber—Initial internal resistance — cell (Ω) — JTC24 requires internal resistance separately for CELL and PACK; this is the cell value.
initialInternalResistancePacknumber—Initial internal resistance — pack (Ω) — Initial internal resistance of the complete battery pack.
cycleLifeReferenceTeststring—Cycle life reference test — Which standardised test produced the cycle count.
cRateOfCycleLifeTestnumber—C-rate of cycle life test (A/Ah) — Test charge/discharge current relative to capacity. A NUMBER is expected: write 1 for 1C.
capacityThresholdForExhaustionnumber—Capacity threshold for exhaustion (%) — Below what share of original capacity the battery counts as exhausted.
temperatureRangeIdleLowernumber—Idle-state temperature range — lower boundary (°C) — Lowest temperature the battery withstands when not in use. May be negative.
temperatureRangeIdleUppernumber—Idle-state temperature range — upper boundary (°C) — Highest temperature the battery withstands when not in use.
stateOfChargeSoC 🔒number—State of Charge — SoC (%) — Current charge level. ITEM-SPECIFIC and continuously changing.
stateOfCertifiedEnergy 🔒number—State of Certified Energy — SOCE (%) — Remaining accessible energy relative to certified energy.
capacityFade 🔒number—Capacity fade (%) — How much of the original capacity has been lost.
powerFade 🔒number—Power fade (%) — How much of the original power capability has been lost.
roundTripEfficiencyFade 🔒number—Round-trip efficiency fade (%) — Decline of round-trip efficiency relative to its initial value.
roundTripEfficiencyNow 🔒number—Round-trip efficiency — current (%) — The unit's CURRENT round-trip efficiency. The Commission guidance (v2.0) asks for the instantaneous value, not only the fade; the fade is computable from this and the initial value. JTC24 has no such field, hence no standard path.
internalResistanceIncreasePack 🔒number—Internal resistance increase — pack (%) — Increase of pack internal resistance versus initial. JTC24 requires only the PACK value here.
internalResistanceCell 🔒number—Internal resistance — cell, current (Ω) — CURRENT internal resistance of the cell. The Commission guidance (v2.0) asks for the instantaneous ohm value; the increase is computable from this and the initial value. JTC24 knows only the initial value and the increase, hence no standard path.
internalResistanceModule 🔒number—Internal resistance — module, current (Ω) — CURRENT internal resistance of the module. The Commission guidance (v2.0) asks for the instantaneous ohm value; the increase is computable from this and the initial value. JTC24 knows only the initial value and the increase, hence no standard path.
internalResistancePack 🔒number—Internal resistance — pack, current (Ω) — CURRENT internal resistance of the pack. The Commission guidance (v2.0) asks for the instantaneous ohm value; the increase is computable from this and the initial value. JTC24 knows only the initial value and the increase, hence no standard path.
selfDischargeRate 🔒number—Self-discharge rate (%/month) — How much charge the unit loses while not in use, per month. The Commission guidance (v2.0) asks for the EVOLUTION of self-discharge for LMT batteries; the evolution follows from the periodically updated value plus the passport's version history. The JTC24 EV profile has no such field.
fullCycleCount 🔒integer—Number of full charging/discharging cycles — How many full cycles the battery has run so far. ITEM-SPECIFIC.
expectedLifetimeYears 🔒number—Expected lifetime (calendar years) — Expected lifetime in years — the other lifetime measure besides the cycle count.
temperatureInformation 🔒number—Temperature information (°C) — JTC24 expects a single °C value here (typically the average or reference operating temperature).
accidentInfoUrl 🔒string—Information on accidents (link) — Link to documentation of accidents or exceptional events affecting the battery.
carbonFootprintnumber—Carbon footprint (kg CO₂e / kWh) — The battery's carbon footprint over the full life cycle, in kg CO₂ equivalent per kWh of total energy.
carbonFootprintClassstring—Carbon footprint class (A–G) — The CF performance class under the regulation, if available. Optional.
carbonFootprintStudyUrlstring—Carbon footprint study (URL) — Link to the study / documentation underlying the carbon footprint calculation (per the CF delegated act). Optional.
carbonFootprintBreakdownarray[object]—Carbon footprint by lifecycle stage — Breakdown of the carbon footprint by life cycle stage (kg CO₂e / kWh) — this is what the CF delegated act expects. Optional.
  ↳ stagestring (enum)✓Lifecycle stage (raw_material · main_production · distribution · recycling)
  ↳ valuenumber✓Value (kg CO₂e / kWh)
recycledContentarray[object]—Recycled raw-material content — The share of recycled critical raw materials per material (cobalt, lithium, nickel, lead). Optional, but the regulation expects it ever more strictly.
  ↳ materialstring (enum)✓Material (cobalt · lithium · nickel · lead)
  ↳ percentagenumber✓Recycled share
hazardousSubstancesarray[object]—Hazardous substances — Hazardous substances present in the battery (beyond mercury, cadmium and lead). Relevant for most data sheets.
  ↳ namestring✓Name
  ↳ casNumberstring—CAS number
  ↳ concentrationRangestring—Concentration range
  ↳ concentrationnumber—Concentration (%) — Concentration as a NUMBER. The range above is for human reading; JTC24 requires a number.
  ↳ hazardPhrasesarray[string]—Hazard statements (H) — CLP hazard statement codes, one per row.
  ↳ precautionaryPhrasesarray[string]—Precautionary statements (P) — CLP precautionary statement codes, one per row.
  ↳ substanceLocationstring—Substance location — Which part of the battery contains the substance.
criticalRawMaterialsarray[string]—Critical raw materials — Names of critical raw materials present in the battery, one per row. Different from recycled content: that is a share, this is mere presence.
cathodeAnodeElectrolyteMaterials 🔒array[string]—Cathode, anode and electrolyte materials — Materials used in the main cell components, listed one per row.
safetyInformation 🌐object—Safety information — Handling, storage and emergency information — one text per language.
dueDiligenceUrlstring—Supply chain due diligence report (URL) — Link to the due diligence policy/report required by the regulation. DELIBERATELY PUBLIC: the battery regulation requires the report to be published, and JTC24 also classifies it as `public` access. (Article 77(4) applies to the other, restricted fields — not to this one.) Optional.
endOfLifeobject✓End of life — Collection, recycling, disassembly — filled in for each language.
  ↳ recyclingInstructions 🌐object✓Collection / recycling — Where it can be returned and how it can be recycled — per language.
  ↳ disposalInstructions 🌐object✓Disposal / warnings — Prohibitions, hazards — per language.
complianceDocumentsarray[object]—Compliance documents — Links to certificates, conformity and test documents (optional). e.g. CE, UN 38.3.
  ↳ typestring✓Type
  ↳ urlstring✓URL
batterySerialNumberstring—Battery serial number — Serial number of the INDIVIDUAL unit. The battery passport is item-level, so this only makes sense per unit, not per model.
uniqueManufacturerIdentifier 🔒string—Unique manufacturer identifier — Unique identifier of the manufacturer (e.g. GLN or EORI). JTC24 places it at AUTHORITIES level.
thirdPartyServiceProvider 🔒string—Third-party service provider — Service provider operating the passport on behalf of the economic operator. AUTHORITIES level.
batteryStatus 🔒string (enum)—Battery status — Lifecycle status of the unit. Follows the JTC24 code list; ITEM-SPECIFIC. (original · re-used · remanufactured · repurposed · waste)
manufacturerInformationobject—Manufacturer information — Details of the MANUFACTURER — not necessarily the same as the economic operator placing it on the market, which is why JTC24 asks for it separately.
  ↳ namestring✓Legal name
  ↳ tradeNamestring✓Registered trade name / trademark
  ↳ postalAddressstring✓Postal address
  ↳ webAddressstring—Web address
  ↳ eMailAddressstring—E-mail
dismantlingInformation 🔒string—Dismantling information (link) — How to safely dismantle the battery. Aimed at repairers and waste operators.
partNumbersForComponents 🔒string—Part numbers for components (link) — List of part numbers for replaceable components.
informationOnSourcesOfSpareParts 🔒string—Sources of spare parts (link) — Where replacement parts can be obtained.
safetyMeasuresUrl 🔒string—Safety measures (link) — Document of precautions to observe during dismantling and handling.
renewableContentSharenumber—Renewable content share (%) — Share of materials from renewable sources in the battery.
wastePreventionInfoUrlstring—Waste prevention — end-user information (link) — What the user can do to prevent waste. PUBLIC information.
separateCollectionInfoUrlstring—Separate collection — end-user information (link) — How the user should hand in the waste battery for separate collection.
secondLifeTreatmentInfoUrlstring—Second life & end-of-life treatment (link) — Collection, preparation for second life and end-of-life treatment.
separateCollectionSymbolUrlstring—Separate collection symbol (link) — Image or description of the crossed-out wheeled bin symbol.
carbonFootprintLabelUrlstring—Carbon footprint label (link) — Label showing the carbon footprint performance class.
euDeclarationOfConformityUrlstring—EU declaration of conformity (link) — Where the EU declaration of conformity can be accessed. Required by the Regulation.
testReportsUrl 🔒string—Test reports proving compliance (link) — Test reports proving compliance. JTC24 places these at AUTHORITIES level.
meaningOfLabelsAndSymbols 🌐object—Meaning of labels and symbols — Explanation of the markings on the battery, for the user.
extinguishingAgentstring—Extinguishing agent — Which extinguishing agent may be used on a battery fire. Critical for firefighters.
extinguishingAgentFireClassstring—Fire class — Fire class of the extinguishing agent (A/B/C/D/F).
industrialSubtypestring (enum)—Industrial battery subtype — The Regulation has a single "industrial" category, but the JTC24 data model uses three distinct codes. This field decides which one the battery passport export uses. Only relevant for the industrial category. (without_bms · stationary_above_2kwh · non_stationary_above_2kwh)
economicOperatorWebstring—Economic operator website — Website of the operator placing the battery on the market.
facilityId 🔒string—Facility identifier — Identifier of the facility where the battery was made — GLN or own identifier.
warrantyPeriodstring—Warranty period — ISO 8601 duration: P8Y = 8 years, P8Y6M = 8 years 6 months.
dateOfPuttingIntoServicestring—Date of putting into service — When this individual unit was put into service. Item-level data.
cellChemistryOtherstring—Cell chemistry — other, specified — If cell chemistry is "Other", name it here. JTC24 has a dedicated field for it.
certifiedUsableEnergyKwhnumber—Certified usable battery energy (kWh) — Usable energy measured in the certification procedure; the SOCE figure refers to it.
initialInternalResistanceModulenumber—Initial internal resistance — module (Ω) — Alongside cell and pack values, JTC24 also knows the module-level figure.
measurementTempLowerinteger—Measurement temperature — lower boundary (°C) — Temperature range in which the voltage and power figures above are valid. JTC24 asks for it per group; here you give it once.
measurementTempUpperinteger—Measurement temperature — upper boundary (°C) — Upper boundary of the validity range for the figures above.
remainingUsableEnergyKwh 🔒number—Remaining usable battery energy (kWh) — Energy currently extractable from this unit; declines against the certified value.
overchargeEventCount 🔒integer—Number of overcharge events — How many times the battery was overcharged; an input to lifetime estimation.
deepDischargeEventCount 🔒integer—Number of deep discharge events — How many times the battery was deeply discharged.
internalResistanceIncreaseCell 🔒number—Internal resistance increase — cell (%) — Increase of cell-level internal resistance versus the original value.
internalResistanceIncreaseModule 🔒number—Internal resistance increase — module (%) — Increase of module-level internal resistance versus the original value.
carbonFootprintAbsoluteinteger—Carbon footprint — absolute (kg CO₂e) — Life-cycle carbon footprint in absolute terms, alongside the per-kWh figure.
supplyChainIndicesstring—Supply chain indices — Supply-chain risk indices as stated in the due diligence policy.
thirdPartyAssurancesstring—Third-party assurances of recognised schemes — Which recognised scheme (e.g. IRMA, RMI) assured the due diligence.
symbolsForCadmiumAndLeadUrlstring—Cadmium/lead symbol (image or document) — Marking warning of Cd/Pb content. Completion expects it for nickel-cadmium and lead-acid chemistries; fill it in anyway if another chemistry exceeds 0.002% cadmium or 0.004% lead.
modulesIndividuallyReplaceable 🔒string (enum)—Cell modules individually replaceable — Can a single faulty module be replaced without replacing the whole pack? (yes · partly · no)
cellsIndividuallyReplaceable 🔒string (enum)—Cells individually replaceable — Can the module be opened down to cell level, or is it a sealed unit? (yes · partly · no)
bmsSeparatelyRemovable 🔒string (enum)—BMS separately removable — Is the electronics accessible without opening the cells? (yes · partly · no)
professionalServiceRequired 🔒string (enum)—Professional service and factory tooling required — Can the product be opened with common tools, or is a trained technician required? (yes · partly · no)
sparePartsAvailabilityYears 🔒integer—Spare part availability (years) — For how many years does the manufacturer commit to supplying spare parts?

furniture Furniture20 field

FieldTypeReq.Description
productNamestring✓Product name — The product name as the buyer will see it on the passport.
skustring✓SKU — Internal item number / identifier — from your own system.
brandNamestring✓Brand
gtinstring—GTIN (optional) — The barcode number (GTIN-8/12/13/14). The system validates the check digit. Leave empty if you don't have one.
commodityCodestring—Commodity code (CN/HS, optional) — Customs/tariff classification: Combined Nomenclature (CN, 8 digits) or HS code. Mandatory metadata of the EU DPP register at registration — it appears on your supplier/customs documents.
economicOperatorobject✓Economic operator
  ↳ rolestring (enum)✓Role — Who places the product on the EU market and is responsible for it. For most manufacturers: Manufacturer. (manufacturer · importer · authorized_representative · distributor · dealer · fulfilment_service_provider)
  ↳ legalNamestring✓Legal name
  ↳ addressstring✓Address
  ↳ countrystring✓Country (ISO 3166-1 alpha-2) — Two-letter ISO country code, in capitals — e.g. HU, DE, IT
  ↳ contactEmailstring✓Contact email — This is where the buyer or an authority can turn.
  ↳ operatorId 🔒string—Unique operator ID (GS1 GLN) — 13-digit GS1 GLN — the EU DPP unique economic operator identifier. Visible at the authorities access level only. Leave empty if you do not have one.
  ↳ eori 🔒string—EORI number — The economic operator's EORI number (customs / EU DPP registry identifier) — the registry identifies you by this. Visible only at authority access level. Leave empty if you don't have one.
languagesarray[string]✓Languages (BCP-47) — Which languages the passport should appear in. The localised fields (care, assembly, life cycle) must be filled in for every language listed here. e.g. hu, en
materialCompositionarray[object]✓Material composition — What the furniture is made of. The percentages must add up to exactly 100. e.g. solid oak 80 + metal 20
  ↳ materialstring (enum)✓Material (wood · engineeredWood · metal · plastic · glass · textile · foam · leather …)
  ↳ percentagenumber✓Percentage
woodCertificationobject—Wood origin & certification — Sustainable sourcing and certification of the wood used, if relevant (optional).
  ↳ schemestring (enum)—Certification scheme — Sustainable forest management certification of the wood. (fsc · pefc · none)
  ↳ countrystring—Country of origin (ISO) — Country of origin of the wood. Two-letter ISO code, e.g. AT, SE, RO. Optional.
dimensionsobject—Dimensions & weight — Overall dimensions and weight of the furniture (optional).
  ↳ widthnumber—Width (cm)
  ↳ depthnumber—Depth (cm)
  ↳ heightnumber—Height (cm)
  ↳ weightKgnumber—Weight (kg)
countryOfManufacturestring✓Country of manufacture (ISO) — Where the finished product was made. Two-letter ISO code, e.g. PL, RO, HU
careInstructions 🌐object✓Care instructions — Cleaning, care, surface maintenance — one text per language.
assemblyInstructions 🌐object—Assembly instructions — How the furniture is assembled — per language (optional). A link can also be given.
repairobject✓Repairability — Whether and how it can be repaired. The “repairable” and “spare parts available” fields are mandatory.
  ↳ repairableboolean✓Repairable
  ↳ instructions 🌐object—Repair instructions — How it can be repaired — per language (optional).
  ↳ sparePartsAvailableboolean✓Spare parts available (e.g. fittings, leg)
  ↳ sparePartsUrlstring—Spare parts (URL) — Where spare parts can be ordered (optional).
warrantyMonthsnumber—Warranty (months) — The length of the manufacturer's warranty in months. Optional.
substancesOfConcernarray[object]—Substances of concern — Substances of concern (e.g. REACH SVHC / SCIP), if present in the product. Can be left empty for most products.
  ↳ namestring✓Name
  ↳ casNumberstring—CAS number
  ↳ notestring—Note
flameRetardantsstring (enum)—Flame retardants — Whether the product (especially the upholstery / foam) contains flame retardant chemicals. Optional. (present · absent · unknown)
complianceDocumentsarray[object]—Compliance documents — Links to certificates and conformity documents (optional). e.g. EN 12520, EN 1728, fire safety certificate.
  ↳ typestring✓Type
  ↳ urlstring✓URL
packagingstring (enum)—Packaging recyclability — Recyclability of the product's packaging. Optional. (recyclable · partiallyRecyclable · notRecyclable)
endOfLifeobject✓End of life — What should happen to the furniture after use — filled in for each language.
  ↳ recyclingInstructions 🌐object✓Recycling instructions — How it can be taken apart and recycled — per language.
  ↳ disposalInstructions 🌐object✓Disposal instructions — If it is not recyclable — per language.

Languages

The languages field (e.g. ["hu","en"]) declares which languages have free-text content. The localized fields (care, end-of-life… — "localized": true in /schema) must be filled only in the first (primary) language; the rest are optional, falling back to an available language if missing. The passport's UI labels are translated to 24 languages automatically, independently of this.

Examples

Bulk load from the ERP

curl -X POST https://veridyn.eu/api/v1/products:batch   -H "Authorization: Bearer vk_<key>"   -H "Content-Type: application/json"   -H "Idempotency-Key: erp-2026-09-24-001"   -d '{
    "category": "battery",
    "on_duplicate": "skip",
    "products": [
      { "sku": "VE60-001", "productName": "PowerCell 60", "...": "..." },
      { "sku": "VE60-002", "productName": "PowerCell 60 XL", "...": "..." }
    ]
  }'

The response is a per-row report: created / updated / exists (SKU already present) / invalid (with the errors) / plan_limit. Partial success is normal — a bad row does not stop the others.

The Idempotency-Key header removes the risk of retries: after a timeout, repeating the request with the same key returns the earlier response instead of creating everything twice.

Create a product

curl -X POST https://veridyn.eu/api/v1/products \
  -H "Authorization: Bearer vk_<key>" \
  -H "Content-Type: application/json" \
  -d '{
    "category": "textile",
    "data": {
      "productName": "Organic cotton tee",
      "sku": "TEE-001",
      "brandName": "Lumora",
      "languages": ["hu","en"],
      "fiberComposition": [{"fiber":"pamut","percentage":100}],
      "countryOfManufacture": "PT",
      "careInstructions": {"hu":"Mosás 30 °C","en":"Wash at 30 °C"},
      "repair": {"repairable": true, "sparePartsAvailable": false},
      "endOfLife": {
        "recyclingInstructions": {"hu":"Textilgyűjtő","en":"Textile bin"},
        "disposalInstructions": {"hu":"Ne a kukába","en":"Not household waste"}
      },
      "economicOperator": {
        "role":"manufacturer","legalName":"Lumora Kft.","address":"Budapest",
        "country":"HU","contactEmail":"[email protected]"
      }
    }
  }'

Response (201):

{
  "data": {
    "id": "a1993ef7-7562-43bb-92ff-eb63f02dcde9",
    "category": "textile",
    "status": "active",
    "version_no": 1,
    "passport_url": "https://veridyn.eu/<account>/p/a1993ef7-…",
    "qr_url": "https://veridyn.eu/api/v1/products/a1993ef7-…/qr",
    "data": { "productName": "Organic cotton tee", … }
  }
}

List

curl https://veridyn.eu/api/v1/products?limit=25 \
  -H "Authorization: Bearer vk_<key>"

Get a product

curl https://veridyn.eu/api/v1/products/{id} \
  -H "Authorization: Bearer vk_<key>"

Update (new version)

curl -X PATCH https://veridyn.eu/api/v1/products/{id} \
  -H "Authorization: Bearer vk_<key>" \
  -H "Content-Type: application/json" \
  -d '{"data": {"recycledContentPercentage": 30}}'

The provided fields are merged onto the existing data (partial update), then validated, and a new, retained version is created — the full change history is preserved.

Batch and unit passports (serialisation)

Create batch or unit passports under a model; the child inherits the parent's data — you pass only the instance-specific fields (lot/serial). Many at once (items[], max 500); the response reports success/error per item.

curl -X POST https://veridyn.eu/api/v1/products/{id}/children \
  -H "Authorization: Bearer vk_<key>" \
  -H "Content-Type: application/json" \
  -d '{"level":"item","items":[{"serial":"SN-0001"},{"serial":"SN-0002"}]}'

The {id} may be a model or a batch. POST to a batch id with level:"item" and the unit is created under that batch, inheriting its lot — its GS1 link is then …/10/lot/21/serial rather than …/21/serial. This is how the full model → batch → unit chain is built.

Writing live data back in bulk

Some unit-level fields are live data: a battery's SoH, cycle count and remaining capacity come from the BMS. You do not have to write these back one by one — a single call updates up to 500 child passports. Targets are identified by serial, lot or id, so what your MES already knows is enough; you need not store our internal identifier.

curl -X PATCH https://veridyn.eu/api/v1/products/{id}/children \
  -H "Authorization: Bearer vk_<key>" \
  -H "Content-Type: application/json" \
  -d '{"change_type":"change","items":[
        {"serial":"SN-0001","data":{"stateOfHealth":97,"fullCycleCount":412}},
        {"serial":"SN-0002","data":{"stateOfHealth":95,"fullCycleCount":508}}
      ]}'

The response is a per-row report: updated · unchanged · not_found · ambiguous (the serial is not unique under the parent) · validation. An unchanged row creates no new version — so a daily fleet sync does not pad the change history with empty versions. change_type applies to the whole batch: a BMS refresh is a change (the earlier value was true at its time), not a correction.

💡 A bulk call emits a single aggregate children.updated webhook, and delivery is queued — it does not happen during the request. Otherwise 500 rows would mean 500 outbound HTTP calls before your response.

Scan analytics

Aggregated QR-scan statistics — per product or account-wide — broken down by country and device. Privacy: no raw IP; unique visitors are an anonymous hashed estimate.

curl "https://veridyn.eu/api/v1/products/{id}/scans?days=30" \
  -H "Authorization: Bearer vk_<key>"
{
  "data": {
    "product_id": "a1993ef7-…",
    "range_days": 30,
    "total": 189, "unique": 142,
    "byCountry": { "HU": 142, "DE": 38, "AT": 9 },
    "byDevice":  { "mobile": 168, "tablet": 9, "desktop": 12 }
  }
}

Incremental sync (changes only)

An ERP cannot re-read the whole passport inventory every night: as the catalogue grows, so does the run time. With the updated_since parameter you get back only what changed since that moment — and the list then returns every level (model, batch, unit), because an MES also synchronises the unit passports.

curl "https://veridyn.eu/api/v1/products?updated_since=2026-09-24T00:00:00Z&limit=100" \
  -H "Authorization: Bearer vk_<key>"
{
  "data": [
    { "id": "a1993ef7-…", "name": "VE60 EV modul", "status": "active",
      "updated_at": "2026-09-24T08:11:32+00:00", "passport_url": "…" },
    { "id": "c7d10b42-…", "name": "VE60-000418", "status": "archived",
      "updated_at": "2026-09-24T14:02:07+00:00", "passport_url": "…" }
  ],
  "per_page": 100,
  "has_more": true,
  "next_cursor": "MjAyNi0wOS0yNCAxNDowMjowN3xjN2QxMGI0Mi0…"
}

The sync response has no page or total — instead there is has_more and next_cursor. You request the next page by passing the received next_cursor back unchanged: ?cursor=…. The cursor is opaque: do not decode or construct it — a hand-made value is rejected with 422 bad_cursor.

curl "https://veridyn.eu/api/v1/products?cursor=MjAyNi0wOS0yNCAxNDowMjowN3xjN2QxMGI0Mi0…" \
  -H "Authorization: Bearer vk_<key>"

Why a cursor and not a page number

Because page-number pagination silently skips rows. The list is ordered by modification time: while you are asking for page 2, an already-seen product is updated, jumps to the front, and everything behind it shifts back one — the row that was exactly on the page boundary never reaches you. Meanwhile the client believes it received everything. A cursor points at the specific last row you processed, not at a position, so this cannot happen. (For timestamps landing in the same second the internal identifier breaks the tie — on an import that affects dozens of rows.)

Archived passports appear too — that is the point

Archiving does not remove a product from the sync window: it comes back with status: "archived". That is the tombstone — this is how your system knows to drop it from its own list. If archived rows were filtered out, the client would keep them as active forever.

Recommended flow: store the last next_cursor; the next run starts from it and pages until has_more is false. For the initial (full) load use updated_since=1970-01-01T00:00:00Z — from then on only changes travel over the wire.

Two things your client should expect

1. The most recent second is held back. The modification timestamp has one-second resolution, so the list only serves already closed seconds: a passport you just saved typically appears in the sync list 1–2 seconds later. This is deliberate. Without it the cursor could step into a second that is still open, and a passport saved later within that same second would never be delivered — not delayed, lost, and silently so.

2. An empty page still carries a cursor. If nothing changed, data is empty and has_more is false — but next_cursor is still not empty, and it has moved forward. Always store it: the cursor advances even when nothing changed.

Certificates and documents

The declaration of conformity, the test report and the safety data sheet live in PDF — and they usually originate in a document management system or in correspondence with a lab, not in a browser tab. The file content therefore travels in the request body as base64:

curl -X POST https://veridyn.eu/api/v1/products/{id}/documents \
  -H "Authorization: Bearer vk_<key>" \
  -H "Content-Type: application/json" \
  -d '{"filename":"ce-tanusitvany.pdf","tier":"public","label":"CE","content_base64":"JVBERi0xLjQK…"}'

The type is decided by the content, not the file name. An extension can lie — and someone lying deliberately is counting on exactly that — so we detect the type from the bytes. If it is not PDF, PNG, JPEG or WebP, the answer is 415 bad_type and nothing is stored. The stored file is named by our identifier; the submitted filename is only a display label.

The tier says who may see it: public (shown on the public passport too), legitimate_interest or authorities — the same ranking as the schema fields use. An unknown value falls back to the narrowest, public, so for a confidential document always set it.

The size limit is 8 MB for the decoded file. Base64 makes the request body larger than that (about 4/3), and if it exceeds the server's body limit, the body never arrives — that returns 413 payload_too_large, and we name the server's actual limit in it so you are not left debugging your own code.

You can also ask for the actual maximum before uploading anything: GET /health (it works without a key) returns limits.document_upload_max_bytes, the narrower of the two. It is worth fetching once at start-up.

curl https://veridyn.eu/api/v1/health
{
  "status": "ok",
  "limits": {
    "document_max_bytes": 8388608,
    "request_body_max_bytes": 8388608,
    "document_upload_max_bytes": 6165626,
    "bulk_max_items": 500
  }
}

The download returns the file itself, not JSON — with its own content type; ?dl=1 serves it as an attachment. A DELETE removes it from disk as well.

curl https://veridyn.eu/api/v1/products/{id}/documents/42?dl=1 \
  -H "Authorization: Bearer vk_<key>" -o tanusitvany.pdf

Supplier data requests

You do not know all of the passport data yourself: the fibre composition comes from the mill, the cell provenance from the cell maker. You start a request for the specific fields, and the supplier fills in their part via a login-free link — you need not give them an account. This has long existed in the UI but not in the API: a procurement system could upload 500 passports, yet had to start the 500 data requests by hand.

curl -X POST https://veridyn.eu/api/v1/supplier-requests \
  -H "Authorization: Bearer vk_<key>" \
  -H "Content-Type: application/json" \
  -d '{"product_id":"a1993ef7-…","email":"[email protected]",
       "fields":["fiberComposition","countryOfManufacture"],
       "message":"For the next shipment, please.","lang":"en"}'
{
  "data": {
    "id": 17, "status": "pending", "email": "[email protected]",
    "fields": ["fiberComposition", "countryOfManufacture"],
    "expires_at": "2026-10-25 09:14:02",
    "link": "https://veridyn.eu/supplier.php?token=9f3c…",
    "emailed": true
  }
}

The link is always in the response — even when we send the email. A machine integration usually writes to the supplier from its own mail system (own template, own sender, own tracking); in that case send_email: false switches off our sending and you deliver the link yourself. `fields` are the category schema's field keys (GET /schema/{category}); internal fields cannot be delegated. The link expires after 30 days.

Reviewing and merging the submission

Until the supplier fills anything in, the request is pending. After submission it is submitted, and the detail endpoint shows what they wrote — with both the raw field key and a human field name, so an approval UI need not know the schema. Merging is a separate step: incoming data does not enter the product automatically.

curl https://veridyn.eu/api/v1/supplier-requests/17 \
  -H "Authorization: Bearer vk_<key>"

curl -X POST https://veridyn.eu/api/v1/supplier-requests/17/merge \
  -H "Authorization: Bearer vk_<key>"

Merging transfers only the REQUESTED fields. The supplier (or anyone who knows the token) may send more, but anything you did not ask for does not touch the product — whoever you only asked about the country of manufacture cannot rewrite your brand name.

If the merged data is invalid (422 validation), the product stays untouched and the request is not closed: you clarify it with the supplier and retry. On success a new version is created and a product.updated webhook goes out marked source: "supplier_merge" — so your own system can see it was a submission, not your own edit.

Archiving and restoring

When a product is discontinued you archive it — but we do not delete the passport: the QR code and link keep working, and the passport is shown as "archived". Legal retention requires this: the passport of a unit already on the market must stay alive even after you stop making the model.

curl -X POST https://veridyn.eu/api/v1/products/{id}/archive \
  -H "Authorization: Bearer vk_<key>"

curl -X POST https://veridyn.eu/api/v1/products/{id}/restore \
  -H "Authorization: Bearer vk_<key>"

No body needed. The operation creates no new version: a visibility change is not a data change, and a version with an empty diff would only add noise to the change history — which is also why this is a separate endpoint rather than a PATCH. A webhook is fired: product.archived and product.restored.

It does not free up quota: the plan's model counter includes archived passports, so archiving does not make room for a new one, and restoring does not consume any.

Change history (versions)

Every PATCH creates a new, retained version, and the chain is hash-sealed — it cannot be rewritten after the fact. The history is machine-readable too: the list returns the versions, and /versions/{no} returns which field changed, from what to what.

curl https://veridyn.eu/api/v1/products/{id}/versions \
  -H "Authorization: Bearer vk_<key>"

curl https://veridyn.eu/api/v1/products/{id}/versions/3 \
  -H "Authorization: Bearer vk_<key>"

Both are reads, so a read-only key is enough — you need not grant write access to an auditing system. The change_type is visible as well: a correction fixes wrong data (already-issued unit passports become out of date), while a change is a real product change from a given date (earlier units keep their correct data).

Access links (authority, legitimate interest)

The public passport shows the public fields. The regulation, however, requires broader access for the market-surveillance authority and parties with legitimate interest (a recycler or repairer, say). That is what expiring access links are for: they open without a login, for a single passport, and go quiet on their own at expiry — you need not hand out accounts.

curl -X POST https://veridyn.eu/api/v1/products/{id}/grants \
  -H "Authorization: Bearer vk_<key>" \
  -H "Content-Type: application/json" \
  -d '{"tier":"authority","label":"NKFH 2026/114","expires_in_days":30}'
{
  "data": {
    "id": 42, "tier": "authority", "label": "NKFH 2026/114",
    "status": "active", "expires_at": "2026-10-25 09:14:02",
    "use_count": 0, "last_used_at": null,
    "token": "9f3c…",
    "passport_url": "https://veridyn.eu/acme/p/a1993ef7-…?grant=9f3c…",
    "history_url":  "https://veridyn.eu/history.php?id=a1993ef7-…&grant=9f3c…"
  }
}
FieldMeaning
tierlegitimate_interest or authority. No other tier can be granted (422 bad_tier): the owner tier is your own account and cannot be handed out as a link.
expires_in_daysOptional — and leaving it out means a permanent link. An authority inspection is finite, so it is worth always setting; after expiry the link returns 410.
labelFree text: whom you gave it to. Visible only to you, not to the recipient.
history_urlFilled in only at authority tier: an authority may also open the full change history; a legitimate-interest party may not.
use_countHow many times it was opened. A freshly issued link is always 0 — issuing is not a use.

The GET lists the live links of that passport, and DELETE /products/{id}/grants/{gid} revokes one immediately — the token then returns 410.

Schema version: when you need to touch your integration

A category's field list moves with the legislation. Until now nothing signalled that a schema had changed — integrations broke silently. So GET /schema/{category} now returns a version and an ETag:

curl -i https://veridyn.eu/api/v1/schema/battery \
  -H "Authorization: Bearer vk_<key>"

HTTP/1.1 200 OK
ETag: "battery.1.0.02894917d482"
X-DPP-Schema-Version: 1.0
Cache-Control: public, max-age=3600

The ETag is built from the machine contract: field names, types, whether they are required, their enum values and access tiers. So what does not change it: labels, help text, translations. You therefore get no false alarm just because a field's wording was made more precise.

curl -i https://veridyn.eu/api/v1/schema/battery \
  -H "Authorization: Bearer vk_<key>" \
  -H 'If-None-Match: "battery.1.0.02894917d482"'

HTTP/1.1 304 Not Modified

Put the ETag into your bootstrap script or nightly job: as long as you get a 304, the contract holds and there is nothing to do. If a 200 arrives, the response's version, revised and contract_hash fields show what moved.

If a backwards-incompatible change is being prepared for a category, the response also carries Deprecation and Sunset headers (RFC 8594), the Sunset being the earliest date the old shape goes away. By default neither header appears — if you see one, you have a real deadline.

Response & errors

Success: the payload is under the data key. On error:

{ "error": "Invalid data.", "code": "validation", "errors": [ … ] }
HTTPcodeMeaning
401unauthorizedMissing/invalid API key.
403plan_requiredThe API is available from the Pro plan (during the open beta, every beta account can use it).
404not_foundNo such product / route.
409gtin_takenThe GTIN is already taken.
422gtin_not_yoursThe GTIN falls outside your account's GS1 company prefix (Settings → GS1 company prefix). Only occurs if you have set a company prefix.
403plan_limitYou reached the level's quota (plan + extra). In bulk calls, the overflowing items go into errors.
422validationInvalid data (details in errors).
403read_onlyYou attempted a write with a read-only key (POST/PATCH/DELETE).
429rate_limitedYou exceeded the per-minute request quota. The Retry-After header says how long to wait.
400bad_jsonThe request body is not valid JSON. The API accepts a JSON body only (Content-Type: application/json).
413too_bigThe uploaded document exceeds the allowed size. GET /health returns the actual limit in limits.document_upload_max_bytes.
413payload_too_largeThe request body never arrived — it hit the server's post_max_size limit. The JSON is not at fault, the size is.
422bad_cursorThe cursor is not a next_cursor we issued. Pass it back unchanged — the cursor is opaque.
422bad_tierA link cannot be issued at this access tier (only legitimate_interest and authority).
501needs_migrationThe feature requires a database update that is not yet active. We deliberately fail rather than return partial data — tell us and we will enable it.

BatteryPass Economic Operator API

The /api/v1 above is our interface. Alongside it we operate a standards-based one: the endpoints of the CEN-CENELEC JTC 24 lifecycle API (EN 18222), “Digital Product Passport – Life Cycle API v1.1”, in the language the rest of the battery ecosystem speaks. Use it when your partner expects a standard integration rather than our API.

Conformance: the interface passes all 11 tests of the official BatteryPass-Ready conformance suite (read by DPP ID and by product ID, data-element read, access-rights filtering, withdrawn passport, update, adding and deleting an attribute, permission check, placing on the market). The suite tests a live system, not an uploaded file.

Base URL and authentication

https://veridyn.eu/eo — authentication: Authorization: Bearer {JWT}. The token carries a role (public, purchaser, authority, economic_operator…), and the response is filtered field by field accordingly — by the same machinery that filters the public passport. Only the economic operator can write.

Where the token comes from

You request the token with your own developer key — there is no separate registration:

curl -X POST https://veridyn.eu/api/v1/eo/token \
  -H "Authorization: Bearer vk_<key>" \
  -H "Content-Type: application/json" \
  -d '{"ttl":3600}'
{
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIs…",
    "token_type": "Bearer",
    "role": "economic_operator",
    "expires_in": 3600,
    "expires_at": "2026-09-25T11:14:02+00:00",
    "api_base": "https://veridyn.eu/eo"
  }
}

The token is scoped to the key's account — we deliberately do not take the tenant from the request, so you cannot request a token for someone else's passports. The only role available is economic_operator: the “this is my product” role. authority or recycler speak on another party's behalf and nobody may grant those to themselves — that is what expiring access links are for.

The ttl is in seconds, between 5 minutes and 24 hours (default 1 hour). The cap is deliberate: a JWT is self-contained and we keep no blacklist, so a lost long-lived token could not be revoked. Short token plus re-request is the right pattern. A read-only key mints no token (403 read_only), because the economic-operator role also authorises writes.

MethodPathDescription
POST/dppsNew passport from the submitted JTC24 document. Response: the identifier (string), 201.
GET/dpps/{dppId}Read by DPP identifier.
PATCH/dpps/{dppId}Partial update (RFC 7396) — creates a new version. A null value deletes the field.
DELETE/dpps/{dppId}Withdrawal. The passport is retained (authorities may request it retrospectively) but returns 404 on this interface.
GET/PATCH/dpps/{dppId}/elements/{path}Read/update a single data element by dotted path.
GET/PATCH/dpps/{dppId}/collections/{group}A data group (e.g. performanceAndDurability).
GET/dppsByProductId/{productId}The active version by product identifier.
GET/dppsByProductIdAndDate/{productId}The version valid at a given point in time. Parameter: date.
POST/dppsByProductIdsBulk identifier resolution. Parameters: limit, cursor.

Backup and registry: under (EU) 2023/1542 the passport data must also be stored with an independent provider, and placing on the market must be reported. If a backup provider is configured for the account, a copy is sent automatically after every create and update — and every call leaves an auditable trace.

Webhooks

Under Settings → Webhooks you register an HTTPS URL. When a product passport is created / updated / archived, Veridyn POSTs a signed JSON to that URL — so your system is notified in real time, without polling.

Events

product.created · product.updated · product.archived · product.restored · children.updated · scan.milestone · scan.clone_suspected

The children.updated event covers a bulk update of child passports and is deliberately a single, aggregate event — not one per row. Data: { parent_id, category, count, items }, where items lists the child passports that actually changed as { id, serial, lot } (unchanged rows are excluded). The per-row detail is in the call's response, not in the webhook — it goes to the caller who initiated the update.

The scan.milestone fires when a passport's scan count crosses a milestone (10, 50, 100, 250, 500, 1000, …) — e.g. "your passport was opened 1000 times". Data: { product_id, count, milestone }.

The scan.clone_suspected flags possible counterfeiting: a unique (serialised) passport was scanned unusually often and from many different countries/devices — which may indicate a copied QR code. Data: { product_id, count, countries, unique_devices }.

Delivery

POST https://your-system.example/veridyn-webhook
Content-Type: application/json
X-Veridyn-Event: product.updated
X-Veridyn-Event-Id: evt_9f21c4a08b73d5e6f1a20c44
X-Veridyn-Timestamp: 2026-07-02T13:19:11+00:00
X-Veridyn-Signature: sha256=<hmac>

{
  "event_id": "evt_9f21c4a08b73d5e6f1a20c44",
  "event": "product.updated",
  "data": { "id": "a1993ef7-…", "category": "textile", "version_no": 2 },
  "sent_at": "2026-07-02T13:19:11+00:00"
}
💡 The event_id and sent_at are in the body as well, not only in headers — so the signature covers them and they cannot be forged. They are also in headers so you can log and de-duplicate before parsing the body; if the two differ, the signed value in the body wins.

Signature verification

The X-Veridyn-Signature is the HMAC-SHA256 of the raw body with the webhook secret (shown in Settings). Verify it — e.g. in PHP:

$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
if (!hash_equals($expected, $_SERVER['HTTP_X_VERIDYN_SIGNATURE'] ?? '')) {
    http_response_code(401); exit; // invalid signature
}

Respond with a 2xx status. If delivery fails (non-2xx / timeout), we automatically retry with exponential backoff (about 1 min → 5 min → 30 min → 2 h → 6 h, up to 6 attempts), giving up on persistent failure (after about 8.5 hours). The status of the last delivery is shown in Settings, where it can also be resent manually.

De-duplication and catching up on missed events

Retrying means the same event may arrive more than once — for example if your response times out although your processing already completed. That is why every event carries an event_id: store the ones you have processed and drop any identifier you have already seen. This matters most for a bulk children.updated event, where a single duplicate would mean processing 500 rows twice.

If your system was unreachable for a long time (maintenance, a weekend outage), the 6 attempts can run out and the event would be lost. That is what GET /events is for: it returns every outgoing event in chronological order, together with its delivery status. Remember the last id you processed and continue from there — not by page number, because new events may appear in the meantime.

curl "https://veridyn.eu/api/v1/events?after_id=1487&limit=100" \
  -H "Authorization: Bearer vk_<key>"
{
  "data": [
    {
      "id": 1488,
      "event_id": "evt_9f21c4a08b73d5e6f1a20c44",
      "event": "product.updated",
      "data": { "id": "a1993ef7-…", "category": "textile", "version_no": 2 },
      "sent_at": "2026-07-02T13:19:11+00:00",
      "delivery": { "status": "failed", "attempts": 6, "http_status": 502, "url": "https://…" }
    }
  ],
  "after_id": 1487,
  "next_id": 1488
}

The delivery.status is delivered (sent successfully), pending (still retrying) or failed (given up). The event is listed even if delivery never succeeded — that is precisely what makes catching up possible.

💡 The category schemas (which fields are required/optional) are visible on the product form and via the /schema/{category} endpoint. Currently available: textile, battery and furniture. The schema grows as the EU finalises categories.

← Settings / API keys