### GET /api/v1/whoami Source: https://opendpp-node.eu/openapi.json Returns a compact, integration-focused view of the calling credential: the workspace, the principal's role and resolved permissions, whether the session is an API key, the operator the key is scoped to (`null` = workspace-wide), and active-passport usage against the tier quota. Use it to verify a key works, discover the effective permission set, and surface remaining quota. This is the public counterpart to the console's `GET /api/v1/me`; profile, localization and billing details are intentionally not exposed here. **Permission:** none beyond a valid tenant-scoped session — any API key can call it. Platform-admin sessions are rejected with `403` (they are not tenant-scoped). **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). ```markdown ### Responses #### 200 - The authenticated identity. **WhoamiResponse** - **success** (boolean) (required) - **tenant** (object) (required) - **id** (string) (required): Workspace (tenant) id. - **name** (string) (required): Workspace company name. - **subdomain** (string,null) (required): Workspace subdomain (`.opendpp-node.eu`), or null if none is assigned. - **tier** (string) (required): Subscription tier (e.g. `pilot`, `micro`, `starter`, `growth`, `scale`, `enterprise`). - **subscriptionStatus** (string,null) (required): Billing status string (e.g. `active`, `past_due`). When not `active`, write operations may return `402`. No amounts or processor identifiers are exposed. - **auth** (object) (required) - **role** (string) (required): The principal's role. - **permissions** (array (string)) (required): Effective permission strings, re-derived server-side from the role (never trusted from the token). May include wildcards like `operator:*`. - **isApiKeySession** (boolean) (required): `true` when authenticated with an API key, `false` for a session JWT. - **operatorId** (string,null) (required): The economic operator this credential is scoped to, or `null` for a workspace-wide key. A scoped key's writes and reads are restricted to this operator. - **usage** (object) (required) - **activePassports** (integer) (required): Active (non-draft, non-archived) passports counted against the quota — operator-scoped for a scoped key. - **passportLimit** (integer,null) (required): Tier passport quota, or `null` for an unlimited tier. #### 401 - response **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. #### 403 - The session is not scoped to a tenant workspace (e.g. a platform-admin session). **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. #### 429 - response - **statusCode** (integer) (required) - **code** (string) - **error** (string) (required) - **message** (string) (required) #### 500 - response **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. ### Example Usage ```bash curl -X GET "https://opendpp-node.eu/api/v1/whoami" ``` ``` -------------------------------- ### GET /api/v1/operators/{id} Source: https://opendpp-node.eu/openapi.json Fetches one economic operator by UUID, scoped to your workspace (`404` if no operator with that id exists in your workspace). **Permission:** `operator:read`. An **operator-scoped API key** may only fetch its own operator (`403` otherwise). **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). ```markdown ### Parameters - **id** (string, path, required): Operator UUID (`EconomicOperator.id`). Must be bound to your workspace. ### Responses #### 200 - The operator. **OperatorGetResponse** - **success** (boolean) (required) - **operator** (object) (required): An economic-operator record (`EconomicOperator`). Operators are scoped to your workspace (each workspace keeps its own row for a given `regId`). Returned verbatim from the database (no field stripping); nullable fields are serialized as `null`. - **id** (string) (required): Operator UUID. - **name** (string) (required): Legal/display name of the operator. - **regId** (string) (required): Official registration id (EORI number, VAT id, DUNS, or national business-registry id). Unique within your workspace and immutable after registration. - **regIdScheme** (string,null) (required): Which kind of registration id `regId` is. `null` = unspecified national/business id. When `EORI`, `regId` is guaranteed to satisfy the EORI syntax `^[A-Z]{2}[A-Za-z0-9]{1,15}$`. ("EORI"|"VAT"|"DUNS"|"NATIONAL"|"OTHER"|"null") - **role** (string) (required): Supply-chain role, free text — e.g. `"MANUFACTURER"`, `"IMPORTER"`, `"RETAILER"`. Defaults to `"MANUFACTURER"` at registration. - **archivedAt** (string,null) (required): Soft-delete / cessation-of-trading marker. Non-null = the operator is archived (its passports are retained and still publicly resolvable). - **createdAt** (string (date-time)) (required) #### 401 - response **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. #### 403 - response **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. #### 404 - No operator with that id exists in your workspace. Body: { success: false, error: "Not Found", message: "Operator not found in your workspace." }. **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. #### 429 - response - **statusCode** (integer) (required) - **code** (string) - **error** (string) (required) - **message** (string) (required) #### 500 - response **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. ### Example Usage ```bash curl -X GET "https://opendpp-node.eu/api/v1/operators/{id}" ``` ``` -------------------------------- ### Schema: GrantListResponse Source: https://opendpp-node.eu/openapi.json List envelope for `GET /api/v1/grants` (paginated). ```markdown ## Schema: GrantListResponse List envelope for `GET /api/v1/grants` (paginated). **Type:** object - **success** (boolean) (required) - **count** (integer) (required): Number of items returned in THIS page (≤ `limit`). - **page** (integer) (required): 1-based page number returned. - **limit** (integer) (required): Effective page size (default 100, max 200). - **total** (integer) (required): Total items matching across all pages. - **totalPages** (integer) (required): Total number of pages (≥ 1). - **grants** (array (GrantRow)) (required): Grants for this page (≤ `limit`), ordered by `status` ascending then `createdAt` descending. Array items: - **id** (string) (required) - **status** (string (PENDING|ACTIVE|DENIED|REVOKED)) (required): `PENDING` = undecided third-party request (no token exists yet); `ACTIVE` = usable token; `DENIED` = rejected request; `REVOKED` = soft-revoked. ("PENDING"|"ACTIVE"|"DENIED"|"REVOKED") - **kind** (string (LEGITIMATE_INTEREST|AUTHORITY)) (required): `LEGITIMATE_INTEREST` (`dpp_li_…` tokens, tenant-issued or approved from a request) or `AUTHORITY` (`dpp_auth_…` tokens, platform-issued for market surveillance; not tenant-revocable). ("LEGITIMATE_INTEREST"|"AUTHORITY") - **granteeName** (string) (required) - **granteeEmail** (string,null) (required) - **organization** (string,null) (required) - **purpose** (string,null) (required): The stated legitimate interest. - **scopeType** (string (UNIT|PASSPORT|TENANT)) (required): What the token unlocks on the public resolvers: a single battery unit, a single passport, or the whole workspace. ("UNIT"|"PASSPORT"|"TENANT") - **passportId** (string,null) (required): Set for `PASSPORT` scope, and also for `UNIT` scope (the unit's parent passport). `null` for `TENANT` scope. - **batteryUnitId** (string,null) (required): Set only for `UNIT` scope. - **issuerType** (string (TENANT|PLATFORM|REQUEST)) (required): `TENANT` = issued directly via this API; `REQUEST` = submitted by a third party through the hosted request-access page; `PLATFORM` = platform-admin-issued (AUTHORITY grants). ("TENANT"|"PLATFORM"|"REQUEST") - **issuerEmail** (string,null) (required): E-mail of the issuing user; `null` when issued by an API key or created from a public request. - **decidedAt** (string,null) (required): When a PENDING request was approved/denied; `null` for direct issuances. - **decidedBy** (string,null) (required): The deciding actor: a user e-mail, `API_KEY_` when decided via an API key, or the literal `unknown` in degenerate authentication states. - **expiresAt** (string (date-time)) (required): Hard expiry; the public resolvers reject the token after this instant. PENDING requests carry a provisional 90-day expiry that is replaced on approval. - **revokedAt** (string,null) (required) - **lastUsedAt** (string,null) (required): Last successful use on a public resolver (book-kept best-effort). - **useCount** (integer) (required): Successful public-resolver uses (incremented best-effort). - **createdAt** (string (date-time)) (required) - **revocable** (boolean) (required): Computed: `false` for `AUTHORITY` grants (platform-managed), `true` otherwise. ``` -------------------------------- ### Schema: MaterialVocabularyListResponse Source: https://opendpp-node.eu/openapi.json Envelope of `GET /api/v1/materials`. Caveat: unlike most authenticated endpoints there is NO `success` field. ```markdown ## Schema: MaterialVocabularyListResponse Envelope of `GET /api/v1/materials`. Caveat: unlike most authenticated endpoints there is NO `success` field. **Type:** object - **materials** (array (MaterialVocabularyRow)) (required): Active vocabulary entries, ordered by `kind` ascending then `name` ascending, capped at `limit` (max 1000). Array items: - **id** (string) (required) - **name** (string) (required): Canonical display name, e.g. "Organic Cotton" or "Lithium Iron Phosphate (LFP)". - **kind** (string (material|fiber|chemistry|substance|hazard|crm)) (required): Vocabulary kind. `crm` = critical raw material. ("material"|"fiber"|"chemistry"|"substance"|"hazard"|"crm") - **casNumber** (string,null) (required): Optional CAS registry number (chemicals/substances); null when not applicable. - **description** (string,null) (required): Optional short note shown in the picker; null when unset. ``` -------------------------------- ### POST /api/v1/passports Source: https://opendpp-node.eu/openapi.json Creates a SKU/type-level Digital Product Passport. **Permission:** `passport:create` (Bearer `op_dpp_token_…` API key or session JWT; cookie sessions must also send the `X-CSRF-Token` double-submit header). Write operations are subject to subscription gating (**402**) and, where the workspace enforces it, MFA (**403**). **Rate limit:** global 100 requests/min per IP (`x-ratelimit-*` headers). **Body limit: 1 MiB (1,048,576 bytes)** → **413** beyond that. **Validation.** Unless `draft: true`, `metadata` is validated against the ESPR category rules for `metadata.category` plus cross-field rules (e.g. `materialComposition` percentages must sum to 100 ±0.1, `originCountry` must be a real ISO 3166-1 alpha-2 code), and the product's EPCIS traceability lineage is audited. For five categories (textiles, batteries, electronics, chemicals, construction) the authoritative per-category JSON Schema is served live at `GET /api/v1/schemas/{category}`; the other four (cosmetics, toys, iron-steel, aluminium) are validated by built-in server-side rules and `GET /api/v1/schemas/{category}` returns **404** for them. Failure returns the **400 Validation Failed** body with per-field `errors[]` (plus `warnings[]` when any exist — the key is omitted entirely when there are none). A passing payload may still produce non-blocking `warnings[]`, echoed in the 201. `friendlyMessage` texts are localized via `?lang=` or `Accept-Language` (default `en`); category-validity errors (`metadata.category` missing or unknown) carry no `friendlyMessage`. **Drafts.** `draft: true` skips ALL validation, stores the passport with `status: "DRAFT"` (not publicly resolvable), returns `message: "Draft passport saved"` with `warnings: []`, and does **not** emit a webhook. **Identifier handling.** `productId` may be a GTIN-14 (14 digits, GS1 mod-10 check digit), a GRAI (14-digit numeric asset id + up to 16 alphanumeric serial chars), or a free-form SKU. A 14-digit `productId` whose GS1 mod-10 check digit is invalid is rejected with **400** (a typo'd GTIN is never silently downgraded to a SKU); a non-numeric or non-14-digit `productId` is accepted as a non-GS1 SKU and carries a non-blocking `warnings[]` advisory that it resolves via `/passport/{id}` with no scannable GS1 QR. A valid GTIN-14 is auto-copied into `metadata.gtin` (a GRAI into `metadata.grai`) before storage. The server mints a UUID passport id and a GS1 Digital Link URI `https://opendpp-node.eu/{01|8003}/{productId}`. **Operator binding.** With `operatorId` omitted, the passport is attributed to the first economic operator bound to your workspace; if no operator is bound at all the request fails **400** (the API never fabricates an operator identity — register one via `POST /api/v1/operators`). An `operatorId` not bound to your workspace → **403**. Operator-scoped API keys force their own operator and **403** on mismatch. The `(productId, operatorId)` pair is unique → **409** on duplicates. An optional `facilityId` must reference a Facility in your workspace (**400** otherwise). **Webhook:** non-draft creation transactionally enqueues a `passport.ingested` event whose payload is the public redacted JSON-LD passport document (same masking as the 201 `passport` field). Drafts emit nothing. **Response caveats:** the 201 `passport` field is the **public, redacted** JSON-LD representation — even for the creator. The owner-only metadata key `facilityDetails` is replaced with the literal placeholder `"[REDACTED - Privileged Access Required]"` (it appears as the placeholder even when you did not supply it), and for `category: "batteries"` the restricted legitimate-interest keys `detailedPerformance`, `lifecycleAndInUse` and `circularityAndDisassembly` (Battery Reg. Annex XIII parts 2-4) are masked the same way when present. `enrichment` is stored outside the validated metadata and Merkle seal and never appears in the JSON-LD document. The 201 body's top-level fields are `success`, `message`, `passport`, `warnings`, and the `vcReady`/`vcReadyReason` UNTP Verifiable-Credential readiness signal (#247). **Other 400 bodies:** non-validation failures (whitespace-only `productId`, no bound operator, unknown `facilityId`) reuse status 400 with the plain `{"success": false, "error": "Bad Request", "message": …}` triple and **no** `errors`/`warnings` arrays. Requests rejected before the handler runs — request-body schema violations (e.g. missing `productId`) and malformed JSON — come back as just `{"error": "Bad Request", "message": …}`. ```markdown ### Parameters - **lang** (string, query, optional): Locale for localized `friendlyMessage` validation texts. One of: en, bg, hr, cs, da, nl, et, fi, fr, de, el, hu, ga, it, lv, lt, mt, pl, pt, ro, sk, sl, es, sv, no, is, uk, tr. Unknown values are ignored; falls back to `Accept-Language`, then `en`. ### Request Body **Content-Type:** application/json - **productId** (string) (required): Product identifier: a GTIN-14 (exactly 14 digits with a valid GS1 mod-10 check digit — auto-copied to `metadata.gtin`), a GRAI (14-digit numeric asset id with valid check digit + optional up to 16 alphanumeric serial chars, total 14–30 — auto-copied to `metadata.grai`), or a free-form SKU. Determines the GS1 Application Identifier (`01` vs `8003`) in the generated Digital Link URI. Whitespace-only values are rejected 400. Unique per economic operator (409 on duplicate). - **operatorId** (string): UUID of an EconomicOperator bound to your tenant workspace (403 if not bound). Defaults to your workspace's first bound operator. Operator-scoped API keys force their own operator (403 on mismatch). - **facilityId** (string): Optional UUID of a Facility (GLN-backed Unique Facility Identifier) in your workspace; 400 if not found. - **metadata** (object) (required): The ESPR product metadata payload. For non-draft ingestion and for the validate-only endpoints, `category` is mandatory and must be one of the 9 ESPR categories; each category then mandates its own field set (e.g. textiles require `fiberComposition`, `careInstructions`, `size`; batteries require `batteryCategory`, `chemistry`, `electrochemicalCapacity`, `durability`, `recycledContentShare`, `carbonFootprint`). For five categories — textiles, batteries, electronics, chemicals, construction — the authoritative per-category JSON Schema (required fields, value constraints, field help) is served live at `GET /api/v1/schemas/{category}`; the other four (cosmetics, toys, iron-steel, aluminium) are validated by built-in server-side rules and `GET /api/v1/schemas/{category}` returns 404 for them. Cross-field rules are enforced on top: `materialComposition` (and textile `fiberComposition`) percentages must sum to 100 ±0.1, `originCountry` must be a real ISO 3166-1 alpha-2 code, textile hazardous-substance concentrations are checked against REACH ppm limits. A documented set of supplementary objects (e.g. `technicalProperties`, `environmentalFootprint`, `circularityAttributes`, `esgDueDiligence`, `detailedPerformance`) produce non-blocking `warnings` instead of `errors` when malformed. With `draft: true` (single ingestion only) validation is skipped entirely and any object is accepted. - **category** (string (textiles|batteries|electronics|cosmetics|toys|iron-steel|aluminium|chemicals|construction)): ESPR product category; selects the validation rules. Required whenever validation runs (i.e. always, except `draft: true` single ingestion). ("textiles"|"batteries"|"electronics"|"cosmetics"|"toys"|"iron-steel"|"aluminium"|"chemicals"|"construction") - **originCountry** (string): ISO 3166-1 alpha-2 country code (validated against the full 249-code set). - **draft** (boolean): When true: skips ALL ESPR/traceability validation, stores the passport with `status: "DRAFT"` (not publicly resolvable), and emits no webhook. Publish later via a validated edit. - **enrichment** (object): Optional presentational (non-regulatory) marketing enrichment, stored OUTSIDE the ESPR-validated metadata and the Merkle seal; it never appears in the JSON-LD passport document. Server-side it is whitelist-sanitized rather than rejected: unknown keys are dropped; `tagline` is trimmed and capped at 200 chars, `description` at 4000, image `caption` at 200, link `label` at 120; at most 24 `images` and 24 `links` are kept; URLs must be http, https, or mailto (any other scheme, e.g. `javascript:` or `data:`, is silently dropped). An enrichment that sanitizes down to nothing is stored as null. - **tagline** (string): Short marketing tagline (server-capped at 200 chars). - **description** (string): Marketing description (server-capped at 4000 chars). - **images** (array (object)): Up to 24 kept. Items without a valid http/https/mailto `url` are dropped. Array items: - **url** (string (uri)): http, https, or mailto only. - **caption** (string): Server-capped at 200 chars. - **links** (array (object)): Up to 24 kept. Items without a valid http/https/mailto `url` are dropped; a missing `label` defaults to the URL. Array items: - **label** (string): Server-capped at 120 chars. - **url** (string (uri)): http, https, or mailto only. ### Responses #### 201 - Passport created (or draft saved). `passport` is the public redacted JSON-LD document; `warnings` is always present (empty array when none, and always empty for drafts). **PassportIngestCreated** - **success** (boolean) (required) - **message** (string) (required): "Digital Product Passport successfully validated and ingested", or "Draft passport saved" when `draft: true`. - **passport** (object) (required): The public, redacted JSON-LD Digital Product Passport document (`application/ld+json`). All listed top-level keys are ALWAYS present (`null` where not applicable). Additionally, every key of the (masked) `metadata` object — except the reserved document keys (`@context`, `@type`, `@id`, `id`, `productId`, `digitalLinkUri`, `digitalSeal`, `signingPublicKey`, `status`, `archivedAt`, `retentionUntil`, `proof`, `createdAt`, `updatedAt`, `economicOperator`, `manufacturingFacility`, `metadata`) — is ALSO flattened onto the document root for direct semantic-graph querying (hence `additionalProperties: true`); flattened values are identical to the corresponding `metadata` values, including redaction placeholders. Tier-masked metadata keys are replaced (in both places) with the literal string `[REDACTED - Privileged Access Required]`. Masking by tier: anonymous public callers lose the per-category restricted keys (category `batteries`: `detailedPerformance`, `lifecycleAndInUse`, `circularityAndDisassembly` — masked only when actually present) AND the owner-only key `facilityDetails`; legitimate-interest/authority grant holders lose only `facilityDetails`; owner-tier responses are unmasked and additionally include the facility street address fields. Note: `facilityDetails` is placeholder-masked in EVERY non-owner response, even when the underlying metadata never contained the key — in that case it has no entry in `proof.redactedLeaves`. Each masked key that exists in the sealed metadata keeps its true Merkle leaf hash in `proof.redactedLeaves`, so the eIDAS seal stays verifiable offline after redaction (see `MerkleTreeAttestationProof` for the reconstruction rule). - **@context** (array (union)) (required): Exactly two entries: the context URL `https://opendpp-node.eu/contexts/dpp/v1` and an inline term map covering the 9 fixed DPP terms (`DigitalProductPassport`, `economicOperator`, `manufacturingFacility`, `metadata`, `digitalSeal`, `signingPublicKey`, `status`, `archivedAt`, `retentionUntil`) plus one generated term per metadata key (`https://opendpp-node.eu/contexts/dpp/v1#`). - **@type** (string) (required) - **@id** (string (uri)) (required): The passport's canonical GS1 Digital Link URI (same value as `digitalLinkUri`). - **id** (string) (required): Server-assigned passport UUID. - **productId** (string) (required): Caller-supplied product identifier: a GTIN-14 (`^[0-9]{14}$` with valid GS1 modulo-10 check digit), a GRAI (`^[0-9]{14}[A-Za-z0-9]{0,16}$`), or a free-form SKU. - **digitalLinkUri** (string (uri)) (required): SKU/type-level GS1 Digital Link URI: `{BASE_URL}/{01|8003}/{productId}` (AI-21 carries the passport UUID at SKU level; individual units carry their physical serial instead). - **digitalSeal** (string,null) (required): eIDAS ADVANCED electronic seal: base64 ECDSA prime256v1 (P-256) signature over the Merkle root of the key-sorted metadata. `null` when the passport has not been sealed. - **signingPublicKey** (string,null) (required): PEM public key that verifies `digitalSeal`. `null` when unsealed. - **status** (string (DRAFT|ACTIVE|RECALLED|DECOMMISSIONED)) (required): Passport lifecycle status (serialized as `ACTIVE` when unset). `DRAFT` is only ever visible to owner-tier callers — public/grant resolution of a draft returns 404. ("DRAFT"|"ACTIVE"|"RECALLED"|"DECOMMISSIONED") - **archivedAt** (string,null) (required): Soft-delete marker (owner off-boarded / decommissioned). Archived passports remain publicly resolvable (ESPR persistence duty). - **retentionUntil** (string,null) (required): Minimum-availability deadline; the passport is never purged before this instant. - **proof** (object) (required): OpenDPP's own proof type — an eIDAS ADVANCED electronic seal: an ECDSA prime256v1 signature over a SHA-256 Merkle root of the key-sorted metadata (one leaf per top-level metadata key). Deliberately NOT a W3C DataIntegrityProof / `ecdsa-jcs-2019` Verifiable Credential (no RFC 8785 JCS canonicalization). Verifiable offline: rebuild the Merkle root from `metadata` — substituting each `redactedLeaves` hash for its placeholder-masked key, and EXCLUDING any placeholder-masked key that has no `redactedLeaves` entry (such a key was never present in the sealed metadata; the serializer injects the owner-only placeholder unconditionally) — then verify `signatureValue` with `publicKeyPem`; the `x5c` chain validates against the platform seal CA (`GET /.well-known/opendpp-seal-ca.pem`) and the `rfc3161` token via `openssl ts -verify`. - **@type** (array (string)) (required): Always `["MerkleTreeAttestationProof"]`. - **type** (string) (required) - **signatureAlgorithm** (string) (required) - **created** (string (date-time)) (required): Mirrors the passport's `updatedAt`. - **proofPurpose** (string) (required) - **verificationMethod** (string (uri)) (required): `https://opendpp-node.eu/passport/{passportId}#key-1`. - **signatureValue** (string) (required): Base64 ECDSA P-256/SHA-256 signature over the hex Merkle root string (same value as the document's `digitalSeal`). - **publicKeyPem** (string) (required): PEM public key for verification (same value as the document's `signingPublicKey`). - **x5c** (array (string)): OPTIONAL (omitted when no chain was recorded at seal time). X.509 chain as base64 DER (no PEM armor), leaf first, binding the signing key to the tenant's legal identity; issued by the platform seal CA. Denormalised at seal time, so later key/cert rotations never retroactively change a proof. - **rfc3161** (object): OPTIONAL (omitted when timestamping was off/unavailable at seal time). RFC 3161 trusted timestamp over SHA-256(merkleRoot) — an independent existed-at anchor from the configured TSA. - **genTime** (string,null) (required): TSA generation time. - **token** (string) (required): Base64 DER RFC 3161 TimeStampToken; verifies offline via `openssl ts -verify`. - **merkleRoot** (string) (required): Hex SHA-256 Merkle root over the key-sorted metadata leaves. - **redactedLeaves** (object): OPTIONAL — present only when at least one masked key actually exists in the underlying sealed metadata. Maps each such metadata key to its TRUE hex leaf hash, so the Merkle root can be reconstructed from the redacted document. A masked key that was never present in the metadata (the owner-only key is placeholder-injected unconditionally for non-owner tiers) yields NO entry here — verifiers must exclude placeholder-valued keys without an entry when rebuilding the tree. - **createdAt** (string (date-time)) (required) - **updatedAt** (string (date-time)) (required) - **economicOperator** (object) (required): Embedded economic-operator JSON-LD node (public in all tiers). - **@type** (string) (required) - **id** (string) (required) - **name** (string) (required) - **regId** (string) (required): EORI number or official business-registry identifier (unique platform-wide), e.g. `EU-DEFAULT-001`. - **role** (string): Operator role in the supply chain, e.g. `MANUFACTURER`, `IMPORTER`, `RETAILER`. Always present in detail/resolution responses; absent from `GET /api/v1/passports` list items. - **manufacturingFacility** (object) (required): Embedded manufacturing-facility JSON-LD node — the GS1 GLN-backed Unique Facility Identifier (UFI, EN 18219). The five listed fields are public; `streetAddress`/`city`/`postalCode` appear ONLY in owner-tier responses (never via legitimate-interest grants). - **@type** (string) (required) - **id** (string) (required) - **gln** (string) (required): GS1 GLN-13 with a valid modulo-10 check digit. - **name** (string) (required) - **activity** (string,null) (required): What the facility does in the chain, e.g. `cell assembly`. - **country** (string) (required): ISO 3166-1 alpha-2 country code. - **streetAddress** (string,null): Owner tier only — omitted from public and grant-tier responses. - **city** (string,null): Owner tier only. - **postalCode** (string,null): Owner tier only. - **metadata** (object) (required): The ESPR category metadata, tier-masked: keys above the caller's tier hold the literal string `[REDACTED - Privileged Access Required]` instead of their value. - **warnings** (array (ValidationErrorItem)) (required): Non-blocking validation findings. Always present; empty array when none and always empty for drafts. Array items: - **path** (string) (required): Dot/bracket path of the offending metadata field. - **message** (string) (required): Technical validation message. - **friendlyMessage** (string): Localized, human-friendly explanation (language from `?lang=` or `Accept-Language`; 28 languages, default `en`). - **vcReady** (boolean): #247: whether this passport can emit a UNTP Verifiable Credential — true only when a manufacturing facility with a country of production is linked (`producedAtFacility` + `countryOfProduction` are required by the UNTP DPP schema; a GLN is optional). The passport still publishes and resolves as AAS / JSON-LD / HTML regardless. - **vcReadyReason** (string,null): Null when `vcReady` is true; otherwise a short, actionable reason (link a facility with a country of production). #### 400 - Three variants share this status: (1) **Validation Failed** — the metadata failed ESPR category / cross-field / traceability validation; carries per-field `errors[]` (and `warnings[]` only when at least one warning exists). (2) **Bad Request** triple — whitespace-only `productId`, a malformed GTIN-14 `productId` (14 digits failing the GS1 mod-10 check), no economic operator bound to the workspace, or unknown `facilityId`; `{success, error, message}` with no `errors`/`warnings`. (3) Pre-handler rejections — request-body schema violations (e.g. missing `productId`) and malformed JSON return only `{error, message}`. - **success** (boolean) (required) - **error** (string) (required) - **message** (string) (required) - **errors** (array (ValidationErrorItem)) (required): Blocking findings. Items produced by the category-validity check (`metadata.category` missing/unknown) carry no `friendlyMessage`. Array items: - **path** (string) (required): Dot/bracket path of the offending metadata field. - **message** (string) (required): Technical validation message. - **friendlyMessage** (string): Localized, human-friendly explanation (language from `?lang=` or `Accept-Language`; 28 languages, default `en`). - **warnings** (array (ValidationErrorItem)): Omitted entirely when there are no warnings. Array items: #### 401 - response **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. #### 402 - response **PassportQuotaError** - **success** (boolean): Always `false` when present. - **error** (string) (required): Short error title. - **message** (string) (required): Human-readable explanation. - **code** (string): Machine-readable discriminator. `passport_quota_exceeded` = the tier's passport cap is reached; omitted for a lapsed-subscription 402. - **quota** (object): Present only on a `passport_quota_exceeded` block: current usage vs the tier cap. - **tier** (string): The workspace subscription tier. - **activePassports** (integer): Active (non-draft, non-archived) passports, counted tenant-wide. - **passportLimit** (integer): The tier's passport cap. - **upgradeUrl** (string): Where the workspace owner can upgrade the plan. #### 403 - response **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. #### 409 - A passport already exists for this `(productId, operatorId)` pair. **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. #### 413 - Body exceeds the 1 MiB (1,048,576-byte) body limit. - **statusCode** (integer) (required) - **code** (string) - **error** (string) (required) - **message** (string) (required) #### 429 - response - **statusCode** (integer) (required) - **code** (string) - **error** (string) (required) - **message** (string) (required) #### 500 - response **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. ### Example Usage ```bash curl -X POST "https://opendpp-node.eu/api/v1/passports?lang=string" \ -H "Content-Type: application/json" \ -d '{ "productId": "string", "operatorId": "string", "facilityId": "string", "metadata": "value", "draft": "false", "enrichment": "value" }' ``` ``` -------------------------------- ### Schema: HealthStatus Source: https://opendpp-node.eu/openapi.json Health-check body of `GET /health`. Carries the running build identity (`apiVersion`/`commit`/`builtAt`) in addition to the liveness fields. ```markdown ## Schema: HealthStatus Health-check body of `GET /health`. Carries the running build identity (`apiVersion`/`commit`/`builtAt`) in addition to the liveness fields. **Type:** object - **status** (string) (required) - **service** (string) (required) - **timestamp** (string (date-time)) (required): Current server time, ISO 8601 UTC with milliseconds. - **apiVersion** (string) (required): SemVer of the public API contract currently served (equals the OpenAPI document's `info.version`; its MAJOR equals the `/api/v1` URL major). - **commit** (string) (required): Short git commit SHA of the running build, or `"unknown"` when a build did not inject it. - **builtAt** (string) (required): Build/deploy timestamp (ISO 8601 UTC), or `"unknown"` when a build did not inject it. ``` -------------------------------- ### GET /api/v1/materials Source: https://opendpp-node.eu/openapi.json Lists active entries from the platform-global material vocabulary that powers the searchable material/fiber/chemistry pickers in the passport form. This is shared reference data, deliberately **not** tenant-scoped, so DPP data stays comparable across tenants. **Auth:** any authenticated session — Bearer API key, Bearer JWT, or `opendpp_session` cookie. **No specific permission string is required** and subscription status is not checked, so this endpoint never returns 402. On a tenant-subdomain host, credentials belonging to a different tenant receive **403** with message `Cross-tenant access blocked.`. **Filtering & ordering:** `kind` filters by vocabulary kind — an unrecognized value is **silently ignored** (the filter simply isn't applied; no 400). `search` is a trimmed, case-insensitive substring match on `name` (blank values ignored). Only active entries are returned, ordered by `kind` ascending then `name` ascending. `limit` is clamped to 1–1000 (default 1000); there is no pagination. **Envelope caveat:** the 200 body is `{ "materials": [...] }` — there is **no `success` field** on this endpoint. **Curation:** this vocabulary is curated by the platform operator — the API is read-only for tenant credentials. Free-text material values in passport metadata remain allowed but are never auto-added to this vocabulary. **Rate limit:** global limiter only — 100 requests/min/IP (standard `x-ratelimit-*` headers). ```markdown ### Parameters - **kind** (string (material|fiber|chemistry|substance|hazard|crm), query, optional): Filter by vocabulary kind. Unrecognized values are silently ignored (no error; the filter is not applied). - **search** (string, query, optional): Case-insensitive substring match on the entry `name` (value is trimmed; blank values ignored). - **limit** (integer, query, optional): Maximum number of entries to return. Clamped to 1–1000 (out-of-range numeric values are clamped, not rejected); a non-numeric value falls back to the default 1000. ### Responses #### 200 - Active vocabulary entries matching the filters, ordered by `kind` then `name` ascending. Note: no `success` field in this envelope. **MaterialVocabularyListResponse** - **materials** (array (MaterialVocabularyRow)) (required): Active vocabulary entries, ordered by `kind` ascending then `name` ascending, capped at `limit` (max 1000). Array items: - **id** (string) (required) - **name** (string) (required): Canonical display name, e.g. "Organic Cotton" or "Lithium Iron Phosphate (LFP)". - **kind** (string (material|fiber|chemistry|substance|hazard|crm)) (required): Vocabulary kind. `crm` = critical raw material. ("material"|"fiber"|"chemistry"|"substance"|"hazard"|"crm") - **casNumber** (string,null) (required): Optional CAS registry number (chemicals/substances); null when not applicable. - **description** (string,null) (required): Optional short note shown in the picker; null when unset. #### 401 - response **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. #### 403 - response **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. #### 429 - response - **statusCode** (integer) (required) - **code** (string) - **error** (string) (required) - **message** (string) (required) #### 500 - response **Error** - **success** (boolean): Always `false` when present. Omitted by public endpoints and some self-service endpoints. - **error** (string) (required): Short error title (usually the HTTP reason phrase). - **message** (string) (required): Human-readable explanation. ### Example Usage ```bash curl -X GET "https://opendpp-node.eu/api/v1/materials?kind=material&search=string&limit=1000" ``` ```