Products & categories
A Product is a tenant-owned commercial aggregate — not automatically a passport. Its category selects a schema that governs the extensions payload you must supply.
Fields
id— assigned on create; capture it as{{productId}}.gtin,serialNumber— the GS1 identity used later for Digital Link resolution.category— resolves to an effective schema; drivesextensions.extensions— category-specific fields (electronics ≈ 8, battery ≈ 30).
Serial rules
- Serial must be ≤ 20 characters; 21 →
400 VALIDATION_ERRORnaming the SGTIN-96 bound. Unprintable serials are rejected before anything irreversible. - Numeric serials must fit the EPC carrier bound (≤ 238) for carrier/EPC generation; alphanumeric serials may still yield a valid Digital Link/QR while EPC carrier generation refuses — a deliberate split.
Lifecycle
A product moves draft → validated → ready_for_dpp via POST /products/{id}/lifecycle/transition. Two transitions are required before a passport can be generated. Illegal transitions are typed errors, not silent no-ops. Every change emits a domain event (product.created, product.updated).
Create guardrails
| Condition | Response |
|---|---|
description < 10 chars | 400 VALIDATION_ERROR |
Missing extensions | 422 EXTENSIONS_REQUIRED |
| Category has no effective schema | 422 SCHEMA_NOT_FOUND |
| Serial > 20 chars | 400 VALIDATION_ERROR (SGTIN-96) |
| Bad GS1 check digit / non-digit GTIN | 400 VALIDATION_ERROR — a valid GTIN-8/12/13 is normalized to 14 digits at write time |
| Update verb | PATCH /products/{id} (canonical, PRD F1) or PUT (alias) — both run the same full-update handler |
Categories & schemas
GET /products/categories lists the tenant's categories with usage counts. Schema discovery lives on the dedicated surface (DevX pass 2026-07-22):
GET /api/v2/schemas/categories
→ every category with its schema versions, jurisdiction, effective window, effectiveNow flag
GET /api/v2/schemas/categories/{category}?jurisdiction=EU&date=2026-07-22T00:00:00Z
→ the schema effective AT that instant for that jurisdiction: schemaId, schemaRegistryId,
version, effectiveFrom/Until, full fieldSchema, and _links.importTemplate
(append &includeFields=false to skip the field definitions)
GET /api/v2/schemas/categories/{category}/import-template?jurisdiction=EU
→ text/csv "import-template-{category}-v{version}.csv" rendered from that SAME schemaResolution uses the identical effective-schema lookup the create and import paths run (T101) — what discovery returns is what validation enforces. A category is only usable when its schema row has a non-null effective_from.
dpp and vehicle whose schema rows have effective_from = NULL — they then fail on create with 422 SCHEMA_NOT_FOUND. Don't trust a hint blindly; confirm against a successful create. (Tracked, status Partial.)