Passport product data model (UNTP)
The product object accepted by POST /v1/dpp/passports and by bulk import follows the UN Transparency Protocol (UNTP) Core Digital Product Passport model, v0.7.0 — specification, JSON-LD context. Keyban uses this model as its internal pivot: whatever the source — a manual create, a bulk import, or the Shopify catalogue sync — every passport's product ends up in this shape, and every downstream projection (the public passport page, the on-chain certificate, the verifiable credential) is derived from it.
Two identifiers are generated by Keyban and must not be sent by the caller: product.id and product.idScheme. Object type tuples (type: ['Product'], type: ['Claim'], …) default server-side and can be omitted from the payload.
Granularity drives which fields are required
Granularity is carried by product.idGranularity, not by a top-level discriminant field:
idGranularity | Identifiers required on product |
|---|---|
model | modelNumber |
batch | modelNumber + batchNumber |
item | modelNumber + batchNumber + itemNumber |
modelNumber, batchNumber and itemNumber are nullable at the schema level for all three granularities — the requirement above is enforced by the import/create service against idGranularity, not by a type union. Sending an item without batchNumber fails validation even though the field type itself allows null.
Core sections of product, and where they surface on the passport
| Section | Key fields | Where it appears on the published passport |
|---|---|---|
| Identity | name, modelNumber / batchNumber / itemNumber | Page title, and the "Product identity" card |
| Category | productCategory[] | "Category" field inside the "Product identity" card |
| Primary image | productImage | Image carousel at the top of the page |
| Technical specifications | characteristics (free-form key/value) | "Technical specifications" card |
| Origin and manufacture | producedAtFacility, countryOfProduction | "Origin and manufacture" card; countryOfProduction is also shown in the hero on model passports |
| Production date | productionDate | Hero of item passports only — not shown on model / batch |
| Dimensions | dimensions (weight, length, width, height, volume) | "Product dimensions" card |
| Material composition | materialProvenance[] | "Material composition" card (material, origin, mass, recycled share, hazardous) |
| Conformity claims | performanceClaim[] | "Evidence of sustainability" card |
| Related parties | relatedParty[] | "Actors involved" card (manufacturer, distributor, …) |
| Related documents | relatedDocument[] | "Documents" card |
| Product labels | productLabel[] | "Labels and certifications" card — images only, name / description are not rendered |
| Packaging | packaging | Not rendered today. Accepted and stored, but the public passport viewer has no card for it yet. |
This table covers what maps to a visible block on the passport, not the exhaustive field list — for that (including fields with no dedicated UI, like expiryDate), see the CreateDppPassportDto schema.
Full example
A model-granularity passport with a material breakdown and a conformity claim, trimmed from a real import payload:
{
"application": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"network": "StarknetMainnet",
"product": {
"name": "Organic cotton crewneck T-shirt — White",
"idGranularity": "model",
"modelNumber": "KB-TS-001",
"description": "100% GOTS-certified organic cotton, classic straight cut.",
"productImage": {
"linkURL": "https://ipfs.io/ipfs/QmNXXmCD9xzH8em8EbhirCyNrHai4SVTYBYpWAfhptBuRn",
"linkName": "Organic cotton crewneck T-shirt — White"
},
"characteristics": {
"Color": "White",
"Size range": "XS-XXL",
"Gender": "Unisex"
},
"productCategory": [
{
"code": "53102501",
"name": "T-shirts",
"schemeID": "https://vocabulary.uncefact.org/unspsc",
"schemeName": "UNSPSC"
}
],
"relatedParty": [
{
"role": "manufacturer",
"party": {
"id": "did:web:keyban.io",
"name": "Keyban",
"registeredId": "3760123450017",
"idScheme": {
"id": "https://id.gs1.org/417",
"name": "GS1 Global Location Number"
}
}
}
],
"dimensions": {
"weight": { "unit": "GRM", "value": 180 }
},
"materialProvenance": [
{
"name": "Organic cotton",
"originCountry": { "countryCode": "IN" },
"materialType": {
"code": "cotton-organic",
"name": "Organic cotton",
"schemeID": "https://vocabulary.uncefact.org/material",
"schemeName": "UN/CEFACT Material Vocabulary"
},
"massFraction": 1,
"recycledMassFraction": 0,
"hazardous": false
}
],
"performanceClaim": [
{
"id": "https://passport.keyban.io/credentials/dcc/kb-ts-001/gots",
"name": "Global Organic Textile Standard (GOTS) 7.0",
"description": "Certifies a high share of organic fibres and compliance with environmental and social criteria across the textile chain.",
"claimDate": "2026-02-10",
"referenceCriteria": [],
"claimedPerformance": [],
"conformityTopic": [
{
"id": "https://vocabulary.uncefact.org/conformityTopic#environment.material",
"name": "environment.material"
}
],
"referenceStandard": [
{
"id": "https://global-standard.org/",
"name": "Global Organic Textile Standard (GOTS) 7.0"
}
],
"evidence": [
{
"linkURL": "https://passport.keyban.io/credentials/dcc/kb-ts-001/gots",
"linkName": "DCC — GOTS certificate"
}
]
}
]
}
}
referenceCriteria and claimedPerformance are schema-valid as empty arrays, though at least one entry is expected in practice — reserve the empty form for a claim backed only by a third-party certificate (evidence) rather than a self-reported measurement.
Shopify sync vs. manual or bulk import
The Shopify catalogue sync auto-fills identity, images, and variant-derived fields (model / item) straight from the Shopify product. It has no native equivalent for the traceability and conformity sections — materialProvenance, performanceClaim, relatedParty, packaging — because Shopify's own product model doesn't carry them. Shopify-sourced passports (source = "shopify") are read-only through PATCH, so these fields stay empty unless populated at the source; passports created through POST /v1/dpp/passports or POST /v1/imports (source = "manual" / "import") can set the full model in one call. See Prepare your Shopify catalogue for how modelNumber is derived from the Shopify product identifier, and why the sync never produces batch-granularity passports.
API reference
For the exhaustive, always-current field list — including nested object shapes, optionality, and validation rules — see CreateDppPassportDto.