Developer Docs
Sandbox · API v2PlaygroundGet API keys

Errors & conventions

Assert these in every integration. The platform's promise is that refusals are self-diagnosing, the error body alone should tell a misconfigured client what to fix.

Universal request conventions

ConventionContract
AuthenticationAuthorization: Bearer <api-key-or-oauth-token> on every call. x-api-key-shaped headers also pass the edge.
IdempotencyIdempotency-Key: <uuid> on mutating POSTs. Required on publish-dpp and oauth/token; accepted & recommended elsewhere.
Content typeapplication/json unless the endpoint says multipart.
ErrorsTyped envelope { error: { code, message, details? } }.
CorrelationOptional X-Correlation-Id flows route → use case → audit log.

The error envelope

JSON
{
  "error": {
    "code": "API_SCOPE_DENIED",
    "message": "Missing required scope for this operation.",
    "details": { "required": "products:create", "available": ["products:view"] }
  }
}

Error catalogue

The codes an integrator must branch on. Two are documented deviations, asserted as-is until the underlying contract changes.

StatusCodeTrigger
401-Bad/garbage or missing credential; also role-level permission denials.
403API_SCOPE_DENIEDGood credential, missing scope. Carries { required, available }.
403FORBIDDENOut-of-scope on publish-dpp, different auth wrapper. Deviation.
403SANDBOX_EXPIREDSandbox tenant past expiry (both conditions required).
403GUARD_REJECTEDIllegal passport lifecycle transition (e.g. EDIT a published passport).
400VALIDATION_ERRORField validation, names the field/bound (e.g. 21-char serial, description < 10).
400CALLBACK_URL_UNSUPPORTEDPOST /api/v2/ingest/products sent callbackUrl. There is no completion callback; poll the status URL.
422EXTENSIONS_REQUIREDMissing extensions on product create.
422SCHEMA_NOT_FOUNDCategory has no effective schema (see the category-hint gotcha).
422MISSING_REQUIRED_CREDENTIALSPOST /api/v2/dpp/generate: a required credential was not received, so no passport was issued and nothing was sent to the EU registry. details.missingCredentials names them; details.issued is false.
502ISSUANCE_FAILED / ISSUANCE_NOT_CONFIRMEDPOST /api/v2/dpp/generate: the credential could not be delivered to the manufacturer wallet, or issuance did not reach a confirmed complete state. No passport was issued and nothing was sent to the EU registry; details.status links the request status.
400INVALID_GTIN / INVALID_SERIALBad check digit / over-long serial at the resolver.
404PRODUCT_NOT_FOUNDUnknown-but-valid GTIN at the resolver, names gtin/serial/host.
404-Draft/nonexistent public passport; cross-tenant fetch. Authz runs before existence, no id enumeration.
404PASSPORT_NOT_FOUNDGET /api/v2/public/passport/{uid}: no passport with this id was ever published. A draft is not found.
404PASSPORT_AMBIGUOUSThe code matches more than one published passport. No passport, count or company is named.
410SERIAL_VOIDEDVoided serialized item, returns a tombstone, zero passport content.
410PASSPORT_GONEGET /api/v2/public/passport/{uid}: the passport is no longer available. For a voided serial or passport, X-DPP-Gone-Reason is serial_voided or passport_voided and a Link header with rel="successor-version" points at the replacement when one is recorded. A battery that has ceased carries neither header: its status is not public. A withdrawn, recalled, suspended or archived passport that was published is never 410: it stays available.
422UNKNOWN_EVENT_TYPEWebhook subscription to a non-catalogue event name.
422RETIRED_EVENT_TYPEWebhook subscription names a retired event type; the message names the type it was replaced by.
422PASSPORT_NOT_PUBLISHABLERegistry broadcast attempted on a passport whose lifecycle state is not publishable.
409DUPLICATE_GTINA second model-granularity product with the same GTIN in this tenant, (company_id, gtin) is unique.
409ALREADY_REVOKEDRevoking a key that is already revoked. Note that rotate revokes the old id for you and returns a NEW data.id, revoke that one.
409-Conflict: re-void, same-key different-body idempotency, scheme-lock change, illegal passport command transition.
503COMPLIANCE_EVALUATION_UNAVAILABLEBattery only, and deliberate: the governed battery obligation pack is quarantined, so no battery compliance verdict may be issued. This is NOT_EVALUATED, a fail-closed refusal to judge, never a compliance failure. Every other category evaluates normally; start on electronics.
503EIDAS_ACTOR_BINDING_UNAVAILABLEPOST /api/v2/products/{id}/publish-dpp: no eIDAS actor identity can be resolved for the caller yet, so nothing is published, anchored or issued. DELETE /api/v2/dpp/{productId} and POST /api/v2/backup/passport/{dppId}/market-status answer the same 503, in the standard error envelope, whether or not the passport was ever published: nothing is revoked or attested. A PUT or PATCH of /api/v2/products/{id} that writes category, gtin or customData answers the same 503 when the passport has ever been published: nothing is changed. On registerDPP it arrives as a 501 with no body, in the X-Norruva-Error-Code header. So does a DELETE of /api/v1/dpps/{dppId} or /api/v2/dpps/{dppId}, or a PATCH to either that writes category or customData, when the passport has ever been published: nothing is archived or changed. Through /dpps/{dppId}, a passport that was never published can still be changed and archived. These refusals happen in every deployed environment. In local development and test the calls proceed, so you can test your integration. Each operation that can answer with this code says so in its responses in the OpenAPI specification.
503REGISTRY_UNAVAILABLENo external registry (CATENA-X / GS1 / EBSI) is configured, the normal sandbox answer to POST /passports/{id}/publish.
503ANCHORING_UNAVAILABLEBlockchain not configured in this environment, the normal sandbox answer.
500ANCHORING_FAILEDA real anchoring fault on a configured chain. Distinct from ANCHORING_UNAVAILABLE (503): only this one means something is wrong.
405-Wrong verb on a route that does not support it. (Product update accepts PATCH, canonical, and PUT as an alias.)
302-Resolver redirect to /p/{uid} (the public uid = the product id).
202-Async accepted (generation, imports).
Rate limiting is enforced: per-tier budgets on a Redis-backed limiter. Over budget → 429 RATE_LIMITED with X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset and Retry-After. Back off on Retry-After.

Idempotency semantics

  • Same key + same body → the first result is replayed, with no duplicate side effects.
  • Same key + different body → a typed conflict.
  • Enforcement is per-route: publish-dpp and oauth/token require a key; POST /products without one still returns 201.
Was this page helpful?
Thanks, noted.Feedback goes to the docs team by email.