{
  "openapi": "3.1.0",
  "info": {
    "title": "TracePass API",
    "version": "1.0.0",
    "summary": "Digital Product Passport REST API for the TracePass platform.",
    "description": "REST API for managing Digital Product Passports on TracePass —\nproducts, passports, parties (economic operators), bulk batches,\nand the JSON-LD tenant export.\n\nMost operations require an API key in the standard\n`Authorization: Bearer <key>` header. v1 API access is available\non every plan including Free. Daily caps apply only to Free\n(100 writes and 100 passport reads per API key per UTC day) and\nBasic (200 + 200) as free-tier abuse prevention; other reads are\nuncounted, and Starter and above are unlimited.\n\nWorked examples in curl / TypeScript / Python live alongside\neach endpoint at https://www.tracepass.eu/docs.\n",
    "contact": {
      "name": "TracePass support",
      "email": "support@tracepass.eu",
      "url": "https://www.tracepass.eu"
    },
    "license": {
      "name": "TracePass Terms of Service",
      "url": "https://www.tracepass.eu/terms"
    },
    "termsOfService": "https://www.tracepass.eu/terms"
  },
  "servers": [
    {
      "url": "https://app.tracepass.eu",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Prose documentation with worked examples",
    "url": "https://www.tracepass.eu/docs"
  },
  "tags": [
    {
      "name": "Passports",
      "description": "Create, read, update, suspend, archive, delete, and bulk-import\nDigital Product Passports. Includes the parties block for economic-\noperator chains.\n",
      "externalDocs": {
        "url": "https://www.tracepass.eu/docs/passports"
      }
    },
    {
      "name": "Products",
      "description": "The catalogue layer — products, images, defaults. One product\ncan have many passports (one per serialised unit).\n",
      "externalDocs": {
        "url": "https://www.tracepass.eu/docs/products"
      }
    },
    {
      "name": "Exports",
      "description": "Bulk JSON-LD tenant export. Dashboard-cookie auth, not API\nkey — see the operation's security block.\n",
      "externalDocs": {
        "url": "https://www.tracepass.eu/docs/exports"
      }
    },
    {
      "name": "EPCIS",
      "description": "GS1 EPCIS 2.0 supply-chain events — export a passport's event\nhistory, capture events from partners and ERP systems, and\nquery the event store. Export is included on Starter plans and\nup; capture and query are a paid add-on.\n",
      "externalDocs": {
        "url": "https://www.tracepass.eu/docs/epcis"
      }
    },
    {
      "name": "Templates",
      "description": "The regulatory DPP schemas — what a compliant passport must\ncontain per category, and the EU regulation behind each\nrequired field. Read-only reference data; use it to discover\nrequirements before creating products and passports.\n",
      "externalDocs": {
        "url": "https://www.tracepass.eu/docs"
      }
    },
    {
      "name": "Scope",
      "description": "EU DPP regulatory scope checking — determine whether a product\nfalls in scope of DPP obligations, run a catalogue-wide scope\nanalysis, and receive webhook notifications when scope status\nchanges. Single-check (`POST /api/v1/scope/check`) is available\non every plan; catalogue analysis requires Growth or higher.\n",
      "externalDocs": {
        "url": "https://www.tracepass.eu/docs/check-scope"
      }
    },
    {
      "name": "Regulatory",
      "description": "Regulatory data endpoints for the DPP Assistant — required DPP\nfields by category with EU article citations, recent regulatory\nchanges, registry guidance, and the company compliance profile\n(GET / PATCH / DELETE). Free with a TracePass account; daily\nanswer cap shared across dashboard, API and MCP connections.\n",
      "externalDocs": {
        "url": "https://www.tracepass.eu/docs/dpp-assistant"
      }
    },
    {
      "name": "OAuth",
      "description": "OAuth 2.0 authorization-server endpoints (authorization-code\nflow with PKCE). These power the user-authorized auth method —\na third-party app or AI assistant a user \"Connects\". Not v1\nresource endpoints; documented here so client libraries can\ndrive the flow. Discovery metadata at\n`/.well-known/oauth-authorization-server` (RFC 8414).\n",
      "externalDocs": {
        "url": "https://www.tracepass.eu/docs/authentication"
      }
    }
  ],
  "security": [
    {
      "BearerAuth": []
    },
    {
      "OAuth2": [
        "passports:read"
      ]
    }
  ],
  "paths": {
    "/api/v1/passports": {
      "post": {
        "operationId": "createPassport",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Create a single passport.",
        "description": "Create a single Digital Product Passport. The passport is\nbound to a product (`productId`) and identified by a GS1\nGTIN + serial unique within that GTIN. New passports start\nin `draft` status — fields are populated via subsequent\nPATCH calls and the passport is published from the\ndashboard once review is complete.\n\nCounts as one v1 write. A draft takes no slot from the\nplan's `maxDpps` limit, so create never returns an overage\nerror and never charges. The plan limit, and any\nper-passport overage charge, apply when the passport is\npublished in the dashboard. A `402` here means billing is\nnot in good standing (see `PaymentRequired`).\n\n**Second-life batteries (Art. 77(7) of Regulation (EU) 2023/1542).**\nA battery that is repurposed, remanufactured or prepared for reuse\ngets a new passport linked to the original one(s). Send a `lineage`\nblock naming each predecessor, either by `internalPassportId` (one\nof your own passports) or by its resolvable `identifier`. The\nplatform derives `batteryStatus` from the triggers, stores the block\nimmutably, and adds a `successors` link on your own predecessor\npassports. A rule violation returns 422 with a `code`-style `error`\n(see `PassportCreateUnprocessable`).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePassportInput"
              },
              "example": {
                "productId": "6650a1b2c3d4e5f6a7b8c9d0",
                "gs1": {
                  "gtin": "04012345000015",
                  "serialNumber": "BP-48V-100-000001"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Passport created. When the `parties` block carries an LEI\nthat GLEIF reports as lapsed, retired or inactive, or whose\nlegal name differs, the response adds `warnings`; an LEI\nGLEIF does not know, or reports as annulled or duplicate,\nis a 400 instead (see `upsertParty`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Passport"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "warnings": {
                          "$ref": "#/components/schemas/LeiWarnings"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/PassportCreateUnprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      },
      "get": {
        "operationId": "listPassports",
        "tags": [
          "Passports"
        ],
        "summary": "List passports.",
        "description": "Paginated list of passports the workspace owns. Filter by\nproduct, status, or a free-text search across GTIN and\nserial.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "name": "productId",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/ResourceId"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PassportStatus"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated passport list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaginatedPassports"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        }
      ],
      "get": {
        "operationId": "getPassport",
        "tags": [
          "Passports"
        ],
        "summary": "Read one passport.",
        "description": "Default response includes the full passport — translatable\nfields carry their `sourceLocale` and a per-locale\n`translations` map. `?lang=<locale>` resolves every field\nthrough the public-viewer chain and returns one resolved\nvalue per field; `?format=full` adds template-derived\nlabels, units, and access levels alongside each value.\n\nAn alternate addressing form exists at\n`GET /api/v1/passports/by-serial/{serial}` with the same\nbody and query-parameter behaviour.\n",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "full"
              ]
            },
            "description": "Set to `full` to include template field labels, units,\nand access levels alongside each field value.\n"
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "ISO 639-1 locale to resolve every field value through\nthe public-viewer chain (viewer-locale translation →\nsource-locale value → English → first-applied →\nuniversal source). One of the 24 EU locales. When set,\nthe `translations` map is dropped from each field.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "Passport.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Passport"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      },
      "delete": {
        "operationId": "deletePassport",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Delete a passport.",
        "description": "What DELETE does depends on the passport's status.\n\n**`marked_for_deletion` — the second step of a deletion.**\nDeleting a passport that has been published takes two steps:\nfirst `POST /api/v1/passports/{id}/mark-for-deletion`, then\nthis DELETE. It is a soft delete: nothing is erased. The\npassport disappears from the API (every v1 read answers 404\nfrom then on) and from every public surface. Scanning its QR\ncode shows a page saying the passport has ended, and its\nmachine-readable formats answer 410 Gone. Returns the passport\nwith `status: deleted` and fires the `passport.deleted`\nwebhook.\n\n**`draft` or `in_review`, never published — permanent delete.**\nStatus must be `draft` or `in_review` AND `publishedAt` must\nbe `null`. Removes the passport with everything that belongs\nonly to it: its extractions, scan and service events,\nownership transfers, EPCIS event links, and any uploaded\ndocuments and stored files no other record uses. Shared\ndocuments stay, with the link removed. Irreversible. No\nwebhook; the deletion is recorded in your account's audit\ntrail. Returns 403 on Free plans with a `reason` pointing at\nthe archive endpoint.\n\n**Any other status — 409.** The body carries a `hint`\n(\"A published passport is deleted in two steps: mark it for\ndeletion first.\") and a `reason`.\n\nDeletion through the API needs a paid plan. Counts as one v1\nwrite. Honours `Idempotency-Key`.\n",
        "responses": {
          "200": {
            "description": "Deleted. A passport that was marked for deletion returns the\npassport with `status: deleted` (soft delete). A\nnever-published passport returns a `message` and a\n`summary` of what was removed (permanent delete).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Passport"
                    },
                    {
                      "$ref": "#/components/schemas/PassportHardDeleteResult"
                    }
                  ]
                },
                "examples": {
                  "softDelete": {
                    "summary": "Soft delete (passport was marked for deletion)",
                    "value": {
                      "_id": "6650b2c3d4e5f6a7b8c9d0e1",
                      "status": "deleted",
                      "deletedAt": "2026-11-02T09:30:00.000Z",
                      "deletedReason": "recycled"
                    }
                  },
                  "hardDelete": {
                    "summary": "Permanent delete (never-published passport)",
                    "value": {
                      "message": "Passport permanently deleted",
                      "summary": {
                        "passportId": "6650b2c3d4e5f6a7b8c9d0e1",
                        "serialNumber": "SN-001",
                        "gtin": "09506000134369",
                        "deletedCounts": {
                          "extractions": 3,
                          "serviceEvents": 0,
                          "scanEvents": 0,
                          "ownershipTransfers": 0,
                          "supplierRequestRefsRemoved": 0,
                          "epcisEventRefsRemoved": 0,
                          "epcisEventsDeleted": 0,
                          "documentRefsRemoved": 1,
                          "documentsDeleted": 1,
                          "filesDeleted": 5
                        },
                        "dppsActiveDelta": 0
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "description": "Plan gate — permanent delete of a never-published passport is paid-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "Permanent delete is not available on your plan",
                  "reason": "Hard-delete is a paid-plan feature. Use POST /api/v1/passports/{id}/archive instead."
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Not eligible: the passport is neither marked for deletion\nnor a never-published draft. Mark it for deletion first.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "Passport is not eligible for permanent deletion",
                  "hint": "A published passport is deleted in two steps: mark it for deletion first.",
                  "reason": "This passport has been published. Archive it instead — the QR remains traceable for audit."
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/mark-for-deletion": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        },
        {
          "$ref": "#/components/parameters/IdempotencyKey"
        }
      ],
      "post": {
        "operationId": "markPassportForDeletion",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Mark a passport for deletion.",
        "description": "The first of the two steps that delete a passport; the second\nis `DELETE /api/v1/passports/{id}`. (A never-published `draft`\nor `in_review` passport can be deleted directly.) Works on a passport in\nany status except archived or already marked for deletion.\n\nA marked passport can no longer be edited. Marking a published\npassport frees its plan slot. Fires the\n`passport.marked_for_deletion` webhook.\n\n**Battery passports need a `reason`.** Under the Battery\nRegulation a battery passport ends only once the battery has\nbeen recycled (Regulation (EU) 2023/1542, Art. 77(8)), so the\nrequest names why this one ends: `recycled`, `never_placed`\n(never placed on the market) or `duplicate_or_error`. Without\none, a battery passport answers 400 `reason_required`. For\nother categories `reason` is optional, and must be one of the\nsame values when given.\n\n**Refused while a newer passport links to it.** While a newer\nsecond-life passport in your account names this one as its\npredecessor (its `lineage` block), the mark answers 409\n`has_live_successor`. Delete the newer passport first.\n\nDeletion through the API needs a paid plan: on Free the mark\nanswers 403 `deletion_not_on_plan`. Counts as one v1 write.\nHonours `Idempotency-Key`. A by-serial alternate exists at\n`POST /api/v1/passports/by-serial/{serial}/mark-for-deletion`.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeletionReasonInput"
              },
              "example": {
                "reason": "recycled"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Marked for deletion. Returns the passport.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Passport"
                },
                "example": {
                  "_id": "6650b2c3d4e5f6a7b8c9d0e1",
                  "status": "marked_for_deletion",
                  "markedForDeletionAt": "2026-11-01T10:00:00.000Z",
                  "markedForDeletionReason": "recycled"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeletionReasonInvalid"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/DeletionNotOnPlan"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/MarkForDeletionConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/compliance": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        }
      ],
      "get": {
        "operationId": "getPassportCompliance",
        "tags": [
          "Passports"
        ],
        "summary": "Compliance verdict for one passport.",
        "description": "Returns a three-tier compliance verdict\n(`compliant` / `compliant_with_warnings` / `incomplete`) with\nregulation-cited findings, so an integration can gap-check a\npassport and re-check after fixing — the read → fix → verify\nloop.\n\nFindings come from three tiers:\n- **static** — required template fields present + approved,\n  required economic-operator parties present (critical), and\n  well-formed field values (warning);\n- **conditional** — per-category rules (battery Reg (EU)\n  2023/1542 Art. 77(1) scope, a warning until the obligation\n  applies on 18 February 2027 and critical from then, plus\n  Annex XIII field-applicability; construction Reg (EU)\n  2024/3110; CE-marking consistency),\n  plus the cross-cutting EU-economic-operator rule\n  (Reg (EU) 2019/1020 Art. 4). A battery whose importer places it\n  on the market without a trade name or postal address gets a\n  warning (`ruleId: \"IMP-1\"`, Art. 41(3)). A company with no\n  stored operator identifier gets advice (`ruleId: \"OID-1\"`,\n  warning, no regulation, since no law requires the identifier in\n  the passport). For batteries, applicability\n  findings (`ruleId: \"BAT-APP\"`, warning) flag fields filled but\n  not applicable to the battery's type — e.g. carbon footprint on\n  a non-rechargeable cell. These reuse the standard\n  `ComplianceFinding` shape (no new fields);\n- **coverage** — `conditionalCoverage` reports whether\n  conditional rules were evaluated for this category or it is\n  `static-only` (no binding conditional in force yet), so the\n  absence of conditional findings is never read as compliance.\n\n`byRegulation` additionally groups those same findings by the\nregulation that raised them, worst first, so a single REACH gap\ndoes not read as \"the DPP obligation is unmet\" when it is fully\nmet. It is derived from the findings above and never contradicts\n`verdict` — the rolled-up verdict stays authoritative for\npublishing. A regulation with no findings is absent from the\narray rather than reported as passing.\n\nRead-only; counts as one v1 passport read against the daily cap.\n",
        "responses": {
          "200": {
            "description": "Compliance verdict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComplianceVerdict"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/registry-readiness": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        }
      ],
      "get": {
        "operationId": "getPassportRegistryReadiness",
        "tags": [
          "Passports"
        ],
        "summary": "Registry-readiness preflight for one passport.",
        "description": "Returns whether a passport would pass the **EU DPP Registry's\nformal submission gate** — `{ ready, findings[] }`. This is the\nregistry's *mechanical* pre-submission check, distinct from and\ncomplementary to the substantive `/compliance` verdict: a\npassport can be registry-ready yet not substantively compliant,\nand vice-versa.\n\nFive formal checks, each carried on the standard\n`ComplianceFinding` shape with a `REG-READY-*` `ruleId`:\n- **`REG-READY-MANDATORY`** — every mandatory field that applies\n  to this battery type is present and approved (applicability-aware:\n  a field that is not applicable to the battery's category is not\n  counted as missing; an `unknown`-applicability field surfaces as\n  a warning, not a block);\n- **`REG-READY-FORMAT`** — filled fields are well-formed\n  (types, enums, patterns, ranges);\n- **`REG-READY-LINK`** — the passport's public link resolves\n  (a live probe; on an unpublished passport this is an advisory\n  warning rather than a block);\n- **`REG-READY-GRANULARITY`** — the passport carries a serial\n  number, since batteries register at item-level granularity\n  (a missing serial is a block);\n- **`REG-READY-COMMODITY`** — the product's commodity code\n  (CN/TARIC) is well-formed where the category carries one; for\n  batteries, which carry no commodity code, this is an advisory\n  warning only, never a block.\n\n`ready` is `true` only when there are zero critical findings.\n**Battery passports only** in this version; a non-battery passport\nreturns `ready: true` with a single `REG-READY-SCOPE` warning.\n\nRead-only; counts as one v1 passport read against the daily cap.\n",
        "responses": {
          "200": {
            "description": "Registry-readiness result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistryReadinessResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/condition-flags": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        }
      ],
      "get": {
        "operationId": "getConditionFlags",
        "tags": [
          "Passports"
        ],
        "summary": "Read the condition-classification flags for a passport.",
        "description": "Returns the resolved condition profile for a passport —\n`Record<flagKey, { value, status, source }>`. Condition flags\nare reviewer-approved yes/no facts (e.g. `hasBMS`,\n`rechargeable`, `externalStorageOnly`, `isStationaryBess` for\nbatteries) that gate conditional legal duties: an `approved`\nflag whose gate applies makes the gated field required, and an\nempty such field is a **hard publish block** (`conditional_missing`).\nAbsent or `pending_review` flags never block publishing.\n\nValid flag keys are category-specific (battery: `hasBMS`,\n`rechargeable`, `externalStorageOnly`, `isStationaryBess`).\nCategories with no flags in the registry return an empty profile.\n\nThe `audit` array is stripped from the v1 response; use the\ndashboard to inspect per-flag history.\n\nRead-only. Counts as one v1 passport read against the daily cap.\n",
        "responses": {
          "200": {
            "description": "Condition profile.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "passportId",
                    "conditionProfile"
                  ],
                  "properties": {
                    "passportId": {
                      "$ref": "#/components/schemas/ResourceId"
                    },
                    "conditionProfile": {
                      "$ref": "#/components/schemas/ConditionProfile"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      },
      "patch": {
        "operationId": "setConditionFlags",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Set or clear condition-classification flags on a passport.",
        "description": "Write one or more condition flags. The body is\n`Record<flagKey, boolean | null>` — `true`/`false` set the flag,\n`null` clears it. Only keys registered for the passport's category\nare accepted (unknown keys return 400).\n\n**Warning:** approving a flag can make previously optional fields\nrequired. If the passport is already published, enabling a flag\nwhose gate applies will raise a `conditional_missing` finding on the\nnext compliance check — fix those fields before or immediately after\nsetting the flag or the passport will be unrepublishable until they\nare filled.\n\nv1 writes default to `status: \"approved\"` (trusted integration).\nEach flag change is audited with `\"via API key <prefix>\"` in the\nflag's internal audit trail.\n\nSupports `Idempotency-Key`. Counts as one v1 write.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConditionFlagsPatch"
              },
              "example": {
                "hasBMS": true,
                "rechargeable": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Condition profile after the update.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "passportId",
                    "conditionProfile",
                    "version"
                  ],
                  "properties": {
                    "passportId": {
                      "$ref": "#/components/schemas/ResourceId"
                    },
                    "conditionProfile": {
                      "$ref": "#/components/schemas/ConditionProfile"
                    },
                    "version": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/PassportArchived"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/snapshots": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        }
      ],
      "get": {
        "operationId": "listPassportSnapshots",
        "tags": [
          "Passports"
        ],
        "summary": "List immutability snapshots for a passport.",
        "description": "Returns a paginated list of **immutability snapshots** for a\npassport, newest first. Each entry re-verifies the stored\ncontent hash so an at-rest tamper is visible (`hashValid: false`)\nrather than silently trusted.\n\nA snapshot is written on publish and after **every change** to a\nnon-draft (published, suspended, expired or archived) passport —\nfield values, statuses, translations, parties, status transitions,\nsupplier answers, AI writes, restores — whenever what the passport\nasserts actually changed (EN 18221:2026 4.2). Changes made\noutside the API are captured too, within a day. Passports that\npredate snapshots start with a `baseline` snapshot.\n\n**Point in time:** `?at=<ISO 8601>` returns the single snapshot\nvalid at that instant — the `SnapshotDetail` shape plus\n`validFrom` and `validUntil` (null while current) — instead of\nthe list. 404 when `at` predates the passport's first snapshot.\n\nRead-only; counts as one v1 passport read against the daily cap.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "name": "at",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Return the snapshot valid at this instant instead of the list."
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated snapshot list, or with `?at=` the snapshot valid then (`SnapshotDetail` plus `validFrom` / `validUntil`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SnapshotList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/snapshots/{snapshotId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        },
        {
          "name": "snapshotId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[0-9a-f]{24}$"
          },
          "description": "The id of the snapshot."
        }
      ],
      "get": {
        "operationId": "getPassportSnapshot",
        "tags": [
          "Passports"
        ],
        "summary": "Full archival record of one snapshot.",
        "description": "Returns the complete authority-level **JSON-LD** the passport\nasserted at snapshot time, plus a re-verification of the content\nhash. A tampered at-rest document surfaces as `hashValid: false`\nrather than being silently trusted.\n\nThe same tenant-scoping and 404-for-foreign-ids semantics as\nthe parent list endpoint apply: an unknown snapshot id and a\nsnapshot that belongs to a different passport or company both\nreturn 404.\n\nRead-only; counts as one v1 passport read against the daily cap.\n",
        "responses": {
          "200": {
            "description": "Snapshot detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SnapshotDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/fields/{key}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        },
        {
          "$ref": "#/components/parameters/FieldKey"
        },
        {
          "$ref": "#/components/parameters/IdempotencyKey"
        }
      ],
      "patch": {
        "operationId": "updatePassportField",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Update one field on a passport.",
        "description": "Patch a single field on a passport. The value is validated\nagainst the field key (must exist on the passport's\ntemplate) and persisted with an audit-trail entry naming the\ncredential (`via API key <prefix>` or `via OAuth app <client>`)\nand, when the `X-Source` header is set, the calling client\n(e.g. `(mcp)`). The channel is recorded only; it never changes\nthe status.\n\nStatus: an API key writes `approved`. An OAuth token writes with\nits user's dashboard rights, so a role below admin lands\n`pending_review`. A value sent with `source: \"ai_suggested\"`\nalways lands `pending_review`, and is refused (400) on a field\nthat only the economic operator may state or that must be\nmeasured (e.g. battery `stateOfHealth`). Send such a value with\nanother source if your organisation is making that statement.\n\n**Identifier lock:** after a passport is first published,\nidentity fields (`gtin`, `serialNumber`, `uniqueProductIdentifier`,\n`globalUniqueProductIdentifier`, `productIdentifier`,\n`identifierStandard`, `batteryUniqueIdentifier`,\n`batteryIdDmcCode`, `batteryPassportIdentifier`) cannot change\nonce they hold an `approved` value. Writing the same value back\n(including a GTIN with or without leading-zero padding, or a\ntrimmed-whitespace match) is a no-op — not a lock. An empty\nfield may be filled; a `pending_review` value may be corrected.\nOn a lock the response is 409 `identifier_locked` and `keys`\nnames the affected fields. Archiving the passport locks ALL\nwrites: 409 `passport_archived`. A concurrent write race\n(another write landed between this read and write) returns 409\n`concurrent_update` — reload and retry.\n\n**No-op:** when the incoming value, computed write status and\nsource locale are all identical to the stored state, the write\nis skipped entirely (no version bump, no audit entry, no\nsnapshot) and the response body carries `unchanged: true`\nalongside the current field and version.\n\n**Required-field guard:** sending `null` or an empty string for\na field the regulation requires for this passport's category is\nrefused with 400 when the passport is already published. The\n`fields` array in the response body names every affected key.\nDraft passports remain freely editable.\n\nAn alternate addressing form exists at PATCH\n`/api/v1/passports/by-serial/{serial}/fields/{key}` with\nthe same body shape.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateFieldInput"
              },
              "example": {
                "value": 5.24
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Field updated, or no-op when the value was already identical.\nWhen `unchanged: true` is present, nothing was written and no\nversion was bumped.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "field",
                    "version"
                  ],
                  "properties": {
                    "field": {
                      "$ref": "#/components/schemas/PassportField"
                    },
                    "version": {
                      "type": "integer",
                      "description": "Current passport version (post-write, or unchanged on a no-op)."
                    },
                    "unchanged": {
                      "type": "boolean",
                      "description": "Present and `true` when the write was a no-op — the incoming value, status and locale matched the stored state so nothing was written.\n"
                    }
                  }
                },
                "examples": {
                  "updated": {
                    "summary": "Field updated",
                    "value": {
                      "field": {
                        "value": 5.24,
                        "source": "manual",
                        "status": "approved",
                        "accessLevel": "public",
                        "sourceLocale": "en",
                        "lastUpdatedAt": "2026-05-09T10:30:00.000Z",
                        "lastUpdatedBy": "api_key:tp_89b2482d"
                      },
                      "version": 4
                    }
                  },
                  "noOp": {
                    "summary": "No-op (unchanged)",
                    "value": {
                      "field": {
                        "value": 5.24,
                        "source": "manual",
                        "status": "approved",
                        "accessLevel": "public",
                        "sourceLocale": "en",
                        "lastUpdatedAt": "2026-05-09T10:30:00.000Z",
                        "lastUpdatedBy": "api_key:tp_89b2482d"
                      },
                      "version": 4,
                      "unchanged": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Covers:\n- invalid field key (key not on template)\n- invalid field value (wrong type or enum option)\n- `ai_suggested` source on a never-AI field\n- emptying a required field on a published passport\n  (`fields` array names the affected keys)\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": [
                        "error"
                      ],
                      "properties": {
                        "error": {
                          "type": "string"
                        },
                        "details": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        }
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "error",
                        "message",
                        "fields"
                      ],
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "required_field_empty"
                          ]
                        },
                        "message": {
                          "type": "string",
                          "example": "Cannot empty required field on a published passport: ratedCapacity"
                        },
                        "fields": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Keys of the required fields that would be emptied.",
                          "example": [
                            "ratedCapacity"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/PassportLocked"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/parties/{role}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        },
        {
          "$ref": "#/components/parameters/PartyRole"
        },
        {
          "$ref": "#/components/parameters/IdempotencyKey"
        }
      ],
      "patch": {
        "operationId": "upsertParty",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Upsert an economic-operator party.",
        "description": "Create or replace the Party for one role on a passport.\nSending the same role twice is an upsert: the existing\nblock is replaced atomically. At least one of `gln`,\n`legacyOperatorId`, or `operatorIdentifier` must be set in\nthe body — without an identifier the Party doesn't actually\nidentify anyone. If both `gln` and `operatorIdentifier`\n(scheme `gln`) are set they must match (400). An\n`operatorIdentifier` of scheme `gln` also fills `gln`.\n\nWhen two roles share the same `gln`, the JSON-LD emission\ncollapses them into a single party with multiple roles\nattached.\n\nAn LEI (`operatorIdentifier` or `facilityIdentifier` with\nscheme `iso6523`, icd `0199`) is verified against the GLEIF\nregister. Rejected (400): not in the GLEIF index,\nANNULLED, DUPLICATE (the message names the LEI to use),\nCANCELLED. Saved with a `warnings` entry: LAPSED (not renewed;\nstill the entity), RETIRED or an inactive entity, or a\n`legalName` that matches none of GLEIF's names for that LEI.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Party"
              },
              "example": {
                "legalName": "EuroBat Recyclers GmbH",
                "gln": "4012345678901",
                "country": "DE",
                "url": "https://eurobat-recyclers.example"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Party upserted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "role",
                    "party",
                    "version"
                  ],
                  "properties": {
                    "role": {
                      "$ref": "#/components/schemas/PartyRoleEnum"
                    },
                    "party": {
                      "$ref": "#/components/schemas/Party"
                    },
                    "version": {
                      "type": "integer"
                    },
                    "warnings": {
                      "$ref": "#/components/schemas/LeiWarnings"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/PassportArchived"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      },
      "delete": {
        "operationId": "deleteParty",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Remove a party from a passport.",
        "description": "Remove the Party for one role on a passport. Idempotent —\nremoving an absent role returns `{ removed: false }`\nwithout bumping the passport version.\n",
        "responses": {
          "200": {
            "description": "Party removed (or already absent).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "role",
                    "removed"
                  ],
                  "properties": {
                    "role": {
                      "$ref": "#/components/schemas/PartyRoleEnum"
                    },
                    "removed": {
                      "type": "boolean"
                    },
                    "version": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/PassportArchived"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/suspend": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        },
        {
          "$ref": "#/components/parameters/IdempotencyKey"
        }
      ],
      "post": {
        "operationId": "suspendPassport",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Suspend a passport (reversible).",
        "description": "Reversible suspend. The public viewer flips to the\nsuspended state page (HTTP 423 with structured body); QR\nscans effectively die without the URL going to 404.\n\nOptional `reason` body surfaces in the\n`passport.suspended` webhook payload and the dashboard\naudit trail (truncated at 500 chars). Empty body is fine.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              },
              "example": {
                "reason": "Quality investigation pending — batch BB-2026-04-12."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Passport"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Wrong status to suspend from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/archive": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        },
        {
          "$ref": "#/components/parameters/IdempotencyKey"
        }
      ],
      "post": {
        "operationId": "archivePassport",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Archive a passport (irreversible).",
        "description": "**Irreversible.** Public viewer returns 404, the GS1\nDigital Link URL stops resolving and the QR code dies for\ngood, unless the passport has been forwarded to a new\nprovider. Use ONLY for products that never shipped — archiving\na passport for a product already in customers' hands\nbreaks every QR scan they'll ever make.\n\nThe HTTP method is POST and the path includes a literal\n`archive` segment, both intentional friction. Fires the\n`passport.archived` webhook.\n\n**Battery passports need a `reason`.** Under the Battery\nRegulation a battery passport ends only once the battery has\nbeen recycled (Regulation (EU) 2023/1542, Art. 77(8)), so\narchiving one names why: `recycled`, `never_placed` (never\nplaced on the market) or `duplicate_or_error`. Without one, a\nbattery passport answers 400 `reason_required`. For other\ncategories the body is optional; a `reason`, when given, must\nbe one of the same values. The reason is carried in the\n`passport.archived` webhook payload.\n\nA passport marked for deletion cannot be archived (409\n`passport_already_marked_for_deletion`).\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeletionReasonInput"
              },
              "example": {
                "reason": "never_placed"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Archived. Idempotent — an already archived passport returns 200 with the existing document.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Passport"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeletionReasonInvalid"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/ArchiveMarkedConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/batch": {
      "post": {
        "operationId": "batchCreatePassports",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Batch-create passports (up to 100 per call).",
        "description": "Create up to 100 passports in one call. Each item carries\nthe same body shape as the single-create endpoint;\npartial-success per item — each gets its own status in the\nresponse array.\n\nThe whole batch consumes N writes upfront against the\ndaily-write budget; if that would overflow, the entire\nbatch returns 429 (no partial billing). Every passport is\ncreated as a draft, and a draft takes no slot from the\nplan's `maxDpps` limit, so a batch never returns an overage\nerror and never charges. The plan limit, and any\nper-passport overage charge, apply when each passport is\npublished in the dashboard. A `402` here means billing is\nnot in good standing (see `PaymentRequired`).\n\nEach item may carry its own `lineage` block (second-life battery\nsuccessors). A lineage rule violation fails that item only: its\nresult has `status: \"error\"` and `error` starting with the rule code,\ne.g. `duplicate_predecessor: …`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchCreatePassportsInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Partial-success batch result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchPassportsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/qr": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        }
      ],
      "get": {
        "operationId": "getPassportQr",
        "tags": [
          "Passports"
        ],
        "summary": "Render the passport's QR code.",
        "description": "Returns the QR code for a passport. Default response is a\nraw SVG image so `<img src=\".../qr\">` works directly. Use\n`?format=png` for a raster PNG, or `?format=json` for an\nenvelope containing both the SVG markup and a base64\nPNG data URI plus the encoded URL + status.\n\nCounts as one v1 passport-read against the daily budget\n(same counter as the data endpoint).\n",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "svg",
                "png",
                "json"
              ],
              "default": "svg"
            }
          },
          {
            "name": "symbology",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "qr",
                "datamatrix"
              ],
              "default": "qr"
            },
            "description": "The 2D symbology to render. EN 18220:2026 permits QR Code\n(ISO/IEC 18004) and Data Matrix (ISO/IEC 16022); both encode the\nsame URL. Data Matrix suits small parts and direct part marking.\nWith `datamatrix`, an 8-char `backgroundColor` renders opaque\nunless its alpha is `00` (fully transparent).\n"
          },
          {
            "name": "useCompanyBranding",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            },
            "description": "When set to `true`, foreground colour comes from the\ncompany's `branding.primaryColor`. Ignored if `color`\nis also set.\n"
          },
          {
            "name": "color",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[0-9A-Fa-f]{6}$"
            },
            "description": "6-char hex without `#`. Wins over useCompanyBranding."
          },
          {
            "name": "backgroundColor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[0-9A-Fa-f]{6,8}$"
            },
            "description": "6 or 8 char hex without `#` (8 = RGBA)."
          }
        ],
        "responses": {
          "200": {
            "description": "QR rendering.",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrJsonEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Invalid colour or symbology parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/by-serial/{serial}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Serial"
        },
        {
          "$ref": "#/components/parameters/GtinQuery"
        }
      ],
      "get": {
        "operationId": "getPassportBySerial",
        "tags": [
          "Passports"
        ],
        "summary": "Read one passport by serial.",
        "description": "By-serial addressing for the read endpoint — same response\nshape and same query parameters (`?lang=`, `?format=full`)\nas `GET /api/v1/passports/{id}`.\n",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "full"
              ]
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Passport.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Passport"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/AmbiguousSerial"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/by-serial/{serial}/qr": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Serial"
        },
        {
          "$ref": "#/components/parameters/GtinQuery"
        }
      ],
      "get": {
        "operationId": "getPassportQrBySerial",
        "tags": [
          "Passports"
        ],
        "summary": "Render the passport's QR code (by serial).",
        "description": "Same shape as `GET /api/v1/passports/{id}/qr`.",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "svg",
                "png",
                "json"
              ],
              "default": "svg"
            }
          },
          {
            "name": "symbology",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "qr",
                "datamatrix"
              ],
              "default": "qr"
            },
            "description": "The 2D symbology to render. EN 18220:2026 permits QR Code\n(ISO/IEC 18004) and Data Matrix (ISO/IEC 16022); both encode the\nsame URL. Data Matrix suits small parts and direct part marking.\nWith `datamatrix`, an 8-char `backgroundColor` renders opaque\nunless its alpha is `00` (fully transparent).\n"
          },
          {
            "name": "useCompanyBranding",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          },
          {
            "name": "color",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[0-9A-Fa-f]{6}$"
            }
          },
          {
            "name": "backgroundColor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[0-9A-Fa-f]{6,8}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "QR rendering.",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrJsonEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/AmbiguousSerial"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/by-serial/{serial}/fields/{key}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Serial"
        },
        {
          "$ref": "#/components/parameters/GtinQuery"
        },
        {
          "$ref": "#/components/parameters/FieldKey"
        },
        {
          "$ref": "#/components/parameters/IdempotencyKey"
        }
      ],
      "patch": {
        "operationId": "updatePassportFieldBySerial",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Update one field on a passport (by serial).",
        "description": "Same body, same response, and same semantics as PATCH\n`/api/v1/passports/{id}/fields/{key}` — including the no-op\n`unchanged: true` response and the required-field guard.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateFieldInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Field updated, or no-op when the value was already identical.\nWhen `unchanged: true` is present, nothing was written and no\nversion was bumped.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "field",
                    "version"
                  ],
                  "properties": {
                    "field": {
                      "$ref": "#/components/schemas/PassportField"
                    },
                    "version": {
                      "type": "integer"
                    },
                    "unchanged": {
                      "type": "boolean",
                      "description": "Present and `true` when the write was a no-op.\n"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/BySerialPassportConflict"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/by-serial/{serial}/suspend": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Serial"
        },
        {
          "$ref": "#/components/parameters/GtinQuery"
        },
        {
          "$ref": "#/components/parameters/IdempotencyKey"
        }
      ],
      "post": {
        "operationId": "suspendPassportBySerial",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Suspend a passport (by serial).",
        "description": "Same shape as POST `/api/v1/passports/{id}/suspend`.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Passport"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/AmbiguousSerial"
          },
          "422": {
            "description": "Wrong status to suspend from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/by-serial/{serial}/archive": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Serial"
        },
        {
          "$ref": "#/components/parameters/GtinQuery"
        },
        {
          "$ref": "#/components/parameters/IdempotencyKey"
        }
      ],
      "post": {
        "operationId": "archivePassportBySerial",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Archive a passport (by serial, irreversible).",
        "description": "Same shape as POST `/api/v1/passports/{id}/archive`, including\nthe `reason` body that battery passports require.\n**Irreversible.**\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeletionReasonInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Archived. Idempotent — an already archived passport returns 200 with the existing document.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Passport"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeletionReasonInvalid"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Two mutually exclusive causes — switch on `error`:\n`ambiguous_serial` (see the `AmbiguousSerial` response;\nadd `?gtin=<gtin>`) or `passport_already_marked_for_deletion`\n(the passport is marked for deletion and cannot be archived).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/by-serial/{serial}/mark-for-deletion": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Serial"
        },
        {
          "$ref": "#/components/parameters/GtinQuery"
        },
        {
          "$ref": "#/components/parameters/IdempotencyKey"
        }
      ],
      "post": {
        "operationId": "markPassportForDeletionBySerial",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Mark a passport for deletion (by serial).",
        "description": "Same shape as POST `/api/v1/passports/{id}/mark-for-deletion`,\nincluding the `reason` body that battery passports require.\nThe passport is then deleted with\n`DELETE /api/v1/passports/{id}`.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeletionReasonInput"
              },
              "example": {
                "reason": "recycled"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Marked for deletion. Returns the passport.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Passport"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DeletionReasonInvalid"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/DeletionNotOnPlan"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Either `ambiguous_serial` (see the `AmbiguousSerial`\nresponse; add `?gtin=<gtin>`) or one of the\nmark-for-deletion conflicts listed on\n`MarkForDeletionConflict`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/by-serial/{serial}/epcis": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Serial"
        },
        {
          "$ref": "#/components/parameters/GtinQuery"
        }
      ],
      "get": {
        "operationId": "passportEpcisExportBySerial",
        "tags": [
          "EPCIS"
        ],
        "summary": "Export a passport's events as an EPCIS 2.0 document (by serial).",
        "description": "Same EPCIS 2.0 export as\n`GET /api/v1/passports/{id}/epcis`, looked up by your own\nserial instead of our internal id and scoped to the API\nkey's company. Returns a standards-valid EPCIS 2.0 JSON-LD\n`EPCISDocument`.\n\nEPCIS export is included on Starter plans and up; a tenant\nwithout it gets `403 epcis_export_not_available`. Counts as\none passport read.\n",
        "responses": {
          "200": {
            "description": "EPCIS 2.0 document.",
            "content": {
              "application/ld+json": {
                "schema": {
                  "$ref": "#/components/schemas/EpcisDocument"
                }
              }
            }
          },
          "400": {
            "description": "Invalid serial.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "EPCIS export not available on this plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/AmbiguousSerial"
          }
        }
      }
    },
    "/api/v1/passports/by-serial/{serial}/condition-flags": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Serial"
        },
        {
          "$ref": "#/components/parameters/GtinQuery"
        }
      ],
      "get": {
        "operationId": "getConditionFlagsBySerial",
        "tags": [
          "Passports"
        ],
        "summary": "Read condition-classification flags (by serial).",
        "description": "Same contract as\n`GET /api/v1/passports/{id}/condition-flags`, addressed\nby your own serial number instead of our internal id.\nPass `?gtin=<gtin>` if a serial is not unique within\nyour account (same disambiguation rule as all other\nby-serial routes; 409 `ambiguous_serial` without it).\n",
        "responses": {
          "200": {
            "description": "Condition profile.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "passportId",
                    "conditionProfile"
                  ],
                  "properties": {
                    "passportId": {
                      "$ref": "#/components/schemas/ResourceId"
                    },
                    "conditionProfile": {
                      "$ref": "#/components/schemas/ConditionProfile"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid serial.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/AmbiguousSerial"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      },
      "patch": {
        "operationId": "setConditionFlagsBySerial",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Set or clear condition flags (by serial).",
        "description": "Same contract as\n`PATCH /api/v1/passports/{id}/condition-flags`, addressed\nby your own serial number. See the by-id operation for the\nfull warning about flag approval making fields required and\npotentially blocking publishing.\n\nSupports `Idempotency-Key`. Counts as one v1 write.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConditionFlagsPatch"
              },
              "example": {
                "hasBMS": true,
                "rechargeable": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Condition profile after the update.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "passportId",
                    "conditionProfile",
                    "version"
                  ],
                  "properties": {
                    "passportId": {
                      "$ref": "#/components/schemas/ResourceId"
                    },
                    "conditionProfile": {
                      "$ref": "#/components/schemas/ConditionProfile"
                    },
                    "version": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/BySerialPassportArchivedConflict"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/measurements": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        }
      ],
      "post": {
        "operationId": "captureMeasurements",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Push battery measurements (state of health, cycles, …).",
        "description": "Battery passports only (Art. 77 / Annex XIII 4 of Regulation (EU)\n2023/1542): push over-life measurements from your own equipment,\ne.g. a BMS reporting state of health. Up to 500 per call.\n\nEvery measurement is stored. The newest one per field (by\n`measuredAt`) also becomes the passport's current value for that\nfield, and sets `dynamicDataAsOf`. An older measurement arriving\nlate is kept in the history but does not overwrite a newer value.\nAn `externalId` makes a measurement idempotent: a repeat is counted\nin `duplicates`, not stored or metered again.\n\nAccepted `fieldKey`s are the Annex XIII point 4 use-data fields\n(state of health, fades, cycle counts, dynamic values, state of\ncharge, negative events, temperature history). `batteryStatus` is not\na measurement: lifecycle status changes go through the dashboard,\nand a second-life battery gets a successor passport (`lineage`). A\nfield the Regulation keeps off this battery category (e.g. state of\ncertified energy on an LMT battery) is refused with\n`field_not_applicable`. The passport must be published.\n\nMeasurements are metered per calendar period against the plan's\nmonthly allowance (`maxMeasurementsPerMonth`), not against the daily\nv1 write budget. Past the allowance, paid plans keep accepting and\ncounting them at no charge; the Free plan stops at its allowance.\nViewing a passport is never metered.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MeasurementsInput"
              },
              "example": {
                "measurements": [
                  {
                    "fieldKey": "stateOfHealth",
                    "value": 96.4,
                    "measuredAt": "2027-03-01T06:00:00Z",
                    "externalId": "bms-7781-2027-03-01"
                  },
                  {
                    "fieldKey": "numberOfFullEquivalentChargingCycles",
                    "value": 112,
                    "measuredAt": "2027-03-01T06:00:00Z"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Measurements accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeasurementsCaptureResult"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_field_key` (not an accepted measurement key, including\n`batteryStatus`), `invalid_value` (wrong type or out of the field's\nrange) or `measuredAt_in_future`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "`not_a_battery`, `passport_not_active` (not published),\n`field_not_applicable`, or an Idempotency-Key reused with a\ndifferent body.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      },
      "get": {
        "operationId": "listMeasurements",
        "tags": [
          "Passports"
        ],
        "summary": "List a passport's measurement history.",
        "description": "Newest first. Filter by `fieldKey` and a `from`/`to` window on\n`measuredAt`; page with `cursor` (the previous page's `nextCursor`).\n",
        "parameters": [
          {
            "name": "fieldKey",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of measurements.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "hasMore",
                    "nextCursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PassportMeasurement"
                      }
                    },
                    "hasMore": {
                      "type": "boolean"
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/measurements/latest": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        }
      ],
      "get": {
        "operationId": "getLatestMeasurements",
        "tags": [
          "Passports"
        ],
        "summary": "Latest measurement per field.",
        "description": "One entry per accepted measurement key: the newest measurement, or\n`null` when none has been received.\n",
        "responses": {
          "200": {
            "description": "Latest values.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "additionalProperties": {
                        "oneOf": [
                          {
                            "$ref": "#/components/schemas/LatestMeasurement"
                          },
                          {
                            "type": "null"
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/by-serial/{serial}/measurements": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Serial"
        },
        {
          "$ref": "#/components/parameters/GtinQuery"
        }
      ],
      "post": {
        "operationId": "captureMeasurementsBySerial",
        "tags": [
          "Passports"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Push battery measurements (by serial).",
        "description": "Battery passports only (Art. 77 / Annex XIII 4 of Regulation (EU)\n2023/1542): push over-life measurements from your own equipment,\ne.g. a BMS reporting state of health. Up to 500 per call.\n\nEvery measurement is stored. The newest one per field (by\n`measuredAt`) also becomes the passport's current value for that\nfield, and sets `dynamicDataAsOf`. An older measurement arriving\nlate is kept in the history but does not overwrite a newer value.\nAn `externalId` makes a measurement idempotent: a repeat is counted\nin `duplicates`, not stored or metered again.\n\nAccepted `fieldKey`s are the Annex XIII point 4 use-data fields\n(state of health, fades, cycle counts, dynamic values, state of\ncharge, negative events, temperature history). `batteryStatus` is not\na measurement: lifecycle status changes go through the dashboard,\nand a second-life battery gets a successor passport (`lineage`). A\nfield the Regulation keeps off this battery category (e.g. state of\ncertified energy on an LMT battery) is refused with\n`field_not_applicable`. The passport must be published.\n\nMeasurements are metered per calendar period against the plan's\nmonthly allowance (`maxMeasurementsPerMonth`), not against the daily\nv1 write budget. Past the allowance, paid plans keep accepting and\ncounting them at no charge; the Free plan stops at its allowance.\nViewing a passport is never metered.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MeasurementsInput"
              },
              "example": {
                "measurements": [
                  {
                    "fieldKey": "stateOfHealth",
                    "value": 96.4,
                    "measuredAt": "2027-03-01T06:00:00Z",
                    "externalId": "bms-7781-2027-03-01"
                  },
                  {
                    "fieldKey": "numberOfFullEquivalentChargingCycles",
                    "value": 112,
                    "measuredAt": "2027-03-01T06:00:00Z"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Measurements accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeasurementsCaptureResult"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_field_key` (not an accepted measurement key, including\n`batteryStatus`), `invalid_value` (wrong type or out of the field's\nrange) or `measuredAt_in_future`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "`not_a_battery`, `passport_not_active` (not published),\n`field_not_applicable`, or an Idempotency-Key reused with a\ndifferent body.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      },
      "get": {
        "operationId": "listMeasurementsBySerial",
        "tags": [
          "Passports"
        ],
        "summary": "List a passport's measurement history (by serial).",
        "description": "Newest first. Filter by `fieldKey` and a `from`/`to` window on\n`measuredAt`; page with `cursor` (the previous page's `nextCursor`).\n",
        "parameters": [
          {
            "name": "fieldKey",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of measurements.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "hasMore",
                    "nextCursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PassportMeasurement"
                      }
                    },
                    "hasMore": {
                      "type": "boolean"
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/by-serial/{serial}/measurements/latest": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Serial"
        },
        {
          "$ref": "#/components/parameters/GtinQuery"
        }
      ],
      "get": {
        "operationId": "getLatestMeasurementsBySerial",
        "tags": [
          "Passports"
        ],
        "summary": "Latest measurement per field (by serial).",
        "description": "One entry per accepted measurement key: the newest measurement, or\n`null` when none has been received.\n",
        "responses": {
          "200": {
            "description": "Latest values.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "additionalProperties": {
                        "oneOf": [
                          {
                            "$ref": "#/components/schemas/LatestMeasurement"
                          },
                          {
                            "type": "null"
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/products": {
      "post": {
        "operationId": "createProduct",
        "tags": [
          "Products"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Create a product.",
        "description": "Create a product. The category template is resolved\nautomatically from the `category` slug — must match a\nseeded category. `model` strings are unique within the\nworkspace.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateProductInput"
              },
              "example": {
                "name": "Li-Ion 48V Battery Pack",
                "model": "BP-48V-100",
                "category": "battery",
                "defaultFieldValues": {
                  "batteryChemistry": "NMC",
                  "nominalVoltage": 48
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Product created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      },
      "get": {
        "operationId": "listProducts",
        "tags": [
          "Products"
        ],
        "summary": "List products.",
        "description": "Paginated list of products in the workspace.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "archived"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated product list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaginatedProducts"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/products/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProductId"
        }
      ],
      "get": {
        "operationId": "getProduct",
        "tags": [
          "Products"
        ],
        "summary": "Read one product.",
        "description": "Read one product by ID. Returns the full document\nincluding default field values, image URLs, template\nreference, and the running `passportCount`.\n",
        "responses": {
          "200": {
            "description": "Product.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      },
      "patch": {
        "operationId": "updateProduct",
        "tags": [
          "Products"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Update a product.",
        "description": "Patch one or more product fields. Send only the keys you\nwant to change — omitted fields stay untouched.\n\n`imageUrls` REPLACES the existing array (deliberate — your\nCMS stays canonical). To append a single image without\nrewriting the list, use the multipart upload endpoint.\n`description: null` clears the description (distinct from\nomitting the key).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateProductInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Product updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      },
      "delete": {
        "operationId": "deleteProduct",
        "tags": [
          "Products"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Delete a product permanently.",
        "description": "**Permanent and irreversible.** Removes the product row +\nany uploaded documents whose only reference was this\nproduct (documents linked to other products/passports\nstay, just with `productId` unset), and the stored files\nthat belonged only to this product.\n\n**Only products with ZERO passports of any status are\neligible** — including archived. The rule preserves audit\nhistory: an archived passport carries the regulatory story\n(\"we published this, then withdrew it\") that would be\norphaned if the parent product disappeared.\n\n**Paid-plan feature.** Returns 403 on Free plans; use the\narchive endpoint instead.\n\nHonours `Idempotency-Key`. No webhook.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Permanently deleted; summary includes cascade counts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "productId": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "deletedCounts": {
                          "type": "object",
                          "properties": {
                            "documentRefsUnlinked": {
                              "type": "integer"
                            },
                            "documentsDeleted": {
                              "type": "integer"
                            },
                            "filesDeleted": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Plan gate — hard-delete is paid-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Product not eligible (any passports still reference it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/products/{id}/archive": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProductId"
        },
        {
          "$ref": "#/components/parameters/IdempotencyKey"
        }
      ],
      "post": {
        "operationId": "archiveProduct",
        "tags": [
          "Products"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Archive a product.",
        "description": "Soft-archive. `Product.status` flips to `archived`; the\nproduct disappears from default listings (still visible\nwith the `?showArchived=true` filter). Existing passports\nkeep resolving — archive blocks NEW passport creation\nagainst this product going forward; it doesn't break any\nQR already in customers' hands.\n\nReturns 409 when any non-archived passport still references\nthe product. Honours `Idempotency-Key`.\n",
        "responses": {
          "200": {
            "description": "Archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Product still has non-archived passports.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/products/batch": {
      "post": {
        "operationId": "batchCreateProducts",
        "tags": [
          "Products"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Batch-create products (up to 100 per call).",
        "description": "Create up to 100 products in one call. Each item carries\nthe same body shape as the single-create endpoint.\nPartial-success per item — each gets its own status in\nthe response array.\n\nThe whole batch consumes N writes upfront against the\ndaily-write budget; if that would overflow, the entire\nbatch returns 429 (no partial billing).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "products"
                ],
                "properties": {
                  "products": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "$ref": "#/components/schemas/CreateProductInput"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Partial-success batch result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProductsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/products/{id}/images": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProductId"
        }
      ],
      "post": {
        "operationId": "uploadProductImage",
        "tags": [
          "Products"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Upload a product image.",
        "description": "Upload a single image file via `multipart/form-data` (field\nname `file`) and append it to the product's `imageUrls`\narray. Returns the stored image's public URL plus the full\nupdated array.\n\nPNG / JPG / WebP only, max 5 MB per file, max 20 images\nper product. **No Idempotency-Key support** — multipart\nbodies aren't safely hashable; the client should check\nexistence and skip if retrying.\n",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Image bytes (PNG, JPG, or WebP, ≤ 5 MB)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Image uploaded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadImageResponse"
                }
              }
            }
          },
          "400": {
            "description": "No file provided.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "description": "Image larger than 5 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported MIME type, or the file's bytes don't match the declared image type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "Product already has 20 images (max), or the virus scanner found malware in the file.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          },
          "503": {
            "description": "The virus scanner is briefly unavailable; nothing was stored. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/products/{id}/images/{index}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProductId"
        },
        {
          "name": "index",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 0
          },
          "description": "Zero-based image index in the product's `imageUrls` array."
        }
      ],
      "delete": {
        "operationId": "deleteProductImage",
        "tags": [
          "Products"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Remove a product image by index.",
        "description": "Remove a single image from `imageUrls` by its zero-based\nindex. The product stops referencing that image URL.\n",
        "responses": {
          "200": {
            "description": "Image removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "removed",
                    "imageUrls"
                  ],
                  "properties": {
                    "removed": {
                      "type": "string",
                      "format": "uri"
                    },
                    "imageUrls": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "uri"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid image index.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Index out of range, or product not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/exports/tenant": {
      "get": {
        "operationId": "tenantExport",
        "tags": [
          "Exports"
        ],
        "summary": "Bulk JSON-LD tenant export.",
        "description": "Returns every product, every passport (regardless of\nstatus), and every referenced category template the\nworkspace owns, as one JSON-LD document.\n\n**Not API-key reachable.** Auth is dashboard JWT (admin\nrole) — same trust level as deleting the company. No plan\nor billing gate: every plan, Free included, whatever the\nbilling status and after cancelling (EU Data Act). The\npragmatic flow is: open Settings → Data export in the\ndashboard and click \"Download JSON-LD\"; \"Export everything\"\non the same page builds a full-account ZIP that also holds\nthe documents, supplier requests, passport versions, battery\nmeasurements and scan analytics.\nProgrammatic access requires a session cookie issued by\n`/api/auth/login`.\n\nSynchronous response with `Content-Disposition:\nattachment; filename=\"tracepass-tenant-export-<companyId>-<YYYY-MM-DD>.jsonld\"`.\nNo pagination — all-or-nothing.\n",
        "security": [
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "Accept",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "application/ld+json",
                "application/json"
              ],
              "default": "application/ld+json"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tenant export.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                },
                "description": "attachment; filename=\"tracepass-tenant-export-<companyId>-<YYYY-MM-DD>.jsonld\"\n"
              }
            },
            "content": {
              "application/ld+json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantExport"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantExport"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin role required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/exports/tenant/epcis": {
      "get": {
        "operationId": "tenantEpcisExport",
        "tags": [
          "Exports"
        ],
        "summary": "Bulk EPCIS 2.0 tenant export.",
        "description": "Returns every **published** passport the workspace owns,\nmerged into a single EPCIS 2.0 `EPCISDocument` (supply-chain,\nservice, ownership, and captured events). The EPCIS sibling of\nthe JSON-LD tenant export at `GET /api/exports/tenant`.\n\nOnly published passports are included — draft / suspended /\narchived / expired passports are not part of a traceability\nfeed. The tenant exporting their own data sees the full event\nset (authority access level).\n\n**Not API-key reachable.** Auth is dashboard JWT (admin role) —\nsame privileged-action trust level as the JSON-LD tenant export.\nNo plan or billing gate: every plan, Free included, whatever the\nbilling status and after cancelling (\"Download EPCIS\" in\nSettings → Data export). Programmatic access requires a\nsession cookie from `/api/auth/login`; the per-passport v1\nEPCIS routes cover API-key access.\n\nSynchronous response with `Content-Disposition: attachment`.\nNo pagination.\n",
        "security": [
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "Accept",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "application/ld+json",
                "application/json"
              ],
              "default": "application/ld+json"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "EPCIS 2.0 document covering every published passport.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                },
                "description": "attachment; filename=\"tracepass-tenant-epcis-<companyId>-<YYYY-MM-DD>.jsonld\"\n"
              }
            },
            "content": {
              "application/ld+json": {
                "schema": {
                  "$ref": "#/components/schemas/EpcisDocument"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EpcisDocument"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin role required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/exports/tenant/registry-batch": {
      "get": {
        "operationId": "tenantRegistryBatchExport",
        "tags": [
          "Exports"
        ],
        "summary": "Batch registration file for the EU DPP Registry.",
        "description": "Builds the batch file an economic operator uploads to the EU\nDPP Registry, as XML. Sibling of the JSON-LD and EPCIS tenant\nexports.\n\n**Publishing a passport does not register it.** The duty to\nregister sits with the economic operator placing the product on\nthe market (Reg (EU) 2023/1542), and TracePass has no registry\nclient. This endpoint hands over the payload already in the\nshape the registry accepts, so the step the operator owes is an\nupload rather than a data-assembly exercise.\n\n**Batteries only** — BATTERIES is the only product group the\nregistry offers, so passports in other categories are excluded\nrather than exported into a group the registry would reject.\nThe response reports how many were left out, so the caller is\nnot guessing.\n\n**Published passports only:** an unpublished passport's link\ndoes not resolve, and the registry probes it mechanically.\n\nAt most 100 items per file.\n\n**Not API-key reachable.** Auth is dashboard JWT with the admin\nrole — the same privileged-action gate as the other tenant\nexports. There is deliberately **no plan gate**: this is the\ncustomer's own regulatory obligation, and putting a paywall\nbetween them and a duty they cannot otherwise discharge is not\na trade worth making.\n\nSynchronous response with `Content-Disposition: attachment`.\n",
        "security": [
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "The batch registration file.",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin role required."
          },
          "404": {
            "description": "Workspace not found."
          }
        }
      }
    },
    "/api/v1/passports/{id}/epcis": {
      "get": {
        "operationId": "passportEpcisExport",
        "tags": [
          "EPCIS"
        ],
        "summary": "Export a passport's events as an EPCIS 2.0 document.",
        "description": "Returns the passport's supply-chain, service, ownership, and\ncaptured events as a standards-valid EPCIS 2.0 JSON-LD\n`EPCISDocument`.\n\nEPCIS export is included on Starter plans and up; a tenant\nwithout it gets `403 epcis_export_not_available`.\n\nA by-serial sibling exists at\n`GET /api/v1/passports/by-serial/{serial}/epcis` — identical\nresponse, looked up by your own serial. Counts as one\npassport read.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The passport's TracePass id."
          }
        ],
        "responses": {
          "200": {
            "description": "EPCIS 2.0 document.",
            "content": {
              "application/ld+json": {
                "schema": {
                  "$ref": "#/components/schemas/EpcisDocument"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "EPCIS export not available on this plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/epcis/capture": {
      "post": {
        "operationId": "epcisCapture",
        "tags": [
          "EPCIS"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "summary": "Capture EPCIS 2.0 events.",
        "description": "The EPCIS 2.0 Capture interface. Accepts an `EPCISDocument`,\nan `EPCISQueryDocument`, a bare event, or a bare array of\nevents as JSON-LD. Each event is validated, its EPCs resolved\nto passports, and stored.\n\nResponds `202 Accepted` with a `captureJobId` — poll\n`GET /api/v1/epcis/capture/{id}` for the per-event outcome.\n\nEPCIS capture is a paid add-on; a tenant without it gets\n`403 epcis_capture_not_available`. Counts as one v1 write.\nIdempotent: send an `Idempotency-Key` header, and each event's\nown EPCIS `eventID` is also deduped.\n",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Stripe-style idempotency key. A replay with the same key\nand body returns the cached 202 without re-capturing.\n"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/ld+json": {
              "schema": {
                "$ref": "#/components/schemas/EpcisDocument"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Capture job accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EpcisCaptureResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Billing not in good standing (past due, unpaid or cancelled), or the plan's monthly EPCIS event quota is reached (epcis_events_hard_block / epcis_events_overage_required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "EPCIS capture add-on not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/epcis/capture/{id}": {
      "get": {
        "operationId": "epcisCaptureJob",
        "tags": [
          "EPCIS"
        ],
        "summary": "Poll an EPCIS capture job.",
        "description": "The read side of the EPCIS 2.0 async capture model. Returns\nthe job's status, how many events were captured, and any\nper-event errors. Counts as one read.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The capture job id from the capture response."
          }
        ],
        "responses": {
          "200": {
            "description": "Capture job status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EpcisCaptureJob"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "EPCIS capture add-on not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/epcis/events": {
      "get": {
        "operationId": "epcisQuery",
        "tags": [
          "EPCIS"
        ],
        "summary": "Query the EPCIS 2.0 event store.",
        "description": "Search your captured supply-chain events using EPCIS 2.0\nquery parameters, returned as an `EPCISQueryDocument`.\n\nEight parameters are supported: `EQ_bizStep`,\n`EQ_disposition`, `EQ_eventType`, `MATCH_epc`,\n`EQ_bizLocation`, `EQ_readPoint`, `GE_eventTime` and\n`LT_eventTime`. `EQ_bizStep`, `EQ_disposition` and `MATCH_epc`\naccept `|`-separated values to match any of several; the other\nparameters take a single value. Matching is exact against the\nvalue as captured (a short-form `shipping` does not match the\nCBV URI form, and vice versa). Any other parameter returns `400\nepcis_query_invalid` rather than being silently ignored, so a\ntypo fails loudly instead of widening your result set.\n\nResults are newest-first and only ever include events your own\nworkspace captured and approved. A response caps at 1000\nevents; if there were more, `X-TracePass-Result-Truncated:\ntrue` is set — narrow the window with `GE_eventTime` /\n`LT_eventTime` to page through.\n\nA workspace where EPCIS has been switched off by support\ngets `403 epcis_query_not_available`. Where query is not\nprovisioned for the deployment the response is `503\nepcis_query_node_unavailable`. Counts as one read.\n",
        "parameters": [
          {
            "name": "EQ_bizStep",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Match events with this bizStep. `|`-separate to match any\nof several, e.g. `shipping|receiving`.\n"
          },
          {
            "name": "GE_eventTime",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Match events at or after this time."
          },
          {
            "name": "MATCH_epc",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Match events referencing this EPC."
          }
        ],
        "responses": {
          "200": {
            "description": "EPCIS query result.",
            "content": {
              "application/ld+json": {
                "schema": {
                  "$ref": "#/components/schemas/EpcisDocument"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "EPCIS query add-on not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "EPCIS query is not enabled for this deployment.\nExport and capture are unaffected.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/templates": {
      "get": {
        "operationId": "listTemplates",
        "tags": [
          "Templates"
        ],
        "summary": "List the DPP category templates (regulatory schemas).",
        "description": "Lists all DPP category templates — the regulatory field\nschemas (battery, textile, electronics, …). Reference data:\neach entry gives the field count, required-field count, and\nthe governing EU regulation, so an integrator or AI assistant\ncan discover what a compliant passport in a category needs\nbefore creating products or passports. Not company-scoped —\ntemplates are global.\n\n**No authentication required.** This is regulatory reference\ndata — the identical field definitions ship as the Apache-2.0\n`@tracepass/dpp-schemas` npm package — so it is readable\nwithout a key. Bounded by an IP rate limit (60 requests/min).\n",
        "security": [],
        "responses": {
          "200": {
            "description": "The available category templates.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "category": {
                            "type": "string",
                            "example": "battery"
                          },
                          "categoryLabel": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "string"
                            },
                            "description": "Localized category label, keyed by locale."
                          },
                          "fieldCount": {
                            "type": "integer",
                            "example": 132
                          },
                          "requiredFieldCount": {
                            "type": "integer",
                            "description": "Category-agnostic required count. Where a template carries `requiredFieldCountByCategory`, prefer that map — for battery this flat number matches none of the per-category totals.",
                            "example": 38
                          },
                          "requiredFieldCountByCategory": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "description": "Required-field totals per sub-category, present only where required-ness varies: battery (keyed by the categories that owe a passport under Art. 77(1) of Reg (EU) 2023/1542 — `EV`, `LMT`, `industrial_gt_2kwh`), detergents (`consumer`, `industrial_institutional`) and fmcg (`productSubcategory`). `portable`, `SLI` and `industrial_lte_2kwh` are deliberately ABSENT rather than zero: those batteries owe no passport at all, which is a different statement from \"zero fields required\".",
                            "additionalProperties": {
                              "type": "integer"
                            },
                            "example": {
                              "EV": 76,
                              "LMT": 79,
                              "industrial_gt_2kwh": 61
                            }
                          },
                          "version": {
                            "type": "integer",
                            "example": 1
                          },
                          "regulation": {
                            "type": "object",
                            "properties": {
                              "name": {
                                "type": "string",
                                "example": "EU Battery Regulation"
                              },
                              "number": {
                                "type": "string",
                                "example": "(EU) 2023/1542"
                              },
                              "effectiveDate": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "mandatoryDate": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "dateBasis": {
                                "type": "string",
                                "enum": [
                                  "statutory",
                                  "indicative",
                                  "none"
                                ],
                                "description": "statutory = dates fixed by an adopted act; indicative = planning estimates with no legal force; none = no passport act is adopted or scheduled, and the dates are the sentinel 9999-12-31 — never display or compare them."
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (60 requests/min per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/templates/{category}": {
      "get": {
        "operationId": "getTemplate",
        "tags": [
          "Templates"
        ],
        "summary": "Get the full regulatory schema for one DPP category.",
        "description": "Returns the full field schema for one DPP category — every\nfield with its key, label, data type, whether it is required,\naccess level (public / restricted / authority), enum options,\nvalidation bounds, and (where known) the regulation article /\nannex that mandates it. This is the \"what does a compliant\nbattery / textile / … passport actually need\" lookup that\npowers compliance-aware integrations and the MCP server's\ncompliance-copilot prompts.\n\n**No authentication required** — see the list endpoint above.\nBounded by an IP rate limit (60 requests/min).\n",
        "security": [],
        "parameters": [
          {
            "name": "category",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "battery",
                "textile",
                "electronics",
                "construction",
                "steel",
                "detergents",
                "paints-coatings",
                "packaging",
                "furniture",
                "tyres",
                "jewelry",
                "toys",
                "fmcg"
              ]
            },
            "description": "The DPP category key."
          }
        ],
        "responses": {
          "200": {
            "description": "The category's full field schema.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "category": {
                      "type": "string",
                      "example": "battery"
                    },
                    "categoryLabel": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "version": {
                      "type": "integer"
                    },
                    "regulation": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "number": {
                          "type": "string"
                        },
                        "effectiveDate": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "mandatoryDate": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "dateBasis": {
                          "type": "string",
                          "enum": [
                            "statutory",
                            "indicative",
                            "none"
                          ],
                          "description": "statutory = dates fixed by an adopted act; indicative = planning estimates with no legal force; none = no passport act is adopted or scheduled, and the dates are the sentinel 9999-12-31 — never display or compare them."
                        }
                      }
                    },
                    "fieldCount": {
                      "type": "integer"
                    },
                    "requiredFieldCount": {
                      "type": "integer",
                      "description": "Count of fields whose category-agnostic `required` is true. Where a template uses per-category applicability, see `requiredFieldCountByCategory` for the per-category totals."
                    },
                    "requiredFieldCountByCategory": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "Per-category required-field totals, present only for templates that use `requiredBy` (today: battery). Keys are the in-scope category values.",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "fields": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string",
                            "example": "batteryUniqueIdentifier"
                          },
                          "label": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "description": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "dataType": {
                            "type": "string",
                            "example": "string"
                          },
                          "unit": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "required": {
                            "type": "boolean",
                            "description": "Category-agnostic default. For templates that carry `requiredBy`, prefer that map — this boolean is the fallback used when the passport's category is unset or absent from the map. Note the battery exception below: for an out-of-scope battery category this boolean does NOT apply, because no field is mandatory at all."
                          },
                          "requiredBy": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "description": "Per-category required-ness, present only where a field's applicability varies by product category (today: the battery template, keyed by `batteryCategory`). Absent means the field is category-agnostic — read `required` instead. IMPORTANT for battery: the map is keyed ONLY by the three categories that owe a battery passport under Art. 77(1) of Reg (EU) 2023/1542 — `EV`, `LMT`, `industrial_gt_2kwh`. A `portable`, `SLI` or `industrial_lte_2kwh` battery owes no passport, so NO field is required for it. Check scope first; falling through to `required` for those three values asserts an obligation the Regulation does not impose. An in-scope category that is not a key in the map falls back to `required` (e.g. `{EV: notApplicable}` means optional for LMT and industrial).",
                            "additionalProperties": {
                              "type": "string",
                              "enum": [
                                "required",
                                "conditional",
                                "notApplicable"
                              ]
                            },
                            "example": {
                              "EV": "required",
                              "LMT": "notApplicable",
                              "industrial_gt_2kwh": "conditional"
                            }
                          },
                          "entryProperties": {
                            "type": "object",
                            "description": "For an `array` field whose entries are objects: the members of one entry. Absent on untyped lists. Battery composition, for example, is `{substanceName, casNumber, massPercent}`, with `massPercent` as a share of the battery's total mass.",
                            "additionalProperties": {
                              "type": "object",
                              "required": [
                                "type"
                              ],
                              "properties": {
                                "type": {
                                  "type": "string",
                                  "enum": [
                                    "string",
                                    "number",
                                    "boolean"
                                  ]
                                },
                                "description": {
                                  "type": "string"
                                },
                                "required": {
                                  "type": "boolean"
                                },
                                "enum": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                },
                                "format": {
                                  "type": "string",
                                  "enum": [
                                    "date",
                                    "iso3166-alpha2",
                                    "cas-number"
                                  ]
                                }
                              }
                            },
                            "example": {
                              "substanceName": {
                                "type": "string",
                                "required": true
                              },
                              "casNumber": {
                                "type": "string",
                                "format": "cas-number"
                              },
                              "massPercent": {
                                "type": "number",
                                "required": true
                              }
                            }
                          },
                          "accessLevel": {
                            "type": "string",
                            "enum": [
                              "public",
                              "restricted",
                              "authority"
                            ]
                          },
                          "category": {
                            "type": "string",
                            "description": "Localized sub-section header this field belongs to."
                          },
                          "enumOptions": {
                            "type": [
                              "array",
                              "null"
                            ],
                            "items": {
                              "type": "object",
                              "properties": {
                                "value": {
                                  "type": "string"
                                },
                                "label": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                }
                              }
                            }
                          },
                          "validation": {
                            "type": "object",
                            "properties": {
                              "minLength": {
                                "type": [
                                  "integer",
                                  "null"
                                ]
                              },
                              "maxLength": {
                                "type": [
                                  "integer",
                                  "null"
                                ]
                              },
                              "min": {
                                "type": [
                                  "number",
                                  "null"
                                ]
                              },
                              "max": {
                                "type": [
                                  "number",
                                  "null"
                                ]
                              },
                              "pattern": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              }
                            }
                          },
                          "regulationRef": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "description": "The regulation article/annex that mandates\nthis field, when known. null for platform-\nderived fields.\n",
                            "properties": {
                              "article": {
                                "type": "string",
                                "example": "Art. 77"
                              },
                              "annex": {
                                "type": "string",
                                "example": "Annex VI"
                              },
                              "instrument": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "description": "CELEX id of the instrument.",
                                "example": "32023R1542"
                              },
                              "provision": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "example": "Annex XIII 1(e)"
                              },
                              "kind": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "enum": [
                                  "legislation",
                                  "standard",
                                  "scheme",
                                  "national",
                                  "international",
                                  "specification",
                                  null
                                ],
                                "description": "What kind of claim the reference is. Only\n`legislation` can make a field required.\n"
                              },
                              "standards": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                },
                                "description": "Keys into the standards registry\n(`standards.json` in @tracepass/dpp-schemas)\nfor the technical standards this field relates\nto. Relating to a standard is not a claim of\nconformity and never makes a field required.\n",
                                "example": [
                                  "EN 10204",
                                  "EN 10025"
                                ]
                              },
                              "description": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No template for that category.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (60 requests/min per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scope/check": {
      "post": {
        "operationId": "scopeCheck",
        "tags": [
          "Scope"
        ],
        "summary": "Check product scope against EU DPP regulations.",
        "description": "Evaluates whether a product falls in scope of EU DPP obligations\ngiven its category, facts, role in the supply chain, and optional\nlocale. Returns a deterministic scope answer with the relevant\nregulation article, confidence level, and a human-readable\nrationale.\n\nAuth: Bearer API key OR OAuth with `regulatory:read` OR\n`passports:read`. Does not count against the daily passport-read cap.\n\nRate limit: Free and Basic plans are capped at a daily scope-check\nquota. A 429 with `error: scope_answer_cap` is returned when the\ncap is reached; `Retry-After` gives the seconds until midnight UTC\nreset.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "regulatory:read"
            ]
          },
          {
            "OAuth2": [
              "passports:read"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "category": {
                    "type": "string",
                    "enum": [
                      "battery",
                      "construction",
                      "detergents",
                      "electronics",
                      "fmcg",
                      "furniture",
                      "jewelry",
                      "packaging",
                      "paints-coatings",
                      "steel",
                      "textile",
                      "toys",
                      "tyres",
                      "vehicles",
                      "other"
                    ],
                    "description": "DPP product category key. Use `other` for categories\nnot yet covered by a specific regulation. `vehicles`\nis in scope (End-of-Life Vehicles Regulation) but\nhas no passport template.\n",
                    "example": "battery"
                  },
                  "facts": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Act-namespaced product facts as key-value pairs\n(e.g. `{ \"battery.use\": \"stationary_energy_storage\",\n\"battery.capacityKWh\": 10 }`). Unknown keys or\nout-of-range values return 400 `invalid_input`.\nCommon facts are not namespaced: `ownBrand: true`\nstates that you place the product on the market\nunder your own name or trademark.\n"
                  },
                  "role": {
                    "type": "string",
                    "maxLength": 120,
                    "example": "importer",
                    "description": "Your role in the act's own terms: `manufacturer`,\n`importer`, `distributor`, `authorised_representative`\nor `fulfilment_service_provider` (an act may define\nmore). Selling under your own name is `manufacturer`\nwith `facts.ownBrand: true`. Determines which duties\nare listed as yours. Defaults from the company\nprofile when omitted; with no role at all, the\nanswer lists the duties of every role.\n"
                  },
                  "placedOnMarket": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "description": "When the product was (or will be) placed on the\nmarket. A placing date is a day, a month or a year\n(`2027-03-01`, `2027-03`, `2027`); precision, when\ngiven, must fit the date string.\n",
                    "properties": {
                      "date": {
                        "type": "string",
                        "pattern": "^\\d{4}(-\\d{2}(-\\d{2})?)?$",
                        "description": "Date as YYYY-MM-DD, YYYY-MM or YYYY.",
                        "example": "2027-03"
                      },
                      "precision": {
                        "type": "string",
                        "enum": [
                          "day",
                          "month",
                          "year"
                        ],
                        "description": "Precision of the date string. Optional; inferred from the string length when omitted."
                      }
                    }
                  },
                  "lifecycleEvent": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "prepared_for_reuse",
                      "prepared_for_repurposing",
                      "repurposed",
                      "remanufactured",
                      "repaired",
                      "became_waste",
                      "recycled"
                    ],
                    "description": "A post-first-sale lifecycle event that may affect scope\nobligations (e.g. a repaired battery may still need a\npassport).\n"
                  },
                  "contains": {
                    "type": "array",
                    "maxItems": 5,
                    "description": "Component products (e.g. cells in a battery pack).\nEach entry may carry its own `label` and `facts` to\nresolve component-level scope rules. At most 5 items.\n",
                    "items": {
                      "type": "object",
                      "properties": {
                        "label": {
                          "type": "string",
                          "maxLength": 60,
                          "description": "Human-readable label for the component."
                        },
                        "facts": {
                          "type": "object",
                          "additionalProperties": true,
                          "description": "Act-namespaced facts for this component."
                        }
                      }
                    }
                  },
                  "locale": {
                    "type": "string",
                    "example": "en",
                    "description": "Response language — `en`, `bg`, `de`, or `it`.\nDefaults to the `Accept-Language` header, then `en`.\n"
                  },
                  "productId": {
                    "type": "string",
                    "description": "Pre-fill facts from this product's saved scope answers.\nMust belong to your company; returns 404 otherwise.\n"
                  },
                  "passportId": {
                    "type": "string",
                    "description": "Pre-fill facts from this passport's field values and\nthe product's saved answers. Must belong to your\ncompany; returns 404 otherwise.\n"
                  }
                }
              },
              "example": {
                "category": "battery",
                "facts": {
                  "battery.use": "stationary_energy_storage",
                  "battery.capacityKWh": 10
                },
                "role": "importer",
                "locale": "en"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scope answer envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "answer",
                    "locale",
                    "data",
                    "ask",
                    "profileUsed",
                    "citations",
                    "ruleIds",
                    "asOf",
                    "ruleStatus",
                    "machineTranslated",
                    "disclaimer"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "answered",
                        "needs_input",
                        "not_covered"
                      ],
                      "description": "`answered` — a deterministic answer is available.\n`needs_input` — blocking facts are missing (see `ask`).\n`not_covered` — no rule covers this product/category.\n"
                    },
                    "answer": {
                      "type": "string",
                      "description": "Human-readable scope answer in the requested locale."
                    },
                    "locale": {
                      "type": "string",
                      "description": "Locale the answer was rendered in."
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "inScope": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "enum": [
                            "yes",
                            "no",
                            "depends",
                            "pending",
                            null
                          ],
                          "description": "`yes` — in scope now. `no` — out of scope.\n`pending` — in scope from a future date.\n`depends` — depends on unanswered facts.\n"
                        },
                        "verdict": {
                          "type": "string",
                          "description": "Raw verdict from the rule engine."
                        },
                        "governing": {
                          "type": "object",
                          "description": "Primary governing act result when one applies. Fields\ninclude `celex`, `name`, `verdict`, `citation`, `level`,\n`role`, `duties`, `appliesFrom`, and other act-specific\ndata. The exact shape depends on the category and rule set.\n",
                          "additionalProperties": true
                        },
                        "alsoApplies": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "additionalProperties": true
                          },
                          "description": "Other acts that also apply alongside the governing act."
                        },
                        "suggestions": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        }
                      }
                    },
                    "ask": {
                      "type": "array",
                      "description": "Facts the engine needs to refine or complete the answer.\n",
                      "items": {
                        "type": "object",
                        "required": [
                          "fact",
                          "question",
                          "blocking",
                          "act",
                          "type"
                        ],
                        "properties": {
                          "fact": {
                            "type": "string",
                            "description": "Fact key (e.g. `battery.use`)."
                          },
                          "question": {
                            "type": "string",
                            "description": "Human-readable question for the fact."
                          },
                          "blocking": {
                            "type": "boolean",
                            "description": "Required to give any verdict when `true`."
                          },
                          "act": {
                            "type": "string",
                            "description": "CELEX of the act that needs this fact."
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "enum",
                              "boolean",
                              "string",
                              "number"
                            ]
                          },
                          "values": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "Allowed values for `enum`-type facts."
                          },
                          "valueLabels": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "string"
                            },
                            "description": "Human-readable labels for each allowed value."
                          }
                        }
                      }
                    },
                    "profileUsed": {
                      "type": "array",
                      "description": "Profile rules applied (currently always empty).",
                      "items": {
                        "type": "object"
                      }
                    },
                    "citations": {
                      "type": "array",
                      "description": "EU law citations backing the answer.",
                      "items": {
                        "type": "object",
                        "required": [
                          "celex",
                          "ref"
                        ],
                        "properties": {
                          "celex": {
                            "type": "string"
                          },
                          "ref": {
                            "type": "string"
                          },
                          "quote": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri"
                          }
                        }
                      }
                    },
                    "ruleIds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Versioned rule IDs in the format\n`<celex>#<rule-name>@<version>` (e.g.\n`32023R1542#duty.storage@1`).\n"
                    },
                    "asOf": {
                      "type": "string",
                      "format": "date",
                      "description": "Date the rules were current on (YYYY-MM-DD)."
                    },
                    "ruleStatus": {
                      "type": "string",
                      "enum": [
                        "verified",
                        "needs_recheck",
                        "draft"
                      ],
                      "description": "Confidence in the rules behind this answer."
                    },
                    "recheckSince": {
                      "type": "string",
                      "format": "date",
                      "description": "Present when `ruleStatus` is `needs_recheck`; the date\nthe governing act was updated.\n"
                    },
                    "machineTranslated": {
                      "type": "boolean",
                      "description": "Whether the answer text was machine-translated."
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "status": "answered",
                  "answer": "Industrial batteries matching the facts you gave need a battery passport from 18 February 2027 (Battery Regulation (EU) 2023/1542, Art. 77(1)). …",
                  "locale": "en",
                  "data": {
                    "inScope": "yes",
                    "verdict": "in_scope",
                    "governing": {
                      "celex": "32023R1542",
                      "name": "Battery Regulation (EU) 2023/1542",
                      "verdict": "in_scope",
                      "citation": {
                        "ref": "Art. 77(1)",
                        "quote": "From 18 February 2027 each LMT battery, each industrial battery with a capacity greater than 2 kWh … shall have an electronic record ('battery passport')."
                      },
                      "appliesFrom": {
                        "date": "2027-02-18",
                        "precision": "day"
                      },
                      "level": {
                        "value": "item",
                        "ref": "Art. 77(2)"
                      },
                      "role": {
                        "asked": "importer",
                        "actTerm": "importer",
                        "definedIn": "Art. 3(1)(64)",
                        "placingOperator": true
                      },
                      "duties": [
                        {
                          "id": "32023R1542#duty.unique_identifier@1",
                          "holder": "placing_operator",
                          "ref": "Art. 77(3)",
                          "status": "active"
                        }
                      ]
                    },
                    "alsoApplies": []
                  },
                  "ask": [
                    {
                      "fact": "battery.incorporatedIn",
                      "question": "Is it built into military equipment, equipment sent into space, or equipment specifically designed for nuclear installations?",
                      "blocking": false,
                      "act": "32023R1542",
                      "type": "enum",
                      "values": [
                        "none",
                        "military",
                        "space",
                        "nuclear_safety",
                        "other"
                      ]
                    }
                  ],
                  "profileUsed": [],
                  "citations": [
                    {
                      "celex": "32023R1542",
                      "ref": "Art. 77(1)",
                      "quote": "From 18 February 2027 each LMT battery…",
                      "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32023R1542"
                    }
                  ],
                  "ruleIds": [
                    "32023R1542#duty.unique_identifier@1",
                    "32023R1542#industrial@3"
                  ],
                  "asOf": "2026-10-09",
                  "ruleStatus": "verified",
                  "machineTranslated": false,
                  "disclaimer": "Regulatory information as of 9 Oct 2026, not legal advice. Check the cited articles before relying on it. — TracePass"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input. The `field` property names the offending field;\n`allowed` lists the valid values when the field is an enum.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "invalid_input",
                  "field": "category",
                  "message": "Unknown category 'sprockets'. Allowed values: battery, textile, electronics, …",
                  "allowed": [
                    "battery",
                    "textile",
                    "electronics"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Product or passport not found, or belongs to another company.\nReturned when `productId` or `passportId` is provided but\ndoes not resolve to a resource owned by the authenticated\ncompany.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "not_found"
                }
              }
            }
          },
          "429": {
            "description": "Daily scope-check cap reached. `Retry-After` header gives\nthe seconds until midnight UTC reset.\n",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the daily cap resets (midnight UTC)."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "scope_answer_cap",
                  "limit": 50,
                  "resetAt": "2026-10-12T00:00:00.000Z"
                }
              }
            }
          },
          "503": {
            "description": "Scope rules temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "scope_rules_unavailable"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scope/catalogue": {
      "get": {
        "operationId": "scopeCatalogue",
        "tags": [
          "Scope"
        ],
        "summary": "Get the catalogue scope report.",
        "description": "Returns the most recent catalogue scope run results: for each\npassport/product pair evaluated, whether it is in scope, the\napplicable regulation article, and whether the scope status\nchanged since the last run.\n\nPaginated via cursor. Each page counts as one v1 passport read\nagainst the daily cap.\n\nAuth: Bearer API key OR OAuth with `passports:read`. Requires the\nGrowth plan or higher (`scopeCatalogueEnabled`); returns 403 with\n`error: plan_upgrade_required` when the plan gate is not met.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <api-key>` or `Bearer <access-token>`."
          },
          {
            "name": "classification",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "missing",
                "draft_only",
                "published_incomplete",
                "published",
                "not_required",
                "open_questions",
                "not_covered"
              ]
            },
            "description": "Filter results by passport classification."
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter results by product category key."
          },
          {
            "name": "changed",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "When `true`, return only passports whose scope status changed."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from the previous page's `nextCursor`."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            },
            "description": "Number of items per page (1–100, default 20)."
          }
        ],
        "responses": {
          "200": {
            "description": "Catalogue scope report page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "run",
                    "running",
                    "totals",
                    "items",
                    "nextCursor",
                    "disclaimer"
                  ],
                  "properties": {
                    "run": {
                      "type": "object",
                      "description": "Metadata of the most recent completed run.",
                      "required": [
                        "id",
                        "finishedAt",
                        "rulesFingerprint"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "finishedAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "rulesFingerprint": {
                          "type": "string",
                          "description": "Hash of the rule set used for this run."
                        }
                      }
                    },
                    "running": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "The currently active run when one is queued or running;\n`null` when no run is in progress.\n",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "queued",
                            "running"
                          ]
                        },
                        "trigger": {
                          "type": "string",
                          "enum": [
                            "manual",
                            "nightly",
                            "rules_change"
                          ]
                        },
                        "createdAt": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "totals": {
                      "type": "object",
                      "required": [
                        "rows",
                        "byClassification",
                        "tiles"
                      ],
                      "properties": {
                        "rows": {
                          "type": "integer",
                          "description": "Total number of product rows evaluated."
                        },
                        "byClassification": {
                          "type": "object",
                          "description": "Product count per passport classification.",
                          "properties": {
                            "missing": {
                              "type": "integer"
                            },
                            "draft_only": {
                              "type": "integer"
                            },
                            "published_incomplete": {
                              "type": "integer"
                            },
                            "published": {
                              "type": "integer"
                            },
                            "not_required": {
                              "type": "integer"
                            },
                            "open_questions": {
                              "type": "integer"
                            },
                            "not_covered": {
                              "type": "integer"
                            }
                          }
                        },
                        "tiles": {
                          "type": "object",
                          "description": "Product count per dashboard tile (includes `changed`).",
                          "properties": {
                            "now": {
                              "type": "integer"
                            },
                            "later": {
                              "type": "integer"
                            },
                            "waiting": {
                              "type": "integer"
                            },
                            "not_required": {
                              "type": "integer"
                            },
                            "depends": {
                              "type": "integer"
                            },
                            "not_covered": {
                              "type": "integer"
                            },
                            "changed": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "productId",
                          "productName",
                          "category",
                          "gtin",
                          "passportIds",
                          "passportCount",
                          "publishedCount",
                          "summary",
                          "classification",
                          "changed",
                          "changedAt"
                        ],
                        "properties": {
                          "productId": {
                            "type": "string"
                          },
                          "productName": {
                            "type": "string"
                          },
                          "category": {
                            "type": "string"
                          },
                          "gtin": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "passportIds": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "passportCount": {
                            "type": "integer"
                          },
                          "publishedCount": {
                            "type": "integer"
                          },
                          "summary": {
                            "type": "object",
                            "description": "Condensed scope result for this product.",
                            "properties": {
                              "status": {
                                "type": "string",
                                "enum": [
                                  "answered",
                                  "needs_input",
                                  "not_covered"
                                ]
                              },
                              "inScope": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "enum": [
                                  "yes",
                                  "no",
                                  "depends",
                                  "pending",
                                  null
                                ]
                              },
                              "governing": {
                                "type": "object",
                                "description": "Primary governing act (at minimum `celex`; may carry `appliesFrom`).",
                                "additionalProperties": true
                              },
                              "alsoApplies": {
                                "type": "array",
                                "items": {
                                  "type": "object"
                                }
                              },
                              "openQuestions": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                },
                                "description": "Fact keys the answer still needs."
                              },
                              "ruleStatus": {
                                "type": "string",
                                "enum": [
                                  "verified",
                                  "needs_recheck",
                                  "draft"
                                ]
                              }
                            }
                          },
                          "classification": {
                            "type": "string",
                            "enum": [
                              "missing",
                              "draft_only",
                              "published_incomplete",
                              "published",
                              "not_required",
                              "open_questions",
                              "not_covered"
                            ]
                          },
                          "changed": {
                            "type": "boolean",
                            "description": "Whether this product's classification changed in the last run."
                          },
                          "changedAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "Timestamp of the last classification change."
                          }
                        }
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Opaque pagination cursor for the next page; `null` on the last page."
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "run": {
                    "id": "6acbb533c5553f06d833479e",
                    "finishedAt": "2026-10-11T16:11:31.619Z",
                    "rulesFingerprint": "86a23cce6480bffb91d360b7a199b473"
                  },
                  "running": null,
                  "totals": {
                    "rows": 1,
                    "byClassification": {
                      "missing": 0,
                      "draft_only": 0,
                      "published_incomplete": 0,
                      "published": 0,
                      "not_required": 0,
                      "open_questions": 1,
                      "not_covered": 0
                    },
                    "tiles": {
                      "now": 0,
                      "later": 0,
                      "waiting": 0,
                      "not_required": 0,
                      "depends": 1,
                      "not_covered": 0,
                      "changed": 0
                    }
                  },
                  "items": [
                    {
                      "productId": "6acbb529ef6a5037c63fff80",
                      "productName": "e2e-fixture product",
                      "category": "battery",
                      "gtin": "09506000E2EFIX",
                      "passportIds": [
                        "6acbb529ef6a5037c63fff81"
                      ],
                      "passportCount": 1,
                      "publishedCount": 1,
                      "summary": {
                        "status": "needs_input",
                        "inScope": "depends",
                        "governing": {
                          "celex": "32023R1542"
                        },
                        "alsoApplies": [],
                        "openQuestions": [
                          "battery.sealed",
                          "battery.weightKg",
                          "battery.capacityKWh",
                          "battery.use",
                          "battery.vehicleCategory"
                        ],
                        "ruleStatus": "verified"
                      },
                      "classification": "open_questions",
                      "changed": false,
                      "changedAt": null
                    }
                  ],
                  "nextCursor": null,
                  "disclaimer": "Regulatory information as of 9 Oct 2026, not legal advice. Check the cited articles before relying on it. — TracePass"
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameter. The `field` property names the\noffending parameter; `allowed` lists valid values for enums.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "invalid_input",
                  "field": "classification",
                  "message": "Unknown classification value."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Plan upgrade required. The catalogue scope check is available\nfrom the Growth plan.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "plan_upgrade_required",
                  "feature": "scopeCatalogueEnabled",
                  "message": "The catalogue scope check is available from the Growth plan."
                }
              }
            }
          },
          "429": {
            "description": "Daily passport-read cap reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scope/catalogue/runs": {
      "post": {
        "operationId": "startCatalogueRun",
        "tags": [
          "Scope"
        ],
        "summary": "Trigger a new catalogue scope run.",
        "description": "Enqueues a full re-evaluation of every passport in the company's\ncatalogue against the current scope rules. Returns 202 immediately\nwith a `runId`; poll `GET /api/v1/scope/catalogue/runs/{id}` for\nstatus. Once `status` is `done`, the updated results are available\nvia `GET /api/v1/scope/catalogue`.\n\nIdempotent within a short window: supply the same\n`Idempotency-Key` header to avoid double-triggering a run. A\n409 is returned if a run is already in progress for this\ncompany. A 429 with `error: run_cooldown` is returned when a\nrun completed too recently.\n\nAuth: Bearer API key OR OAuth with `passports:read`. Requires\nthe Growth plan or higher.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <api-key>` or `Bearer <access-token>`."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Client-generated key (UUID recommended). The server returns the\nsame 202 response for duplicate requests with the same key\nwithin the idempotency window.\n"
          }
        ],
        "responses": {
          "202": {
            "description": "Run accepted and queued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued"
                      ]
                    }
                  }
                },
                "example": {
                  "runId": "6728abc123def456789abcde",
                  "status": "queued"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Plan upgrade required. The catalogue scope check is available\nfrom the Growth plan.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "plan_upgrade_required",
                  "feature": "scopeCatalogueEnabled",
                  "message": "The catalogue scope check is available from the Growth plan."
                }
              }
            }
          },
          "409": {
            "description": "A catalogue run is already queued or running for this company.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "run_in_progress"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "runId": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "ID of the active run, when known."
                    },
                    "run": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "Serialised active run (same shape as GET /runs/{id} 200), when available.",
                      "additionalProperties": true
                    }
                  }
                },
                "example": {
                  "error": "run_in_progress",
                  "message": "A catalogue run is already queued or running.",
                  "runId": "6acbb533c5553f06d833479e",
                  "run": null
                }
              }
            }
          },
          "429": {
            "description": "Run cooldown: a run completed too recently. `Retry-After` header\ngives the seconds until the cooldown expires.\n",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the run cooldown expires."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message",
                    "retryAfter",
                    "run"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "run_cooldown"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "retryAfter": {
                      "type": "integer",
                      "description": "Seconds until the cooldown expires (mirrors the `Retry-After` header)."
                    },
                    "run": {
                      "type": "object",
                      "description": "The completed run that triggered the cooldown (same shape as GET /runs/{id} 200).",
                      "additionalProperties": true
                    }
                  }
                },
                "example": {
                  "error": "run_cooldown",
                  "message": "A catalogue run finished a few minutes ago. Try again shortly.",
                  "retryAfter": 600,
                  "run": {
                    "id": "6acbb533c5553f06d833479e",
                    "trigger": "manual",
                    "status": "done",
                    "rulesFingerprint": "86a23cce6480bffb91d360b7a199b473",
                    "counts": {
                      "evaluated": 1,
                      "distinctFactSets": 1,
                      "changed": 1,
                      "removed": 0,
                      "answersChanged": 0
                    },
                    "createdAt": "2026-10-11T16:11:31.583Z",
                    "startedAt": "2026-10-11T16:11:31.588Z",
                    "finishedAt": "2026-10-11T16:11:31.619Z",
                    "error": null
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scope/catalogue/runs/{id}": {
      "get": {
        "operationId": "getCatalogueRun",
        "tags": [
          "Scope"
        ],
        "summary": "Get the status and counts of a catalogue scope run.",
        "description": "Poll this endpoint after `POST /api/v1/scope/catalogue/runs` until\n`status` is `done` or `failed`. Once done, the fresh results are\navailable via `GET /api/v1/scope/catalogue`. A run belonging to\nanother company returns 404. Does not count against daily caps.\n\nAuth: Bearer API key OR OAuth with `passports:read`. Requires\nthe Growth plan or higher.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <api-key>` or `Bearer <access-token>`."
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The run ID from `POST /api/v1/scope/catalogue/runs`.",
            "example": "6728abc123def456789abcde"
          }
        ],
        "responses": {
          "200": {
            "description": "Run status and counts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "trigger",
                    "status",
                    "rulesFingerprint",
                    "counts",
                    "createdAt",
                    "startedAt",
                    "finishedAt",
                    "error"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Run ID."
                    },
                    "trigger": {
                      "type": "string",
                      "enum": [
                        "manual",
                        "nightly",
                        "rules_change"
                      ],
                      "description": "What triggered the run."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "running",
                        "done",
                        "failed"
                      ]
                    },
                    "rulesFingerprint": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Hash of the rule set used. `null` when the run has not started."
                    },
                    "counts": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "Per-run evaluation counts; `null` until the run finishes.",
                      "properties": {
                        "evaluated": {
                          "type": "integer",
                          "description": "Total fact-sets evaluated."
                        },
                        "distinctFactSets": {
                          "type": "integer",
                          "description": "Distinct fact-set fingerprints seen."
                        },
                        "changed": {
                          "type": "integer",
                          "description": "Products whose classification changed."
                        },
                        "removed": {
                          "type": "integer",
                          "description": "Products removed from the catalogue since the prior run."
                        },
                        "answersChanged": {
                          "type": "integer",
                          "description": "Products whose scope answer changed."
                        }
                      }
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the run was enqueued."
                    },
                    "startedAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "When the run started processing; `null` when still queued."
                    },
                    "finishedAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "When the run completed; `null` when still running."
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Error message when `status` is `failed`; otherwise `null`."
                    }
                  }
                },
                "example": {
                  "id": "6acbb533c5553f06d833479e",
                  "trigger": "manual",
                  "status": "done",
                  "rulesFingerprint": "86a23cce6480bffb91d360b7a199b473",
                  "counts": {
                    "evaluated": 1,
                    "distinctFactSets": 1,
                    "changed": 1,
                    "removed": 0,
                    "answersChanged": 0
                  },
                  "createdAt": "2026-10-11T16:11:31.583Z",
                  "startedAt": "2026-10-11T16:11:31.588Z",
                  "finishedAt": "2026-10-11T16:11:31.619Z",
                  "error": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Plan upgrade required. The catalogue scope check is available\nfrom the Growth plan.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "plan_upgrade_required",
                  "feature": "scopeCatalogueEnabled",
                  "message": "The catalogue scope check is available from the Growth plan."
                }
              }
            }
          },
          "404": {
            "description": "Run not found (or belongs to another company).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "not_found"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/regulatory/required-fields": {
      "get": {
        "operationId": "getRegulatoryRequiredFields",
        "tags": [
          "Regulatory"
        ],
        "summary": "Required DPP fields for a category with EU article citations.",
        "description": "Returns the mandatory fields for a DPP product category, each\nannotated with the EU regulation article that requires it and —\nfor battery passports — the role that holds the field\n(manufacturer / importer / distributor).\n\nAuth: Bearer API key OR OAuth with `regulatory:read` OR\n`passports:read`. Counts as one scope answer against the daily cap\nwhen called via the assistant channel (`X-Source: assistant`).\n\n**Account gate (OAuth + `X-Source: assistant` only):** the\nrequesting user's email must be verified and not from a disposable\ndomain. API-key callers bypass this gate.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "regulatory:read"
            ]
          },
          {
            "OAuth2": [
              "passports:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <api-key>` or `Bearer <access-token>`."
          },
          {
            "name": "category",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Template category key (e.g. `battery`, `textile`,\n`electronics`). Must be a key returned by\n`GET /api/v1/templates`.\n",
            "example": "battery"
          },
          {
            "name": "group",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Optional sub-group filter (e.g. `PortableIndustrialBattery`).\nWhen omitted, all groups for the category are returned.\n"
          },
          {
            "name": "subType",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Optional sub-type filter within a group (e.g. `LFP`). When\nomitted, all sub-types are returned.\n"
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "en",
              "enum": [
                "en",
                "bg",
                "de",
                "it"
              ]
            },
            "description": "Language for field labels and article descriptions."
          }
        ],
        "responses": {
          "200": {
            "description": "Required fields with EU citations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "category",
                    "fieldCount",
                    "requiredFields",
                    "ruleIds"
                  ],
                  "properties": {
                    "category": {
                      "type": "string",
                      "description": "The requested category key."
                    },
                    "fieldCount": {
                      "type": "integer",
                      "description": "Total number of required fields returned."
                    },
                    "requiredFields": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "key",
                          "label",
                          "regulation",
                          "article"
                        ],
                        "properties": {
                          "key": {
                            "type": "string",
                            "description": "Field key (matches the template schema)."
                          },
                          "label": {
                            "type": "string",
                            "description": "Human-readable field name in the requested locale."
                          },
                          "regulation": {
                            "type": "string",
                            "description": "Primary regulation name (e.g. `EU 2023/1542 (Battery\nRegulation)`).\n"
                          },
                          "article": {
                            "type": "string",
                            "description": "The specific article reference (e.g. `Art. 10(1)(a)`).\n"
                          },
                          "holder": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Supply-chain role that holds this field; `null` when\nnot role-specific.\n",
                            "enum": [
                              "manufacturer",
                              "importer",
                              "distributor",
                              null
                            ]
                          }
                        }
                      }
                    },
                    "ruleIds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Rule identifiers backing this answer — pass back to\n`POST /api/v1/scope/reports` to route a correction.\n"
                    }
                  }
                },
                "example": {
                  "category": "battery",
                  "fieldCount": 2,
                  "requiredFields": [
                    {
                      "key": "carbonFootprint",
                      "label": "Carbon footprint",
                      "regulation": "EU 2023/1542 (Battery Regulation)",
                      "article": "Art. 10(1)(a)",
                      "holder": "manufacturer"
                    },
                    {
                      "key": "energyDensity",
                      "label": "Energy density",
                      "regulation": "EU 2023/1542 (Battery Regulation)",
                      "article": "Art. 10(1)(c)",
                      "holder": "manufacturer"
                    }
                  ],
                  "ruleIds": [
                    "32023R1542#field.carbonFootprint@1",
                    "32023R1542#field.energyDensity@1"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing `category` (or invalid `group`/`subType`).\n`allowed` lists the valid values.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "field",
                    "message",
                    "allowed"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "invalid_input"
                      ]
                    },
                    "field": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "allowed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "error": "invalid_input",
                  "field": "category",
                  "message": "Invalid option: expected one of \"battery\"|\"textile\"|...",
                  "allowed": [
                    "battery",
                    "textile",
                    "electronics",
                    "furniture"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Account not eligible (OAuth + `X-Source: assistant` only).\n`reason` is `email_not_verified` or `disposable_email`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "reason"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "account_not_eligible"
                      ]
                    },
                    "reason": {
                      "type": "string",
                      "enum": [
                        "email_not_verified",
                        "disposable_email"
                      ]
                    }
                  }
                },
                "example": {
                  "error": "account_not_eligible",
                  "reason": "email_not_verified"
                }
              }
            }
          },
          "429": {
            "description": "Daily scope-answer cap reached for this company. The counter\nresets at 00:00 UTC.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "resetsAt"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "scope_answer_cap"
                      ]
                    },
                    "resetsAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "error": "scope_answer_cap",
                  "resetsAt": "2026-10-12T00:00:00.000Z"
                }
              }
            }
          },
          "503": {
            "description": "Regulatory data temporarily unavailable. Retry after the\n`Retry-After` header value (seconds).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "regulatory_data_unavailable"
                      ]
                    }
                  }
                },
                "example": {
                  "error": "regulatory_data_unavailable"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/regulatory/changes": {
      "get": {
        "operationId": "getRegulatoryChanges",
        "tags": [
          "Regulatory"
        ],
        "summary": "Recent regulatory changes for one or more product categories.",
        "description": "Returns a summary of what changed in law and standards for the\nspecified product categories since a given date. Useful for keeping\na compliance profile up-to-date and for answering \"what changed?\"\nquestions.\n\nAuth: Bearer API key OR OAuth with `regulatory:read` OR\n`passports:read`. Counts as one scope answer.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "regulatory:read"
            ]
          },
          {
            "OAuth2": [
              "passports:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <api-key>` or `Bearer <access-token>`."
          },
          {
            "name": "categories",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "style": "form",
            "explode": false,
            "description": "Comma-separated list of category keys. When omitted, returns\nchanges across all categories in the regulatory data feed.\n",
            "example": "battery,textile"
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "ISO-8601 date. Only changes published on or after this date\nare returned. Defaults to 90 days ago.\n",
            "example": "2026-01-01"
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "en",
              "enum": [
                "en",
                "bg",
                "de",
                "it"
              ]
            },
            "description": "Language for labels and summaries."
          }
        ],
        "responses": {
          "200": {
            "description": "Recent regulatory changes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "since",
                    "changes",
                    "ruleIds"
                  ],
                  "properties": {
                    "since": {
                      "type": "string",
                      "format": "date",
                      "description": "The effective `since` filter applied."
                    },
                    "changes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "category",
                          "date",
                          "summary",
                          "instrument"
                        ],
                        "properties": {
                          "category": {
                            "type": "string",
                            "description": "Affected product category key."
                          },
                          "date": {
                            "type": "string",
                            "format": "date",
                            "description": "Date the change was published or entered into force."
                          },
                          "summary": {
                            "type": "string",
                            "description": "Short plain-language summary of what changed, in the\nrequested locale.\n"
                          },
                          "instrument": {
                            "type": "string",
                            "description": "Regulation or standard that changed (e.g. `EU 2023/1542`).\n"
                          },
                          "article": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Article reference when the change is article-specific."
                          }
                        }
                      }
                    },
                    "ruleIds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "since": "2026-01-01",
                  "changes": [
                    {
                      "category": "battery",
                      "date": "2026-02-18",
                      "summary": "Carbon footprint thresholds under Art. 10(3) were clarified in Commission Delegated Regulation (EU) 2026/287.",
                      "instrument": "EU 2026/287",
                      "article": "Art. 10(3)"
                    }
                  ],
                  "ruleIds": [
                    "32026R0287#change.art10_3@1"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Account not eligible (OAuth + `X-Source: assistant` only).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "reason"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "account_not_eligible"
                      ]
                    },
                    "reason": {
                      "type": "string",
                      "enum": [
                        "email_not_verified",
                        "disposable_email"
                      ]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Daily scope-answer cap reached.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "resetsAt"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "scope_answer_cap"
                      ]
                    },
                    "resetsAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Regulatory data temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "regulatory_data_unavailable"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/regulatory/registry-guidance": {
      "get": {
        "operationId": "getRegulatoryRegistryGuidance",
        "tags": [
          "Regulatory"
        ],
        "summary": "How to register and which identifiers/data carriers to use.",
        "description": "Returns guidance on DPP registry registration, identifier schemes\n(GTIN, serial, GS1 Digital Link), data carrier requirements (QR\ncode, NFC, RFID), and where a passport is actually created for a\ngiven topic.\n\nAuth: Bearer API key OR OAuth with `regulatory:read` OR\n`passports:read`. Counts as one scope answer.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "regulatory:read"
            ]
          },
          {
            "OAuth2": [
              "passports:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <api-key>` or `Bearer <access-token>`."
          },
          {
            "name": "topic",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "registration",
                "identifiers",
                "data_carriers",
                "qr_code",
                "nfc_rfid",
                "gs1_digital_link",
                "access_rights"
              ]
            },
            "description": "The guidance topic to retrieve. One of: `registration`,\n`identifiers`, `data_carriers`, `qr_code`, `nfc_rfid`,\n`gs1_digital_link`, `access_rights`.\n",
            "example": "registration"
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Optional product category to tailor the guidance (e.g.\n`battery` returns battery-specific registry requirements\nalongside the general guidance).\n",
            "example": "battery"
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "en",
              "enum": [
                "en",
                "bg",
                "de",
                "it"
              ]
            },
            "description": "Language for the guidance text."
          }
        ],
        "responses": {
          "200": {
            "description": "Registry guidance for the requested topic.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "topic",
                    "guidance",
                    "sources",
                    "ruleIds"
                  ],
                  "properties": {
                    "topic": {
                      "type": "string",
                      "description": "The topic as requested."
                    },
                    "guidance": {
                      "type": "string",
                      "description": "Plain-language guidance text in the requested locale,\nincluding relevant article and annex references.\n"
                    },
                    "sources": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "instrument",
                          "article"
                        ],
                        "properties": {
                          "instrument": {
                            "type": "string"
                          },
                          "article": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "ruleIds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "topic": "registration",
                  "guidance": "Manufacturers placing batteries on the EU market must register in the EU Battery Registry before the first placement. The registration obligation applies from 18 August 2024 under Art. 47 of EU 2023/1542.",
                  "sources": [
                    {
                      "instrument": "EU 2023/1542",
                      "article": "Art. 47"
                    }
                  ],
                  "ruleIds": [
                    "32023R1542#guidance.registration@1"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid `topic`. `allowed` lists valid values.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "field",
                    "message",
                    "allowed"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "invalid_input"
                      ]
                    },
                    "field": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "allowed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "error": "invalid_input",
                  "field": "topic",
                  "message": "Invalid option: expected one of \"registration\"|\"identifiers\"|...",
                  "allowed": [
                    "registration",
                    "identifiers",
                    "data_carriers",
                    "qr_code",
                    "nfc_rfid",
                    "gs1_digital_link",
                    "access_rights"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Account not eligible (OAuth + `X-Source: assistant` only).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "reason"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "account_not_eligible"
                      ]
                    },
                    "reason": {
                      "type": "string",
                      "enum": [
                        "email_not_verified",
                        "disposable_email"
                      ]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Daily scope-answer cap reached.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "resetsAt"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "scope_answer_cap"
                      ]
                    },
                    "resetsAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Regulatory data temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "regulatory_data_unavailable"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/company/compliance-profile": {
      "get": {
        "operationId": "getComplianceProfile",
        "tags": [
          "Regulatory"
        ],
        "summary": "Read the company compliance profile.",
        "description": "Returns the stored compliance profile for the authenticated\ncompany — the company facts the DPP Assistant uses to tailor its\nanswers (role, product categories, EU markets, size, sales channels,\ndata systems). Returns `{ \"complianceProfile\": {} }` when no profile\nhas been saved yet.\n\nAuth: Bearer API key OR OAuth with `regulatory:read` OR\n`passports:read`. Does not count as a scope answer.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "regulatory:read"
            ]
          },
          {
            "OAuth2": [
              "passports:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <api-key>` or `Bearer <access-token>`."
          }
        ],
        "responses": {
          "200": {
            "description": "Company compliance profile (may be empty).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "complianceProfile"
                  ],
                  "properties": {
                    "complianceProfile": {
                      "type": "object",
                      "description": "The stored profile. Empty object `{}` when nothing\nhas been saved yet.\n",
                      "properties": {
                        "role": {
                          "type": "string",
                          "enum": [
                            "manufacturer",
                            "importer",
                            "distributor",
                            "authorised_representative",
                            "fulfilment_service_provider"
                          ]
                        },
                        "ownBrand": {
                          "type": "boolean"
                        },
                        "categories": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "markets": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "ISO-3166-1 alpha-2 codes or `EU`."
                        },
                        "sizeBand": {
                          "type": "string",
                          "enum": [
                            "sme",
                            "large"
                          ]
                        },
                        "salesChannels": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "own_shop",
                              "marketplace",
                              "retail",
                              "b2b",
                              "fulfilment"
                            ]
                          }
                        },
                        "dataSystems": {
                          "type": "string",
                          "maxLength": 200
                        }
                      }
                    },
                    "updatedAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the profile was last updated; absent when empty."
                    }
                  }
                },
                "examples": {
                  "populated": {
                    "summary": "Profile with stored facts",
                    "value": {
                      "complianceProfile": {
                        "role": "manufacturer",
                        "categories": [
                          "battery"
                        ],
                        "markets": [
                          "EU"
                        ],
                        "sizeBand": "sme"
                      },
                      "updatedAt": "2026-10-11T19:01:12.152Z"
                    }
                  },
                  "empty": {
                    "summary": "No profile saved yet",
                    "value": {
                      "complianceProfile": {}
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "patch": {
        "operationId": "patchComplianceProfile",
        "tags": [
          "Regulatory"
        ],
        "summary": "Update (merge) the company compliance profile.",
        "description": "Merges the provided fields into the stored compliance profile and\nreturns the updated profile with `updatedAt`. Fields not included\nin the request are left unchanged. Set a field to `null` to remove\nit.\n\nAuth: Bearer API key OR OAuth with `passports:write`. For OAuth\nprincipals, the user must have the `admin` or `owner` role.\nAPI-key callers are not role-gated. Does not count as a scope\nanswer.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <api-key>` or `Bearer <access-token>`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "manufacturer",
                      "importer",
                      "distributor",
                      "authorised_representative",
                      "fulfilment_service_provider",
                      null
                    ],
                    "description": "Company's role in the supply chain."
                  },
                  "ownBrand": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Whether the company sells under its own brand."
                  },
                  "categories": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string"
                    },
                    "description": "Template category keys the company deals with."
                  },
                  "markets": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string"
                    },
                    "description": "ISO-3166-1 alpha-2 codes or `EU` for all EU markets."
                  },
                  "sizeBand": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "sme",
                      "large",
                      null
                    ],
                    "description": "Company size."
                  },
                  "salesChannels": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string",
                      "enum": [
                        "own_shop",
                        "marketplace",
                        "retail",
                        "b2b",
                        "fulfilment"
                      ]
                    }
                  },
                  "dataSystems": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "Short description of data systems (ERP, PIM, etc.)."
                  }
                }
              },
              "example": {
                "role": "manufacturer",
                "categories": [
                  "battery"
                ],
                "markets": [
                  "EU"
                ],
                "sizeBand": "sme"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated compliance profile.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "complianceProfile",
                    "updatedAt"
                  ],
                  "properties": {
                    "complianceProfile": {
                      "type": "object",
                      "description": "The full profile after the merge."
                    },
                    "updatedAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "complianceProfile": {
                    "role": "manufacturer",
                    "categories": [
                      "battery"
                    ],
                    "markets": [
                      "EU"
                    ],
                    "sizeBand": "sme"
                  },
                  "updatedAt": "2026-10-11T19:01:12.152Z"
                }
              }
            }
          },
          "400": {
            "description": "Invalid field value. `field` names the offending key;\n`allowed` lists valid options for enum fields.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "field",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "invalid_input"
                      ]
                    },
                    "field": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "allowed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "error": "invalid_input",
                  "field": "role",
                  "message": "Invalid option: expected one of \"manufacturer\"|\"importer\"|..."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin or owner role required (OAuth principals only).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "admin role required to update the compliance profile"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteComplianceProfile",
        "tags": [
          "Regulatory"
        ],
        "summary": "Delete the company compliance profile.",
        "description": "Removes the entire `complianceProfile` sub-document from the\ncompany record. Returns `{ \"deleted\": true }` on success. After\ndeletion, `GET /api/v1/company/compliance-profile` returns an\nempty profile. Does not count as a scope answer.\n\nAuth: Bearer API key OR OAuth with `passports:write`. For OAuth\nprincipals, the user must have the `admin` or `owner` role.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "passports:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <api-key>` or `Bearer <access-token>`."
          }
        ],
        "responses": {
          "200": {
            "description": "Profile deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deleted"
                  ],
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                },
                "example": {
                  "deleted": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin or owner role required (OAuth principals only).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "admin role required to delete the compliance profile"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scope/reports": {
      "post": {
        "operationId": "postScopeReport",
        "tags": [
          "Scope"
        ],
        "summary": "Report a wrong or unclear scope or regulatory answer.",
        "description": "Submits a correction report to a review queue. Include the\n`ruleIds` from the response envelope of the scope-check or\nregulatory-assistant call to route the report quickly. Does not\ncount as a scope answer.\n\nAuth: Bearer API key OR OAuth with `regulatory:read` OR\n`passports:read`.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "OAuth2": [
              "regulatory:read"
            ]
          },
          {
            "OAuth2": [
              "passports:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`Bearer <api-key>` or `Bearer <access-token>`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "what"
                ],
                "properties": {
                  "what": {
                    "type": "string",
                    "description": "Plain-language description of what is wrong or unclear.\nInclude the product type, the facts used, and what the\nanswer said that was incorrect.\n"
                  },
                  "ruleIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Rule IDs from the `ruleIds` array in the response\nenvelope that the report concerns.\n"
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "api",
                      "mcp",
                      "assistant",
                      "dashboard"
                    ],
                    "description": "Channel the original answer came from."
                  }
                }
              },
              "example": {
                "what": "The answer said toy safety applies from 2030-08-01 but I believe the date for products already on the market is different.",
                "ruleIds": [
                  "32025R2509#duty.dpp_toys@1"
                ],
                "channel": "assistant"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Report created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Report ID."
                    }
                  }
                },
                "example": {
                  "id": "6acbdcfd8683429825453952"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid `what` field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": "invalid_input",
                  "field": "what",
                  "message": "Invalid input: expected string, received undefined"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/oauth/register": {
      "post": {
        "operationId": "oauthRegister",
        "tags": [
          "OAuth"
        ],
        "summary": "Register an OAuth client (Dynamic Client Registration).",
        "description": "RFC 7591 Dynamic Client Registration. Public, unauthenticated —\na hosted client (e.g. an MCP host) self-registers before any\nuser is involved. Public clients (PKCE-only) omit\n`token_endpoint_auth_method` or set it to `none` and receive no\nsecret; confidential clients set `client_secret_post` and\nreceive a `client_secret` once. Rate-limited per IP.\n(Developers can also register apps interactively at Developer →\nOAuth Apps in the dashboard.)\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "client_name",
                  "redirect_uris"
                ],
                "properties": {
                  "client_name": {
                    "type": "string",
                    "example": "Acme ERP Connector"
                  },
                  "redirect_uris": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uri"
                    },
                    "example": [
                      "https://acme.example/oauth/callback"
                    ]
                  },
                  "scope": {
                    "type": "string",
                    "example": "passports:read passports:write offline_access"
                  },
                  "token_endpoint_auth_method": {
                    "type": "string",
                    "enum": [
                      "none",
                      "client_secret_post"
                    ],
                    "default": "none"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Client registered. `client_secret` present only for confidential clients.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "client_id": {
                      "type": "string",
                      "example": "oac_3f9a..."
                    },
                    "client_secret": {
                      "type": "string",
                      "description": "Returned ONCE; confidential clients only."
                    },
                    "client_name": {
                      "type": "string"
                    },
                    "redirect_uris": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "scope": {
                      "type": "string"
                    },
                    "token_endpoint_auth_method": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid client metadata or redirect_uri."
          }
        }
      }
    },
    "/api/oauth/authorize": {
      "get": {
        "operationId": "oauthAuthorize",
        "tags": [
          "OAuth"
        ],
        "summary": "Authorization endpoint (start the auth-code flow).",
        "description": "RFC 6749 §4.1.1. Validates the request, then redirects the\nlogged-in user to the consent screen; on approval the browser\nis redirected back to `redirect_uri` with a single-use `code`.\nPKCE is mandatory (`code_challenge_method=S256`). An unknown\nclient or unregistered `redirect_uri` returns 400 (no redirect);\nother parameter errors redirect back with `?error=`.\n",
        "security": [],
        "parameters": [
          {
            "name": "response_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "code"
              ]
            }
          },
          {
            "name": "client_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "redirect_uri",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uri"
            }
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Space-delimited scopes (narrowed to the client's ceiling)."
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "code_challenge",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "base64url SHA-256 of the code_verifier."
          },
          {
            "name": "code_challenge_method",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "S256"
              ]
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to the consent screen, or back to the client with code / error."
          },
          "400": {
            "description": "Unknown client or unregistered redirect_uri."
          }
        }
      }
    },
    "/api/oauth/token": {
      "post": {
        "operationId": "oauthToken",
        "tags": [
          "OAuth"
        ],
        "summary": "Token endpoint (exchange code or refresh token).",
        "description": "RFC 6749 §3.2. Form-encoded. `grant_type=authorization_code`\nexchanges a PKCE-bound code (send `code_verifier`) for a 15-min\naccess token (+ refresh token when `offline_access` was\ngranted). `grant_type=refresh_token` rotates the refresh token.\nConfidential clients also send `client_secret`. Responses are\n`Cache-Control: no-store`.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": [
                      "authorization_code",
                      "refresh_token"
                    ]
                  },
                  "code": {
                    "type": "string"
                  },
                  "redirect_uri": {
                    "type": "string",
                    "format": "uri"
                  },
                  "code_verifier": {
                    "type": "string"
                  },
                  "refresh_token": {
                    "type": "string"
                  },
                  "client_id": {
                    "type": "string"
                  },
                  "client_secret": {
                    "type": "string",
                    "description": "Confidential clients only."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Access token issued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string"
                    },
                    "token_type": {
                      "type": "string",
                      "enum": [
                        "Bearer"
                      ]
                    },
                    "expires_in": {
                      "type": "integer",
                      "example": 900
                    },
                    "scope": {
                      "type": "string"
                    },
                    "refresh_token": {
                      "type": "string",
                      "description": "Present only when offline_access was granted."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_grant / invalid_request / unsupported_grant_type."
          },
          "401": {
            "description": "invalid_client (bad client authentication)."
          }
        }
      }
    },
    "/api/oauth/revoke": {
      "post": {
        "operationId": "oauthRevoke",
        "tags": [
          "OAuth"
        ],
        "summary": "Revoke a token (RFC 7009).",
        "description": "Revoke a refresh token. Always returns 200 even for an unknown\ntoken (per RFC 7009, don't leak token validity). Confidential\nclients authenticate with `client_secret`.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "token",
                  "client_id"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "client_id": {
                    "type": "string"
                  },
                  "client_secret": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledged (regardless of whether the token existed)."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "opaque",
        "description": "API-key Bearer token — the service-account method, best for\nserver-to-server and ERP integrations. Mint at Developer →\nAPI keys in the dashboard. Keys carry the `tp_` prefix\nfollowed by an opaque random suffix, and are company-scoped\n(all-or-nothing — they bypass OAuth scope checks).\n"
      },
      "OAuth2": {
        "type": "oauth2",
        "description": "OAuth 2.0 with PKCE — the user-authorized method, best for\nthird-party apps and AI assistants that act on a user's\nbehalf. A user grants specific scopes on a consent screen;\nthe resulting access token is limited to the intersection of\nthe user's dashboard role and the granted scopes. Register an\napp at Developer → OAuth Apps, or via Dynamic Client\nRegistration (RFC 7591) at `/api/oauth/register`. Discovery\nmetadata is at `/.well-known/oauth-authorization-server`.\n",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://app.tracepass.eu/api/oauth/authorize",
            "tokenUrl": "https://app.tracepass.eu/api/oauth/token",
            "refreshUrl": "https://app.tracepass.eu/api/oauth/token",
            "scopes": {
              "passports:read": "View products and digital product passports",
              "passports:write": "Create and update passports (publishing stays in the dashboard)",
              "regulatory:read": "Check product scope against EU DPP regulations",
              "documents:read": "Read uploaded documents and extractions",
              "documents:write": "Upload documents and run AI extractions",
              "suppliers:read": "View supplier requests",
              "suppliers:write": "Create and manage supplier requests",
              "offline_access": "Issue a refresh token (stay connected)"
            }
          }
        }
      },
      "SessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "tp_session",
        "description": "Dashboard session cookie issued by `/api/auth/login`.\nUsed only for the bulk JSON-LD tenant export, which is\ndeliberately not exposed via API key.\n"
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 64
        },
        "description": "UUID v4 (or any opaque string ≤ 64 chars) per logical\noperation. The platform stores the first response for 24\nhours and replays it on the same key + same workspace.\nReusing the same key with a different request body returns\n422. A second request with the same key while the first is\nstill running returns 409 `idempotency_key_in_use`; retry\nwith the same key once the first has finished.\n"
      },
      "PassportId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/ResourceId"
        }
      },
      "ProductId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/ResourceId"
        }
      },
      "PartyRole": {
        "name": "role",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/PartyRoleEnum"
        }
      },
      "FieldKey": {
        "name": "key",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Field key (camelCase) as defined on the passport's\ncategory template — e.g. `nominalVoltage`,\n`batteryChemistry`, `recycledContentCobalt`.\n"
      },
      "Serial": {
        "name": "serial",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100
        },
        "description": "Passport serial number, as supplied by the customer at\npassport-create time. Used by the by-serial alternate\naddressing forms.\n"
      },
      "GtinQuery": {
        "name": "gtin",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "Optional disambiguator for the by-serial routes. A serial is\nunique only WITHIN a GTIN, so if the same serial exists on\npassports under two different GTINs in your account, a\nserial-only lookup is ambiguous and returns 409. Pass the\n`gtin` to resolve precisely — `(account, gtin, serial)` is\nunique. Omit it when your serials are unique account-wide.\n"
      },
      "Page": {
        "name": "page",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 1
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        }
      }
    },
    "schemas": {
      "ResourceId": {
        "type": "string",
        "pattern": "^[a-f0-9]{24}$",
        "example": "6650a1b2c3d4e5f6a7b8c9d0",
        "description": "A TracePass resource ID — 24 lowercase hexadecimal characters."
      },
      "GTIN": {
        "type": "string",
        "pattern": "^[0-9]{8}$|^[0-9]{12}$|^[0-9]{13}$|^[0-9]{14}$",
        "example": "04012345000015",
        "description": "GS1 GTIN with a valid check digit. Accepts 8 digits (GTIN-8), 12 digits (GTIN-12/UPC-A), 13 digits (GTIN-13/EAN-13), or 14 digits (GTIN-14). All forms are left-padded to 14 digits before storage; stored and returned values are always 14 digits."
      },
      "GLN": {
        "type": "string",
        "pattern": "^[0-9]{13}$",
        "example": "4012345678901",
        "description": "GS1 Global Location Number (13 digits, mod-10)."
      },
      "PartyRoleEnum": {
        "type": "string",
        "enum": [
          "manufacturer",
          "importer",
          "authorisedRepresentative",
          "distributor",
          "recycler",
          "producerResponsibilityOrg"
        ]
      },
      "Party": {
        "type": "object",
        "required": [
          "legalName"
        ],
        "description": "Economic-operator party. At least one of `gln`, `legacyOperatorId`,\nor `operatorIdentifier` must be set — without an identifier the\nParty doesn't actually identify anyone.\n\nIf both `gln` and `operatorIdentifier` (scheme `gln`) are present\nthey must carry the same 13-digit value; a mismatch returns 400\nwith `path: operatorIdentifier`. An `operatorIdentifier` of scheme\n`gln` also fills the top-level `gln` field. `facilityIdentifier`\nnever fills `gln`. Typed identifiers are not populated by AI\nextraction or CSV import.\n",
        "properties": {
          "gln": {
            "$ref": "#/components/schemas/GLN"
          },
          "legalName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "example": "EuroBat Recyclers GmbH"
          },
          "country": {
            "type": "string",
            "pattern": "^[A-Z]{2}$",
            "description": "ISO 3166-1 alpha-2 country code.",
            "example": "DE"
          },
          "legacyOperatorId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "Free-text fallback identifier (VAT, EORI, supplier\ncode) for parties without a GLN.\n",
            "example": "DE123456789"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 500
          },
          "tradeName": {
            "type": "string",
            "maxLength": 200,
            "description": "Registered trade name or registered trade mark. For a battery\nimporter, one of the details it must mark on the battery (Battery\nRegulation Art. 41(3)). Shown on the public passport.\n",
            "example": "EuroBat"
          },
          "postalAddress": {
            "type": "string",
            "maxLength": 500,
            "description": "Postal address with a single contact point, as one line. Shown on\nthe public passport.\n",
            "example": "Industriestraße 42, 80339 Munich, Germany"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Contact e-mail address. Shown on the public passport, so use a\ncompany contact address, not a person's.\n",
            "example": "compliance@example.com"
          },
          "status": {
            "type": "string",
            "readOnly": true,
            "description": "Server-side status (active, etc.). Returned but ignored on write."
          },
          "operatorIdentifier": {
            "description": "Structured operator identifier per EN 18219 §6.2–6.5. One of\nfour schemes: iso6523, gln, did, doi. When scheme is `gln` this\nalso fills the top-level `gln` field; they must match if both\nare set (400 with path `operatorIdentifier`). An LEI (iso6523,\nicd `0199`) is also verified against the GLEIF register (see\n`upsertParty`). Not populated by AI extraction or CSV import.\n",
            "oneOf": [
              {
                "$ref": "#/components/schemas/OperatorIdentifierISO6523"
              },
              {
                "$ref": "#/components/schemas/OperatorIdentifierGLN"
              },
              {
                "$ref": "#/components/schemas/OperatorIdentifierDID"
              },
              {
                "$ref": "#/components/schemas/OperatorIdentifierDOI"
              }
            ],
            "discriminator": {
              "propertyName": "scheme",
              "mapping": {
                "iso6523": "#/components/schemas/OperatorIdentifierISO6523",
                "gln": "#/components/schemas/OperatorIdentifierGLN",
                "did": "#/components/schemas/OperatorIdentifierDID",
                "doi": "#/components/schemas/OperatorIdentifierDOI"
              }
            }
          },
          "facilityIdentifier": {
            "description": "Structured facility identifier per EN 18219 §6.2–6.5. Same four\nschemes as `operatorIdentifier`; the `gln` variant also accepts\nan optional `extension` (GS1 SGLN sub-location code). Never fills\nthe top-level `gln` field. Not populated by AI extraction or CSV\nimport.\n",
            "oneOf": [
              {
                "$ref": "#/components/schemas/OperatorIdentifierISO6523"
              },
              {
                "$ref": "#/components/schemas/FacilityIdentifierGLN"
              },
              {
                "$ref": "#/components/schemas/OperatorIdentifierDID"
              },
              {
                "$ref": "#/components/schemas/OperatorIdentifierDOI"
              }
            ],
            "discriminator": {
              "propertyName": "scheme",
              "mapping": {
                "iso6523": "#/components/schemas/OperatorIdentifierISO6523",
                "gln": "#/components/schemas/FacilityIdentifierGLN",
                "did": "#/components/schemas/OperatorIdentifierDID",
                "doi": "#/components/schemas/OperatorIdentifierDOI"
              }
            }
          }
        }
      },
      "OperatorIdentifierISO6523": {
        "type": "object",
        "required": [
          "scheme",
          "icd",
          "value"
        ],
        "description": "ISO/IEC 6523 operator identifier. ICD 0199 = LEI (ISO 17442 check\ndigits), 0088 = GLN, 0060 = DUNS. EN 18219 §6.2.\n",
        "properties": {
          "scheme": {
            "type": "string",
            "enum": [
              "iso6523"
            ]
          },
          "icd": {
            "type": "string",
            "pattern": "^\\d{4}$",
            "description": "4-digit ISO/IEC 6523 ICD (International Code Designator).",
            "example": "0199"
          },
          "value": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256,
            "description": "The identifier value within the specified ICD scheme.",
            "example": "529900T8BM49AURSDO55"
          }
        }
      },
      "OperatorIdentifierGLN": {
        "type": "object",
        "required": [
          "scheme",
          "gln"
        ],
        "description": "GS1 GLN operator identifier. Also fills the Party's top-level `gln`\nfield; they must agree if both are set. EN 18219 §6.3.\n",
        "properties": {
          "scheme": {
            "type": "string",
            "enum": [
              "gln"
            ]
          },
          "gln": {
            "type": "string",
            "pattern": "^\\d{13}$",
            "description": "13-digit GS1 Global Location Number.",
            "example": "4012345678901"
          }
        }
      },
      "FacilityIdentifierGLN": {
        "type": "object",
        "required": [
          "scheme",
          "gln"
        ],
        "description": "GS1 GLN facility identifier. Optionally carries a GS1 SGLN\nsub-location extension. Does not fill the Party's top-level `gln`\nfield. EN 18219 §6.3.\n",
        "properties": {
          "scheme": {
            "type": "string",
            "enum": [
              "gln"
            ]
          },
          "gln": {
            "type": "string",
            "pattern": "^\\d{13}$",
            "description": "13-digit GS1 Global Location Number.",
            "example": "4012345678901"
          },
          "extension": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "GS1 SGLN sub-location extension (identifies a specific location within the GLN).",
            "example": "1"
          }
        }
      },
      "OperatorIdentifierDID": {
        "type": "object",
        "required": [
          "scheme",
          "did"
        ],
        "description": "W3C DID (Decentralised Identifier). Syntax-checked only per\nEN 18219 §6.4.2(b) — it becomes a verifiable operator identifier\nwhen an authorised register issues a VC; the platform does not\nperform VC verification. EN 18219 §6.4.\n",
        "properties": {
          "scheme": {
            "type": "string",
            "enum": [
              "did"
            ]
          },
          "did": {
            "type": "string",
            "pattern": "^did:[a-z0-9]+:.+",
            "maxLength": 512,
            "description": "Decentralised Identifier matching did:<method>:<method-specific-id>. W3C DID Core requires the method to be lowercase alphanumerics only.",
            "example": "did:web:example.com"
          }
        }
      },
      "OperatorIdentifierDOI": {
        "type": "object",
        "required": [
          "scheme",
          "doi"
        ],
        "description": "ISO 26324 DOI operator identifier. A `doi:` or\n`https://doi.org/` / `https://dx.doi.org/` prefix is accepted on\ninput and stripped; stored bare. EN 18219 §6.5.\n",
        "properties": {
          "scheme": {
            "type": "string",
            "enum": [
              "doi"
            ]
          },
          "doi": {
            "type": "string",
            "pattern": "^(?:doi:|https?:\\/\\/(?:dx\\.)?doi\\.org\\/)?10\\.\\d{4,9}\\/\\S+$",
            "maxLength": 512,
            "description": "Digital Object Identifier. Bare `10.<4–9 digit registrant>/<suffix>`\nform, or with an accepted prefix (`doi:` / `https://doi.org/` /\n`https://dx.doi.org/`) which is stripped before storage.\n",
            "example": "10.1234/example"
          }
        }
      },
      "PartyMap": {
        "type": "object",
        "additionalProperties": {
          "$ref": "#/components/schemas/Party"
        },
        "description": "Map of role → Party. Each role appears at most once.\n"
      },
      "GS1Block": {
        "type": "object",
        "required": [
          "gtin",
          "serialNumber"
        ],
        "properties": {
          "gtin": {
            "$ref": "#/components/schemas/GTIN"
          },
          "serialNumber": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "example": "BP-48V-100-000001"
          },
          "digitalLinkUri": {
            "type": "string",
            "format": "uri",
            "readOnly": true,
            "example": "https://id.tracepass.eu/p/01/04012345000015/21/BP-48V-100-000001"
          }
        }
      },
      "PassportStatus": {
        "type": "string",
        "description": "`marked_for_deletion` is the first step of a deletion\n(`POST /api/v1/passports/{id}/mark-for-deletion`); a marked\npassport can no longer be edited. `deleted` appears only in\nthe response of the `DELETE /api/v1/passports/{id}` that\ndeletes it: a deleted passport is never returned by a read,\nand every v1 call on it answers 404.\n",
        "enum": [
          "draft",
          "in_review",
          "approved",
          "published",
          "suspended",
          "expired",
          "archived",
          "marked_for_deletion",
          "deleted"
        ]
      },
      "DeletionReason": {
        "type": "string",
        "description": "Why a passport ends. Required for battery passports when they\nare marked for deletion or archived: under the Battery\nRegulation a battery passport ends only once the battery has\nbeen recycled (Regulation (EU) 2023/1542, Art. 77(8)).\n`never_placed` means the battery was never placed on the\nmarket.\n",
        "enum": [
          "recycled",
          "never_placed",
          "duplicate_or_error"
        ]
      },
      "DeletionReasonInput": {
        "type": "object",
        "description": "Optional request body. `reason` is required for battery\npassports and optional for other categories; when given it\nmust be one of the listed values.\n",
        "properties": {
          "reason": {
            "$ref": "#/components/schemas/DeletionReason"
          }
        }
      },
      "PassportHardDeleteResult": {
        "type": "object",
        "description": "Receipt of a permanent delete of a never-published passport.",
        "properties": {
          "message": {
            "type": "string"
          },
          "summary": {
            "type": "object",
            "properties": {
              "passportId": {
                "type": "string"
              },
              "serialNumber": {
                "type": "string"
              },
              "gtin": {
                "type": "string"
              },
              "deletedCounts": {
                "type": "object",
                "description": "What was removed with the passport.",
                "properties": {
                  "extractions": {
                    "type": "integer"
                  },
                  "serviceEvents": {
                    "type": "integer"
                  },
                  "scanEvents": {
                    "type": "integer"
                  },
                  "ownershipTransfers": {
                    "type": "integer"
                  },
                  "supplierRequestRefsRemoved": {
                    "type": "integer"
                  },
                  "epcisEventRefsRemoved": {
                    "type": "integer"
                  },
                  "epcisEventsDeleted": {
                    "type": "integer"
                  },
                  "documentRefsRemoved": {
                    "type": "integer"
                  },
                  "documentsDeleted": {
                    "type": "integer"
                  },
                  "filesDeleted": {
                    "type": "integer"
                  }
                }
              },
              "dppsActiveDelta": {
                "type": "integer",
                "enum": [
                  -1,
                  0
                ],
                "description": "How the live-active counter moved (always -1 or 0)."
              }
            }
          }
        }
      },
      "PassportField": {
        "type": "object",
        "properties": {
          "value": {},
          "source": {
            "type": "string",
            "enum": [
              "manual",
              "ai_suggested",
              "ai_approved",
              "reference_db",
              "supplier",
              "system",
              "derived"
            ],
            "description": "Who set the stored value. `derived` is a value calculated from other fields by a formula, not written by an AI model."
          },
          "aiProvenance": {
            "$ref": "#/components/schemas/AiProvenance"
          },
          "aiTranslated": {
            "type": "boolean",
            "description": "Present and `true` on a `?lang=` response when the returned value is a machine translation (EU AI Act Art. 50(2) marking)."
          },
          "sourceProvider": {
            "type": "string",
            "description": "Identifier of the reference source that vouches for an AI-suggested value (for example a public register), with `sourceUrl` linking to its record where one is published. An open string: the set of sources can grow, so treat unknown values as \"a named source\". Absent when no single source vouches for the stored value, such as a value read from your own document or submitted by a supplier.",
            "example": "eprel"
          },
          "sourceUrl": {
            "type": "string",
            "format": "uri",
            "description": "The provider's canonical URL for this value, when it published one. Machine-readable companion to `evidence`.",
            "example": "https://eprel.ec.europa.eu/qr/450201"
          },
          "status": {
            "type": "string"
          },
          "accessLevel": {
            "type": "string"
          },
          "sourceLocale": {
            "type": "string"
          },
          "translations": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "value": {},
                "source": {
                  "type": "string"
                },
                "generatedAt": {
                  "type": "string",
                  "format": "date-time"
                },
                "aiProvenance": {
                  "$ref": "#/components/schemas/AiProvenance"
                }
              }
            }
          },
          "lastUpdatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastUpdatedBy": {
            "type": "string"
          }
        }
      },
      "AiProvenance": {
        "type": "object",
        "description": "Marks a value an AI system generated (EU AI Act Art. 50(2)). It stays on the field after a person approves or edits the value; only the two review flags change. Public passport outputs (viewer, JSON-LD, credential, PDF) mark the `research` and `translation` kinds.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "research",
              "translation",
              "extraction"
            ],
            "description": "`research`: found and written by the AI. `translation`: machine translated. `extraction`: read from a document you uploaded."
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "model": {
            "type": "string",
            "description": "The model that generated the value. Returned only to the owning account."
          },
          "humanReviewed": {
            "type": "boolean"
          },
          "humanEdited": {
            "type": "boolean"
          }
        }
      },
      "ComplianceFinding": {
        "type": "object",
        "description": "One compliance gap or warning, with regulation provenance.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "missing_field",
              "unapproved_field",
              "missing_party",
              "invalid_format",
              "conditional_missing",
              "unverifiable_conditional"
            ]
          },
          "severity": {
            "type": "string",
            "enum": [
              "critical",
              "warning"
            ]
          },
          "target": {
            "type": "string",
            "description": "Field key or party role this finding concerns."
          },
          "regulation": {
            "type": "string",
            "description": "Regulation number, e.g. \"(EU) 2023/1542\"."
          },
          "article": {
            "type": "string",
            "description": "Article / annex citation, e.g. \"Art. 77\"."
          },
          "ruleId": {
            "type": "string",
            "description": "Rule id from the verified spec (e.g. \"BAT-1\", \"CC-1\")."
          },
          "why": {
            "type": "string",
            "description": "One-line explanation of why this matters."
          },
          "fix": {
            "type": "string",
            "description": "One-line actionable next step."
          }
        },
        "required": [
          "type",
          "severity",
          "why"
        ]
      },
      "SnapshotEntry": {
        "type": "object",
        "description": "A single item in the immutability snapshot list. Contains everything needed for an ERP integration to locate and inspect a snapshot, including whether the stored hash still verifies.\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "The snapshot's TracePass ID (24 hexadecimal characters)."
          },
          "version": {
            "type": "integer",
            "description": "The passport version counter at the time of this snapshot."
          },
          "reason": {
            "type": "string",
            "enum": [
              "published",
              "republished",
              "manual",
              "field_edit",
              "status_change",
              "parties",
              "supplier",
              "ai",
              "restore",
              "system",
              "baseline"
            ],
            "description": "Why this snapshot was taken."
          },
          "actor": {
            "type": "string",
            "description": "Who caused the change, when known: `user:<id>`, `apikey:<prefix>`, `oauth:<clientId>`, `supplier:<requestId>` or `system:<job>`."
          },
          "snapshotAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the snapshot was taken (ISO 8601)."
          },
          "contentHash": {
            "type": "string",
            "description": "SHA-256 hex digest of the canonical JSON-LD archived at snapshot time. Re-verified on every read — if the stored document was tampered with at rest, `hashValid` is false.\n"
          },
          "hashValid": {
            "type": "boolean",
            "description": "True when the freshly-computed sha256(jsonLd) matches the stored `contentHash`. False indicates at-rest tampering.\n"
          },
          "restorable": {
            "type": "boolean",
            "description": "True when the snapshot carries `rawFields` and can drive a field-level restore. Snapshots taken before `rawFields` was captured are evidence-only (permanently `restorable: false`).\n"
          },
          "fieldCount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Number of fields captured in `rawFields`, or `null` for pre-`rawFields` snapshots.\n"
          }
        },
        "required": [
          "id",
          "version",
          "reason",
          "snapshotAt",
          "contentHash",
          "hashValid",
          "restorable",
          "fieldCount"
        ]
      },
      "SnapshotList": {
        "type": "object",
        "description": "Paginated list of passport snapshots.",
        "properties": {
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SnapshotEntry"
            }
          },
          "total": {
            "type": "integer",
            "description": "Total snapshots matching the query across all pages."
          },
          "page": {
            "type": "integer",
            "description": "Current page number (1-based)."
          },
          "limit": {
            "type": "integer",
            "description": "Page size used for this response."
          },
          "totalPages": {
            "type": "integer",
            "description": "Total number of pages at the current limit."
          }
        },
        "required": [
          "entries",
          "total",
          "page",
          "limit",
          "totalPages"
        ]
      },
      "SnapshotDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SnapshotEntry"
          },
          {
            "type": "object",
            "description": "Full archival record for one snapshot — all `SnapshotEntry` fields plus the complete authority-level JSON-LD the passport asserted at snapshot time.\n",
            "properties": {
              "jsonLd": {
                "type": "object",
                "description": "The complete JSON-LD document archived at snapshot time — what the public passport viewer would have returned for this passport on the snapshot date. The content hash covers this field only.\n"
              }
            },
            "required": [
              "jsonLd"
            ]
          }
        ]
      },
      "RegistryReadinessResult": {
        "type": "object",
        "description": "Whether a passport would pass the EU DPP Registry's formal submission gate. Distinct from the substantive ComplianceVerdict.\n",
        "properties": {
          "ready": {
            "type": "boolean",
            "description": "True only when there are zero critical findings — i.e. the passport would pass the registry's formal gate.\n"
          },
          "findings": {
            "type": "array",
            "description": "Formal-gate findings, each with a REG-READY-* ruleId (REG-READY-MANDATORY / REG-READY-FORMAT / REG-READY-LINK / REG-READY-GRANULARITY / REG-READY-COMMODITY / REG-READY-SCOPE). Critical findings block readiness; warnings do not.\n",
            "items": {
              "$ref": "#/components/schemas/ComplianceFinding"
            }
          }
        },
        "required": [
          "ready",
          "findings"
        ]
      },
      "ConditionFlag": {
        "type": "object",
        "description": "A single resolved condition flag — an approved yes/no fact about a product that gates conditional legal duties (e.g. `hasBMS` for batteries gates specific Annex XIII fields under Regulation (EU) 2023/1542).\n",
        "required": [
          "value",
          "status"
        ],
        "properties": {
          "value": {
            "type": "boolean",
            "description": "The flag's current value (true = condition applies)."
          },
          "status": {
            "type": "string",
            "enum": [
              "approved",
              "pending_review"
            ],
            "description": "`approved` — the flag is authoritative and gates conditional duties. `pending_review` — the flag has been set but not yet reviewed; it does not trigger conditional requirements until approved.\n"
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "description": "Origin of the last write. API writes are tagged `api:<key-prefix>`; dashboard writes are tagged with the reviewer's name or left null for system writes.\n"
          }
        }
      },
      "ConditionProfile": {
        "type": "object",
        "description": "A map of flagKey → ConditionFlag representing the resolved condition profile for a passport. Valid flag keys are category-specific: `battery` supports `hasBMS`, `rechargeable`, `externalStorageOnly`, and `isStationaryBess`. Categories with no flags return an empty object. The `audit` array is stripped from v1 responses.\n",
        "additionalProperties": {
          "$ref": "#/components/schemas/ConditionFlag"
        },
        "example": {
          "hasBMS": {
            "value": true,
            "status": "approved",
            "source": "api:tp_abc123"
          },
          "rechargeable": {
            "value": true,
            "status": "approved",
            "source": "api:tp_abc123"
          }
        }
      },
      "ConditionFlagsPatch": {
        "type": "object",
        "description": "Partial update for the condition profile. Each key must be registered for the passport's category (unknown keys return 400). Set a flag with `true`/`false`; clear it with `null`. Provide at least one key.\n",
        "additionalProperties": {
          "type": [
            "boolean",
            "null"
          ]
        },
        "minProperties": 1,
        "example": {
          "hasBMS": true,
          "rechargeable": false
        }
      },
      "ComplianceVerdict": {
        "type": "object",
        "description": "Three-tier compliance verdict for a passport.",
        "properties": {
          "verdict": {
            "type": "string",
            "enum": [
              "compliant",
              "compliant_with_warnings",
              "incomplete"
            ]
          },
          "category": {
            "type": "string"
          },
          "conditionalCoverage": {
            "type": "string",
            "enum": [
              "evaluated",
              "static-only"
            ],
            "description": "Whether per-category conditional rules ran (`evaluated`) or\nno binding conditional exists yet for this category\n(`static-only`). Absence of conditional findings is only\nmeaningful alongside this flag.\n"
          },
          "critical": {
            "type": "array",
            "description": "Findings that force the `incomplete` verdict.",
            "items": {
              "$ref": "#/components/schemas/ComplianceFinding"
            }
          },
          "warnings": {
            "type": "array",
            "description": "Findings that downgrade to `compliant_with_warnings`.",
            "items": {
              "$ref": "#/components/schemas/ComplianceFinding"
            }
          },
          "checkedRules": {
            "type": "array",
            "description": "Rule ids that actually ran (static checks + each conditional rule).",
            "items": {
              "type": "string"
            }
          },
          "completionPercentage": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "byRegulation": {
            "type": "array",
            "description": "The same findings grouped by the regulation that raised them, worst\nfirst. Additive and purely derived — it never disagrees with\n`verdict`, it explains it. `verdict` answers \"is this publishable\";\nthis answers \"which regime do I go and fix\", which a single\n`incomplete` cannot.\n\nA regulation appears ONLY if it raised at least one finding, so an\nabsent regulation means \"no finding\", never \"passed\" — listing every\nregulation a category could touch would assert a pass for checks\nthat never ran.\n",
            "items": {
              "$ref": "#/components/schemas/RegimeResult"
            }
          }
        },
        "required": [
          "verdict",
          "category",
          "conditionalCoverage",
          "critical",
          "warnings",
          "checkedRules",
          "completionPercentage",
          "byRegulation"
        ]
      },
      "RegimeResult": {
        "type": "object",
        "description": "One regulation's slice of a compliance result. The verdict follows the\nsame rule as the overall one — any critical finding is `incomplete`,\notherwise warnings downgrade to `compliant_with_warnings` — but is\nscoped to this regulation's own findings.\n",
        "properties": {
          "regulation": {
            "type": "string",
            "description": "Regulation number exactly as the findings carry it, e.g.\n`(EU) 2023/1542`. Findings that carry no regulation are grouped\nunder the literal `unattributed` rather than dropped, so the two\npresentations always account for the same number of findings.\n",
            "example": "(EU) 2023/1542"
          },
          "verdict": {
            "type": "string",
            "enum": [
              "compliant",
              "compliant_with_warnings",
              "incomplete"
            ]
          },
          "critical": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ComplianceFinding"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ComplianceFinding"
            }
          }
        },
        "required": [
          "regulation",
          "verdict",
          "critical",
          "warnings"
        ]
      },
      "ProductIdentifier": {
        "oneOf": [
          {
            "type": "object",
            "description": "GS1 GTIN + serial number (scheme \"gs1\"). Required for battery passports.",
            "required": [
              "scheme",
              "gtin",
              "serialNumber"
            ],
            "properties": {
              "scheme": {
                "type": "string",
                "enum": [
                  "gs1"
                ]
              },
              "gtin": {
                "$ref": "#/components/schemas/GTIN"
              },
              "serialNumber": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
              },
              "digitalLinkUri": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          {
            "type": "object",
            "description": "ISO/IEC 15459 identifier (scheme \"iso15459\"). Also valid for battery passports.",
            "required": [
              "scheme",
              "issuingAgencyCode",
              "primaryId"
            ],
            "properties": {
              "scheme": {
                "type": "string",
                "enum": [
                  "iso15459"
                ]
              },
              "issuingAgencyCode": {
                "type": "string",
                "minLength": 1,
                "maxLength": 3
              },
              "primaryId": {
                "type": "string"
              },
              "serial": {
                "type": "string"
              },
              "raw": {
                "type": "string",
                "description": "The ISO/IEC 15459 unique identifier: issuing agency code + primary id + serial, concatenated. Always derived by the server from the parts; a value sent on create is ignored. Present on every read."
              },
              "uri": {
                "type": "string",
                "format": "uri",
                "description": "The ISO/IEC 18975 Data-Identifier query-form resolver URL generated by the server, following EN 18219 Table B.12: `https://id.tracepass.eu/?.25P=<IAC><primaryId>&.S=<serial>` (`.S` component omitted when there is no serial; parameter values are percent-encoded). Always derived by the server from the identifier parts; present on every read."
              }
            }
          },
          {
            "type": "object",
            "description": "IEC 61406 Identification Link — an https URI (scheme \"iec61406\"). Not valid for battery passports (Battery Regulation Art. 77(3) requires gs1 or iso15459).",
            "required": [
              "scheme",
              "uri"
            ],
            "properties": {
              "scheme": {
                "type": "string",
                "enum": [
                  "iec61406"
                ]
              },
              "uri": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          {
            "type": "object",
            "description": "W3C DID Core identifier (scheme \"did\"). Not valid for battery passports (Battery Regulation Art. 77(3) requires gs1 or iso15459).",
            "required": [
              "scheme",
              "did",
              "method"
            ],
            "properties": {
              "scheme": {
                "type": "string",
                "enum": [
                  "did"
                ]
              },
              "did": {
                "type": "string"
              },
              "method": {
                "type": "string"
              }
            }
          },
          {
            "type": "object",
            "description": "Digital Object Identifier — ISO 26324 (scheme \"doi\"). Stored as bare `10.<registrant>/<suffix>`. Any `https://doi.org/` or `doi:` prefix is stripped on input; the DOI resolves as `https://doi.org/<doi>`. `granularity` is REQUIRED per EN 18219 §5.6.2(b): a DOI product identifier must declare whether it identifies the product model, a production batch, or an individual item. Not valid for battery passports (Battery Regulation Art. 77(3) requires gs1 or iso15459).",
            "required": [
              "scheme",
              "doi",
              "granularity"
            ],
            "properties": {
              "scheme": {
                "type": "string",
                "enum": [
                  "doi"
                ]
              },
              "doi": {
                "type": "string",
                "pattern": "^10\\."
              },
              "granularity": {
                "type": "string",
                "enum": [
                  "model",
                  "batch",
                  "item"
                ],
                "description": "Whether the DOI identifies the product model, a production batch, or an individual item (EN 18219 §5.6.2(b)). Must be supplied on create; returned unchanged on read."
              }
            }
          }
        ],
        "discriminator": {
          "propertyName": "scheme"
        }
      },
      "Passport": {
        "type": "object",
        "properties": {
          "_id": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "companyId": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "productId": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "templateId": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "identifier": {
            "$ref": "#/components/schemas/ProductIdentifier"
          },
          "identifierKey": {
            "type": "string",
            "description": "Canonical dedup key in `<scheme>:<parts>` form, e.g.\n`gs1:00012345678905:SN-001` or `did:did:example:123`.\nGlobally unique (sparse index) — the same identifier cannot exist\nacross different accounts.\n"
          },
          "gs1": {
            "$ref": "#/components/schemas/GS1Block"
          },
          "status": {
            "$ref": "#/components/schemas/PassportStatus"
          },
          "completionPercentage": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "fieldCounts": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "empty": {
                "type": "integer"
              },
              "approved": {
                "type": "integer"
              },
              "pendingReview": {
                "type": "integer"
              },
              "flagged": {
                "type": "integer"
              }
            }
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/PassportField"
            }
          },
          "parties": {
            "$ref": "#/components/schemas/PartyMap"
          },
          "lineage": {
            "$ref": "#/components/schemas/PassportLineage"
          },
          "dynamicDataAsOf": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "`measuredAt` of the newest battery measurement applied to this\npassport; null before the first one. Returned with `format=full`.\n"
          },
          "successors": {
            "type": "array",
            "description": "Forward links to successor passports created from this one\n(same account only). Never changes this passport's fields.\n",
            "items": {
              "$ref": "#/components/schemas/PassportSuccessor"
            }
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "suspendedAt": {
            "type": "string",
            "format": "date-time"
          },
          "suspensionReason": {
            "type": "string"
          },
          "archivedAt": {
            "type": "string",
            "format": "date-time"
          },
          "archiveReason": {
            "$ref": "#/components/schemas/DeletionReason"
          },
          "markedForDeletionAt": {
            "type": "string",
            "format": "date-time"
          },
          "markedForDeletionReason": {
            "$ref": "#/components/schemas/DeletionReason"
          },
          "deletedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Set on the passport the soft DELETE returns; a deleted passport is never returned by a read."
          },
          "deletedReason": {
            "$ref": "#/components/schemas/DeletionReason"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LineageTrigger": {
        "type": "string",
        "description": "The Art. 77(7) operation that produced the successor battery.",
        "enum": [
          "preparation_for_reuse",
          "preparation_for_repurposing",
          "repurposing",
          "remanufacturing"
        ]
      },
      "LineagePredecessorInput": {
        "type": "object",
        "description": "One predecessor passport. Supply at least one of `identifier` or\n`internalPassportId`. An `internalPassportId` must belong to your\naccount (422 `predecessor_not_found` otherwise); its canonical URI\nis filled in for you.\n",
        "required": [
          "trigger"
        ],
        "properties": {
          "identifier": {
            "type": "string",
            "maxLength": 2000,
            "description": "Resolvable identifier of the predecessor passport (http(s) URL, `urn:`, or bare token)."
          },
          "internalPassportId": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "trigger": {
            "$ref": "#/components/schemas/LineageTrigger"
          }
        }
      },
      "LineageInput": {
        "type": "object",
        "description": "Battery passports only. Links a second-life battery's new passport to\nthe original battery passport(s), as Art. 77(7) of Regulation (EU)\n2023/1542 requires. Immutable after create. `batteryStatus` is derived\nfrom the triggers (repurposed, remanufactured, …). Rules (422):\n`lineage_not_applicable` (non-battery), `duplicate_predecessor`,\n`status_trigger_mismatch` (the product's default `batteryStatus`\ncontradicts the triggers), `predecessor_reason_required` (no\npredecessors and no reason), `predecessor_not_found`,\n`invalid_gtin_check_digit`. A second-life `batteryStatus` with no\nlineage block returns `successor_passport_required`.\n",
        "required": [
          "predecessors"
        ],
        "properties": {
          "predecessors": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/LineagePredecessorInput"
            }
          },
          "noPredecessorReason": {
            "type": "string",
            "maxLength": 500,
            "description": "Only with an empty `predecessors` list: why no original passport\nexists, e.g. a battery placed on the market before 18 Feb 2027.\n"
          }
        }
      },
      "PassportLineage": {
        "type": "object",
        "description": "The stored, immutable lineage block of a successor battery passport.",
        "properties": {
          "predecessors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "identifier": {
                  "type": "string"
                },
                "trigger": {
                  "$ref": "#/components/schemas/LineageTrigger"
                },
                "internalPassportId": {
                  "$ref": "#/components/schemas/ResourceId"
                }
              }
            }
          },
          "noPredecessorReason": {
            "type": "string"
          }
        }
      },
      "PassportSuccessor": {
        "type": "object",
        "properties": {
          "passportId": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "identifier": {
            "type": "string",
            "description": "Canonical resolvable URI of the successor passport."
          },
          "addedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MeasurementInput": {
        "type": "object",
        "required": [
          "fieldKey",
          "value",
          "measuredAt"
        ],
        "properties": {
          "fieldKey": {
            "type": "string",
            "description": "An Annex XIII point 4 use-data field key of the battery template, e.g. `stateOfHealth`."
          },
          "value": {
            "description": "Typed as the template field declares (number for most; an array for `negativeEvents`, an object for `temperatureConditionsHistorical`). At most 16 KB serialised; larger values return 400 `invalid_value`."
          },
          "measuredAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the equipment took the measurement. Not in the future."
          },
          "externalId": {
            "type": "string",
            "description": "Your idempotency key for this measurement; a repeat is reported as a duplicate, not stored again."
          },
          "unit": {
            "type": "string"
          }
        }
      },
      "MeasurementsInput": {
        "type": "object",
        "required": [
          "measurements"
        ],
        "properties": {
          "measurements": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "items": {
              "$ref": "#/components/schemas/MeasurementInput"
            }
          }
        }
      },
      "MeasurementsCaptureResult": {
        "type": "object",
        "properties": {
          "accepted": {
            "type": "integer"
          },
          "storedCount": {
            "type": "integer"
          },
          "materializedCount": {
            "type": "integer",
            "description": "How many became the passport's current value (newer than what it held)."
          },
          "duplicates": {
            "type": "integer",
            "description": "How many were skipped because their `externalId` had already been received (in this call or an earlier one)."
          },
          "duplicateExternalIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "measurementIds": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResourceId"
            }
          }
        }
      },
      "PassportMeasurement": {
        "type": "object",
        "properties": {
          "_id": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "passportId": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "fieldKey": {
            "type": "string"
          },
          "value": {},
          "measuredAt": {
            "type": "string",
            "format": "date-time"
          },
          "receivedAt": {
            "type": "string",
            "format": "date-time"
          },
          "externalId": {
            "type": "string"
          },
          "unit": {
            "type": "string"
          },
          "materialized": {
            "type": "boolean"
          },
          "source": {
            "type": "string"
          }
        }
      },
      "LatestMeasurement": {
        "type": "object",
        "properties": {
          "value": {},
          "measuredAt": {
            "type": "string",
            "format": "date-time"
          },
          "unit": {
            "type": "string"
          },
          "externalId": {
            "type": "string"
          }
        }
      },
      "PassportSummary": {
        "type": "object",
        "description": "Trimmed passport shape returned in list responses.",
        "properties": {
          "_id": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "productId": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "identifier": {
            "$ref": "#/components/schemas/ProductIdentifier"
          },
          "identifierKey": {
            "type": "string",
            "description": "Canonical dedup key in `<scheme>:<parts>` form."
          },
          "gs1": {
            "$ref": "#/components/schemas/GS1Block"
          },
          "status": {
            "$ref": "#/components/schemas/PassportStatus"
          },
          "completionPercentage": {
            "type": "integer"
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Product": {
        "type": "object",
        "properties": {
          "_id": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "name": {
            "type": "string"
          },
          "model": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "templateId": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "defaultFieldValues": {
            "type": "object",
            "additionalProperties": {},
            "description": "Seed values for passports created from this product, keyed by\ntemplate field. Each value is checked against the template: a\nnumeric string for a number field is stored as a number, and any\nother value the template rejects answers 400\n`Invalid default field values` with `details` (in a batch, that\nitem's result is an error).\n"
          },
          "imageUrls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri",
              "maxLength": 2048
            },
            "maxItems": 20
          },
          "passportCount": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ]
          },
          "sourceLocale": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreatePassportInput": {
        "type": "object",
        "description": "Exactly one of `identifier` (EN 18219 scheme-tagged) or the legacy\n`gs1` block must be supplied. When `identifier` is provided with\n`scheme: \"gs1\"` the platform also writes the legacy `gs1` block for\nbackward compatibility. Battery passports may only carry `gs1` or\n`iso15459` identifiers (Art. 77(3) Battery Regulation).\n",
        "required": [
          "productId"
        ],
        "properties": {
          "productId": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "identifier": {
            "$ref": "#/components/schemas/ProductIdentifier"
          },
          "gs1": {
            "type": "object",
            "description": "Legacy GS1 identifier block. Prefer `identifier` with\n`scheme: \"gs1\"` on new integrations.\n",
            "required": [
              "gtin",
              "serialNumber"
            ],
            "properties": {
              "gtin": {
                "$ref": "#/components/schemas/GTIN"
              },
              "serialNumber": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
              }
            }
          },
          "parties": {
            "$ref": "#/components/schemas/PartyMap"
          },
          "lineage": {
            "$ref": "#/components/schemas/LineageInput"
          }
        }
      },
      "UpdateFieldInput": {
        "type": "object",
        "required": [
          "value"
        ],
        "properties": {
          "value": {
            "description": "New value, of the type the field's template entry\nexpects. Number fields take a raw number, not a string.\n"
          },
          "source": {
            "type": "string",
            "enum": [
              "manual",
              "ai_suggested",
              "reference_db",
              "supplier"
            ],
            "description": "Tag the origin of the value. Default `manual`.\n`ai_suggested` lands in the review queue; the others write\nwith the caller's own rights (see the operation). A response\nmay still show `ai_approved` or `system`: TracePass sets\nthose, and a request claiming them is refused (400).\n"
          },
          "sourceLocale": {
            "type": "string",
            "description": "ISO 639-1 locale of the value. One of the 24 EU\nlocales. Defaults to the passport's `sourceLocale`.\n"
          }
        }
      },
      "CreateProductInput": {
        "type": "object",
        "required": [
          "name",
          "model",
          "category"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "model": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "Unique within the workspace."
          },
          "category": {
            "type": "string",
            "description": "Seeded category slug — `battery`, `textile`,\n`jewelry`, `electronics`, `furniture`, `detergents`,\n`paints-coatings`, `packaging`, `toys`, `fmcg`, `tyres`,\n`steel`, `construction`.\n"
          },
          "description": {
            "type": "string",
            "maxLength": 2000
          },
          "defaultFieldValues": {
            "type": "object",
            "additionalProperties": {},
            "description": "Seed values for passports created from this product, keyed by\ntemplate field. Each value is checked against the template: a\nnumeric string for a number field is stored as a number, and any\nother value the template rejects answers 400\n`Invalid default field values` with `details` (in a batch, that\nitem's result is an error).\n"
          },
          "imageUrls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri",
              "maxLength": 2048
            },
            "maxItems": 20
          },
          "sourceLocale": {
            "type": "string"
          }
        }
      },
      "UpdateProductInput": {
        "type": "object",
        "description": "Send only the keys you want to change. `imageUrls`\nREPLACES the existing array. `description: null` clears\nthe description.\n",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "model": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "description": {
            "oneOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "defaultFieldValues": {
            "type": "object",
            "additionalProperties": {},
            "description": "Seed values for passports created from this product, keyed by\ntemplate field. Each value is checked against the template: a\nnumeric string for a number field is stored as a number, and any\nother value the template rejects answers 400\n`Invalid default field values` with `details` (in a batch, that\nitem's result is an error).\n"
          },
          "imageUrls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri",
              "maxLength": 2048
            },
            "maxItems": 20
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ]
          },
          "sourceLocale": {
            "type": "string"
          }
        }
      },
      "UploadImageResponse": {
        "type": "object",
        "required": [
          "imageUrl",
          "imageUrls"
        ],
        "properties": {
          "imageUrl": {
            "type": "string",
            "format": "uri"
          },
          "imageUrls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          }
        }
      },
      "PaginationEnvelope": {
        "type": "object",
        "required": [
          "items",
          "total",
          "page",
          "limit",
          "totalPages"
        ],
        "properties": {
          "total": {
            "type": "integer"
          },
          "page": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "totalPages": {
            "type": "integer"
          }
        }
      },
      "PaginatedPassports": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PaginationEnvelope"
          },
          {
            "type": "object",
            "required": [
              "items"
            ],
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PassportSummary"
                }
              }
            }
          }
        ]
      },
      "PaginatedProducts": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PaginationEnvelope"
          },
          {
            "type": "object",
            "required": [
              "items"
            ],
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Product"
                }
              }
            }
          }
        ]
      },
      "BatchResultItem": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "index",
              "status",
              "data"
            ],
            "properties": {
              "index": {
                "type": "integer"
              },
              "status": {
                "type": "string",
                "enum": [
                  "created"
                ]
              },
              "data": {
                "$ref": "#/components/schemas/Passport"
              },
              "warnings": {
                "$ref": "#/components/schemas/LeiWarnings"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "index",
              "status",
              "error"
            ],
            "properties": {
              "index": {
                "type": "integer"
              },
              "status": {
                "type": "string",
                "enum": [
                  "error"
                ]
              },
              "error": {
                "type": "string"
              }
            }
          }
        ]
      },
      "LeiWarnings": {
        "type": "array",
        "description": "Non-blocking findings from the GLEIF check of an LEI the write\nset (see `upsertParty`). Present only when non-empty. The value\nwas saved; the warning says what GLEIF reports about it.\n",
        "items": {
          "type": "object",
          "required": [
            "path",
            "message"
          ],
          "properties": {
            "role": {
              "$ref": "#/components/schemas/PartyRoleEnum"
            },
            "path": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "example": [
                "operatorIdentifier"
              ]
            },
            "message": {
              "type": "string",
              "example": "LEI 353800SZHNPYLZTIX835 has lapsed: its GLEIF record has not been renewed. It still identifies the entity."
            }
          }
        }
      },
      "BatchSummary": {
        "type": "object",
        "required": [
          "created",
          "errors",
          "total"
        ],
        "properties": {
          "created": {
            "type": "integer"
          },
          "errors": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "BatchPassportsResponse": {
        "type": "object",
        "required": [
          "results",
          "summary"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BatchResultItem"
            }
          },
          "summary": {
            "$ref": "#/components/schemas/BatchSummary"
          }
        }
      },
      "BatchCreatePassportsInput": {
        "type": "object",
        "required": [
          "passports"
        ],
        "properties": {
          "passports": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/CreatePassportInput"
            }
          }
        }
      },
      "BatchProductResultItem": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "index",
              "status",
              "data"
            ],
            "properties": {
              "index": {
                "type": "integer"
              },
              "status": {
                "type": "string",
                "enum": [
                  "created"
                ]
              },
              "data": {
                "$ref": "#/components/schemas/Product"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "index",
              "status",
              "error"
            ],
            "properties": {
              "index": {
                "type": "integer"
              },
              "status": {
                "type": "string",
                "enum": [
                  "error"
                ]
              },
              "error": {
                "type": "string"
              }
            }
          }
        ]
      },
      "BatchProductsResponse": {
        "type": "object",
        "required": [
          "results",
          "summary"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BatchProductResultItem"
            }
          },
          "summary": {
            "$ref": "#/components/schemas/BatchSummary"
          }
        }
      },
      "QrJsonEnvelope": {
        "type": "object",
        "required": [
          "passportId",
          "publicUrl",
          "status",
          "qrSvg",
          "qrPngDataUri",
          "colors"
        ],
        "description": "`format=json` response from the QR endpoints. Carries the\nSVG markup, a base64-encoded PNG data URI, and the\nencoded URL + passport status — useful for clients that\nwant both renderings in one round-trip plus the metadata\nthe QR encodes.\n\n`publicUrl` is the URL the QR encodes. For GS1 passports this is\nthe Digital Link URI (`id.tracepass.eu/01/<gtin>/21/<serial>`).\nFor iec61406, did, and doi passports it is the `/x/<scheme>/<value>`\nresolver form (`id.tracepass.eu/x/did/<encodedDid>`). All five\nschemes resolve to the same public viewer.\n",
        "properties": {
          "passportId": {
            "$ref": "#/components/schemas/ResourceId"
          },
          "gtin": {
            "$ref": "#/components/schemas/GTIN"
          },
          "serial": {
            "type": "string"
          },
          "identifier": {
            "type": "string",
            "description": "For non-GS1 passports: the `identifierKey` string, e.g. `did:did:example:123abc`. Present when `gtin`/`serial` are absent."
          },
          "publicUrl": {
            "type": "string",
            "format": "uri"
          },
          "symbology": {
            "type": "string",
            "enum": [
              "qr",
              "datamatrix"
            ],
            "description": "The symbology rendered. `qrSvg` and `qrPngDataUri` keep their names for compatibility and hold the Data Matrix image when this is `datamatrix`."
          },
          "status": {
            "$ref": "#/components/schemas/PassportStatus"
          },
          "qrSvg": {
            "type": "string",
            "description": "Inline `<svg>` markup, ready to embed."
          },
          "qrPngDataUri": {
            "type": "string",
            "description": "data:image/png;base64,..."
          },
          "colors": {
            "type": "object",
            "properties": {
              "foreground": {
                "type": "string"
              },
              "background": {
                "type": "string"
              }
            }
          }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "details": {
            "type": "object"
          }
        }
      },
      "PassportLockedBody": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "description": "Response body for a 409 write-lock refusal.\n- `passport_archived` — the passport is archived and permanently\n  read-only. Suspended and expired passports remain editable.\n- `identifier_locked` — one or more identity fields (`keys`)\n  cannot change because the passport is published and those\n  values are already approved. An empty field may be filled; a\n  `pending_review` value may be corrected. Writing the same\n  value back (including GTIN with or without leading-zero\n  padding, or a trimmed-whitespace match) is not a lock.\n- `concurrent_update` — another write changed the passport\n  between this read and write. Reload and retry.\n",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "passport_archived",
              "identifier_locked",
              "concurrent_update"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation of the lock."
          },
          "keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Identifier field keys that triggered an\n`identifier_locked` error. Omitted for other codes.\n"
          }
        }
      },
      "TenantExport": {
        "type": "object",
        "description": "Bulk JSON-LD export envelope.",
        "properties": {
          "@context": {
            "type": "string",
            "format": "uri"
          },
          "@type": {
            "type": "string",
            "enum": [
              "TenantExport"
            ]
          },
          "exportedAt": {
            "type": "string",
            "format": "date-time"
          },
          "company": {
            "type": "object",
            "properties": {
              "_id": {
                "$ref": "#/components/schemas/ResourceId"
              },
              "name": {
                "type": "string"
              },
              "country": {
                "type": "string"
              }
            }
          },
          "counts": {
            "type": "object",
            "properties": {
              "products": {
                "type": "integer"
              },
              "passports": {
                "type": "integer"
              },
              "templates": {
                "type": "integer"
              }
            }
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Product"
            }
          },
          "passports": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Passport"
            }
          },
          "templates": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "$ref": "#/components/schemas/ResourceId"
                },
                "category": {
                  "type": "string"
                },
                "version": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "EpcisDocument": {
        "type": "object",
        "description": "A GS1 EPCIS 2.0 JSON-LD document. The shape is defined by the\nGS1 EPCIS 2.0 standard, not by TracePass — only the top-level\nenvelope is sketched here.\n",
        "properties": {
          "@context": {
            "description": "EPCIS 2.0 JSON-LD context (string or array)."
          },
          "type": {
            "type": "string",
            "enum": [
              "EPCISDocument",
              "EPCISQueryDocument"
            ]
          },
          "schemaVersion": {
            "type": "string",
            "example": "2.0"
          },
          "creationDate": {
            "type": "string",
            "format": "date-time"
          },
          "epcisBody": {
            "type": "object",
            "description": "Carries `eventList` (an array of EPCIS events) for an\nEPCISDocument, or `queryResults` for an EPCISQueryDocument.\n"
          }
        }
      },
      "EpcisCaptureResult": {
        "type": "object",
        "description": "The 202 response from the EPCIS Capture interface.",
        "properties": {
          "captureJobId": {
            "type": "string",
            "description": "Poll GET /api/v1/epcis/capture/{id} with this id."
          },
          "status": {
            "type": "string",
            "enum": [
              "success",
              "failed"
            ]
          },
          "eventCount": {
            "type": "integer",
            "description": "Events found in the submitted payload."
          },
          "capturedCount": {
            "type": "integer",
            "description": "Events actually stored (after idempotency dedup)."
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "EpcisCaptureJob": {
        "type": "object",
        "description": "An EPCIS capture-job record, returned by the poll endpoint.",
        "properties": {
          "captureJobId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "running",
              "success",
              "failed"
            ]
          },
          "eventCount": {
            "type": "integer"
          },
          "capturedCount": {
            "type": "integer"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "finishedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Validation error or malformed request.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Plan-gate or workspace permission denied.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource doesn't exist or is in a different workspace.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "Conflict": {
        "description": "Conflict — duplicate model on a product, GTIN registered\nto a different tenant, serial collision within a GTIN.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "AmbiguousSerial": {
        "description": "The serial maps to more than one passport in your account\n(serials are unique per GTIN, not per account). The request\nis rejected rather than acting on the wrong passport. Pass\n`?gtin=<gtin>` to disambiguate, or address the passport by\nits id instead. `count` is how many passports the serial\nmatched.\n",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "ambiguous_serial"
                  ]
                },
                "message": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                }
              },
              "required": [
                "error",
                "message",
                "count"
              ]
            }
          }
        }
      },
      "IdempotencyConflict": {
        "description": "Idempotency-Key reused with a different request body. Use\na new key for the new request, or reuse the original body.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "PassportCreateUnprocessable": {
        "description": "Either the Idempotency-Key was reused with a different body, or a\nbattery `lineage` rule failed. Lineage failures carry the rule code\nin `error` (e.g. `duplicate_predecessor`, `predecessor_not_found`,\n`status_trigger_mismatch`) and a human-readable `message`.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "The subscription is past due, unpaid or canceled. Writes that add\nor change content are blocked until billing is resolved. Every\nread keeps working, and published passports stay accessible.\nScope checks, wrong-answer reports, catalogue runs and\ncompliance-profile changes keep working too.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": "Subscription is past due — resolve billing to continue creating new content. Published passports remain accessible."
            }
          }
        }
      },
      "PassportLocked": {
        "description": "The passport is locked and the write was rejected.\nSwitch on `error` to distinguish:\n\n- `passport_archived` — the passport is archived and permanently\n  read-only. Suspended and expired passports remain editable.\n  A passport marked for deletion is read-only too and answers\n  the same code, with its own `message`.\n- `identifier_locked` — one or more identity fields named in\n  `keys` cannot change after the passport is published and the\n  value is approved. Fill an empty field or correct a\n  `pending_review` value freely; writing an equivalent value\n  (GTIN with/without leading-zero padding, trimmed whitespace)\n  is not a lock.\n- `concurrent_update` — another write landed between this read\n  and write. Reload the passport and retry.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PassportLockedBody"
            },
            "examples": {
              "archived": {
                "summary": "Passport archived",
                "value": {
                  "error": "passport_archived",
                  "message": "This passport is archived and can no longer be changed."
                }
              },
              "marked_for_deletion": {
                "summary": "Passport marked for deletion",
                "value": {
                  "error": "passport_archived",
                  "message": "This passport is marked for deletion and can no longer be changed."
                }
              },
              "identifier_locked": {
                "summary": "Identifier field locked",
                "value": {
                  "error": "identifier_locked",
                  "message": "Identifier fields cannot change after the passport is published: gtin.",
                  "keys": [
                    "gtin"
                  ]
                }
              },
              "concurrent_update": {
                "summary": "Concurrent update race",
                "value": {
                  "error": "concurrent_update",
                  "message": "The passport changed while this edit was being saved. Reload it and try again."
                }
              }
            }
          }
        }
      },
      "BySerialPassportConflict": {
        "description": "409 on a by-serial write route. Two mutually exclusive\ncauses — switch on `error`:\n\n- `ambiguous_serial` — the serial matches more than one\n  passport in your account (serials are unique per GTIN,\n  not per account). Add `?gtin=<gtin>` to disambiguate.\n  `count` is how many passports matched.\n- `passport_archived`, `identifier_locked`,\n  `concurrent_update` — see the `PassportLocked` response.\n",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/PassportLockedBody"
                },
                {
                  "type": "object",
                  "required": [
                    "error",
                    "message",
                    "count"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "ambiguous_serial"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    }
                  }
                }
              ]
            }
          }
        }
      },
      "PassportArchived": {
        "description": "The passport is archived and permanently read-only, so the write\nwas rejected. Suspended and expired passports remain editable.\nA passport marked for deletion is read-only too and answers the\nsame code, with its own `message`.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PassportLockedBody"
            },
            "examples": {
              "archived": {
                "summary": "Passport archived",
                "value": {
                  "error": "passport_archived",
                  "message": "This passport is archived and can no longer be changed."
                }
              }
            }
          }
        }
      },
      "BySerialPassportArchivedConflict": {
        "description": "409 on a by-serial write route. Two mutually exclusive\ncauses — switch on `error`:\n\n- `ambiguous_serial` — the serial matches more than one\n  passport in your account (serials are unique per GTIN,\n  not per account). Add `?gtin=<gtin>` to disambiguate.\n  `count` is how many passports matched.\n- `passport_archived` — see the `PassportArchived` response.\n",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/PassportLockedBody"
                },
                {
                  "type": "object",
                  "required": [
                    "error",
                    "message",
                    "count"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "ambiguous_serial"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    }
                  }
                }
              ]
            }
          }
        }
      },
      "DeletionReasonInvalid": {
        "description": "The `reason` is missing or not one of the accepted values.\nSwitch on `error`:\n\n- `reason_required` — a battery passport needs a `reason`\n  (`recycled`, `never_placed` or `duplicate_or_error`).\n- `invalid_reason` — the value is not one of those three.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "examples": {
              "reason_required": {
                "summary": "Battery passport without a reason",
                "value": {
                  "error": "reason_required",
                  "message": "A reason is required for battery passports: send { \"reason\": \"recycled\" | \"never_placed\" | \"duplicate_or_error\" }. A battery passport ends only when the battery has been recycled."
                }
              },
              "invalid_reason": {
                "summary": "Unknown reason",
                "value": {
                  "error": "invalid_reason",
                  "message": "Invalid reason. Must be one of: recycled, never_placed, duplicate_or_error."
                }
              }
            }
          }
        }
      },
      "DeletionNotOnPlan": {
        "description": "Deletion through the API needs a paid plan.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": "deletion_not_on_plan",
              "message": "Deletion is available on every paid plan; archive the passport instead."
            }
          }
        }
      },
      "MarkForDeletionConflict": {
        "description": "The passport cannot be marked for deletion. Switch on `error`:\n\n- `already_marked_for_deletion` — it is already marked; delete\n  it with `DELETE /api/v1/passports/{id}`.\n- `passport_archived` — an archived passport cannot be marked.\n- `has_live_successor` — a newer second-life passport in your\n  account names this one as its predecessor. Delete the newer\n  passport first.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "examples": {
              "already_marked_for_deletion": {
                "summary": "Already marked",
                "value": {
                  "error": "already_marked_for_deletion",
                  "message": "This passport is already marked for deletion."
                }
              },
              "passport_archived": {
                "summary": "Archived",
                "value": {
                  "error": "passport_archived",
                  "message": "This passport is archived. It cannot be marked for deletion."
                }
              },
              "has_live_successor": {
                "summary": "A newer passport links to it",
                "value": {
                  "error": "has_live_successor",
                  "message": "This passport has a live successor. Delete the successor first before marking this passport for deletion."
                }
              }
            }
          }
        }
      },
      "ArchiveMarkedConflict": {
        "description": "The passport is marked for deletion and cannot be archived.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": "passport_already_marked_for_deletion",
              "message": "This passport is already marked for deletion and cannot be archived."
            }
          }
        }
      },
      "RateLimit": {
        "description": "Daily v1 write or passport-read budget exhausted. Resets\nat 00:00 UTC.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      }
    }
  }
}
