{
  "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 access is gated to the\nGrowth plan and above; Free / Basic / Starter workspaces don't\nsee the API key minter in the dashboard.\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, and bulk-import Digital\nProduct 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"
      }
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "paths": {
    "/api/v1/passports": {
      "post": {
        "operationId": "createPassport",
        "tags": [
          "Passports"
        ],
        "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 AND consumes one DPP slot from the\nplan's `maxDpps` quota — when that quota is exhausted and\nthe plan supports overage, returns 402 with an\n`overage_required` body. Retry with\n`confirmOverage: true` to accept the per-passport charge.\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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Passport"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/OverageRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "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/ObjectId"
            }
          },
          {
            "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": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PaginatedPassports"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "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"
          }
        }
      }
    },
    "/api/v1/passports/{id}/fields/{key}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        },
        {
          "$ref": "#/components/parameters/FieldKey"
        },
        {
          "$ref": "#/components/parameters/IdempotencyKey"
        }
      ],
      "patch": {
        "operationId": "updatePassportField",
        "tags": [
          "Passports"
        ],
        "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 tagged\n`via API key <prefix>`. Writes default to `status:\n\"approved\"` — API-key-driven integrations are trusted by\nconvention. Override with `source: \"ai_suggested\"` or\n`source: \"supplier\"` to land in the review queue instead.\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.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "field",
                    "version"
                  ],
                  "properties": {
                    "field": {
                      "$ref": "#/components/schemas/PassportField"
                    },
                    "version": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "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"
        ],
        "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` or\n`legacyOperatorId` must be set in the body — without an\nidentifier the Party doesn't actually identify anyone.\n\nWhen two roles share the same `gln`, the JSON-LD emission\ncollapses them into a single party with multiple roles\nattached.\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"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      },
      "delete": {
        "operationId": "deleteParty",
        "tags": [
          "Passports"
        ],
        "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/{id}/suspend": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PassportId"
        },
        {
          "$ref": "#/components/parameters/IdempotencyKey"
        }
      ],
      "post": {
        "operationId": "suspendPassport",
        "tags": [
          "Passports"
        ],
        "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"
        ],
        "summary": "Archive a passport (irreversible).",
        "description": "**Irreversible.** Public viewer returns 404, the GS1\nDigital Link URL stops resolving, the QR code dies for\ngood. 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",
        "responses": {
          "200": {
            "description": "Archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Passport"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Already archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/batch": {
      "post": {
        "operationId": "batchCreatePassports",
        "tags": [
          "Passports"
        ],
        "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). Same applies to\nDPP overage at the batch level — when the DPP quota would\nbe exceeded the batch returns 402 with `overage_required`,\nand `confirmOverage: true` accepts the overage charge for\nthe whole batch.\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/OverageRequired"
          },
          "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": "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 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"
        }
      ],
      "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/by-serial/{serial}/qr": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Serial"
        }
      ],
      "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": "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/passports/by-serial/{serial}/fields/{key}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Serial"
        },
        {
          "$ref": "#/components/parameters/FieldKey"
        },
        {
          "$ref": "#/components/parameters/IdempotencyKey"
        }
      ],
      "patch": {
        "operationId": "updatePassportFieldBySerial",
        "tags": [
          "Passports"
        ],
        "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}`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateFieldInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Field updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "field",
                    "version"
                  ],
                  "properties": {
                    "field": {
                      "$ref": "#/components/schemas/PassportField"
                    },
                    "version": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "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/IdempotencyKey"
        }
      ],
      "post": {
        "operationId": "suspendPassportBySerial",
        "tags": [
          "Passports"
        ],
        "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"
          },
          "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/IdempotencyKey"
        }
      ],
      "post": {
        "operationId": "archivePassportBySerial",
        "tags": [
          "Passports"
        ],
        "summary": "Archive a passport (by serial, irreversible).",
        "description": "Same shape as POST `/api/v1/passports/{id}/archive`. **Irreversible.**",
        "responses": {
          "200": {
            "description": "Archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Passport"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Already archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/api/v1/products": {
      "post": {
        "operationId": "createProduct",
        "tags": [
          "Products"
        ],
        "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": "batteries",
                "defaultFieldValues": {
                  "battery_chemistry": "lithium-ion",
                  "nominal_voltage": 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": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PaginatedProducts"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "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"
        ],
        "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"
          }
        }
      }
    },
    "/api/v1/products/batch": {
      "post": {
        "operationId": "batchCreateProducts",
        "tags": [
          "Products"
        ],
        "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"
        ],
        "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 resulting public R2 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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "Product already has 20 images (max).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/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"
        ],
        "summary": "Remove a product image by index.",
        "description": "Remove a single image from `imageUrls` by its zero-based\nindex. The underlying R2 object is not deleted; the URL\njust stops being referenced.\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) + paying-customer gate — same trust level as\ndeleting the company. The pragmatic flow is: open Settings\n→ Data export in the dashboard and click \"Export tenant\".\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"
          },
          "402": {
            "description": "Active paid subscription required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Admin role required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "opaque",
        "description": "API-key Bearer token. Mint at Developer → API keys in the\ndashboard (Growth+ plans only). Keys carry the `tp_`\nprefix followed by an opaque random suffix.\n"
      },
      "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": 255
        },
        "description": "UUID v4 (or any opaque string ≤ 255 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.\n"
      },
      "PassportId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/ObjectId"
        }
      },
      "ProductId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/ObjectId"
        }
      },
      "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 (snake_case) as defined on the passport's\ncategory template — e.g. `nominal_voltage`,\n`recycled_content_pct`, `country_of_origin`.\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"
      },
      "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": {
      "ObjectId": {
        "type": "string",
        "pattern": "^[a-f0-9]{24}$",
        "example": "6650a1b2c3d4e5f6a7b8c9d0",
        "description": "MongoDB ObjectId hex string (24 chars)."
      },
      "GTIN": {
        "type": "string",
        "pattern": "^[0-9]{14}$",
        "example": "04012345000015",
        "description": "GS1 GTIN-14 with valid check digit."
      },
      "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` or\n`legacyOperatorId` must be set — without an identifier the\nParty doesn't actually identify anyone.\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
          },
          "status": {
            "type": "string",
            "readOnly": true,
            "description": "Server-side status (active, etc.). Returned but ignored on write."
          }
        }
      },
      "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",
        "enum": [
          "draft",
          "in_review",
          "approved",
          "published",
          "suspended",
          "expired",
          "archived"
        ]
      },
      "PassportField": {
        "type": "object",
        "properties": {
          "value": {},
          "source": {
            "type": "string",
            "enum": [
              "manual",
              "ai_suggested",
              "ai_approved",
              "reference_db",
              "supplier",
              "system"
            ]
          },
          "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"
                }
              }
            }
          },
          "lastUpdatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastUpdatedBy": {
            "type": "string"
          }
        }
      },
      "Passport": {
        "type": "object",
        "properties": {
          "_id": {
            "$ref": "#/components/schemas/ObjectId"
          },
          "companyId": {
            "$ref": "#/components/schemas/ObjectId"
          },
          "productId": {
            "$ref": "#/components/schemas/ObjectId"
          },
          "templateId": {
            "$ref": "#/components/schemas/ObjectId"
          },
          "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"
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "suspendedAt": {
            "type": "string",
            "format": "date-time"
          },
          "suspensionReason": {
            "type": "string"
          },
          "archivedAt": {
            "type": "string",
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PassportSummary": {
        "type": "object",
        "description": "Trimmed passport shape returned in list responses.",
        "properties": {
          "_id": {
            "$ref": "#/components/schemas/ObjectId"
          },
          "productId": {
            "$ref": "#/components/schemas/ObjectId"
          },
          "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/ObjectId"
          },
          "name": {
            "type": "string"
          },
          "model": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "templateId": {
            "$ref": "#/components/schemas/ObjectId"
          },
          "defaultFieldValues": {
            "type": "object",
            "additionalProperties": {}
          },
          "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",
        "required": [
          "productId",
          "gs1"
        ],
        "properties": {
          "productId": {
            "$ref": "#/components/schemas/ObjectId"
          },
          "gs1": {
            "type": "object",
            "required": [
              "gtin",
              "serialNumber"
            ],
            "properties": {
              "gtin": {
                "$ref": "#/components/schemas/GTIN"
              },
              "serialNumber": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
              }
            }
          },
          "parties": {
            "$ref": "#/components/schemas/PartyMap"
          },
          "confirmOverage": {
            "type": "boolean",
            "description": "Accept the per-passport overage charge when the plan's\n`maxDpps` quota is already exhausted. Required only\nafter a 402 response on a previous attempt.\n"
          }
        }
      },
      "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",
              "ai_approved",
              "reference_db",
              "supplier",
              "system"
            ],
            "description": "Tag the origin of the value. Default `manual`.\n`ai_suggested` and `supplier` land in the review queue;\neverything else writes as approved.\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 — `batteries`, `textiles`,\n`jewelry`, `electronics`, `furniture`, `chemicals`,\n`packaging`, `toys`, `fmcg`, `tyres`, `iron-steel`,\n`construction`.\n"
          },
          "description": {
            "type": "string",
            "maxLength": 2000
          },
          "defaultFieldValues": {
            "type": "object",
            "additionalProperties": {}
          },
          "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": {}
          },
          "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"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "index",
              "status",
              "error"
            ],
            "properties": {
              "index": {
                "type": "integer"
              },
              "status": {
                "type": "string",
                "enum": [
                  "error"
                ]
              },
              "error": {
                "type": "string"
              }
            }
          }
        ]
      },
      "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"
        ],
        "description": "Top-level `confirmOverage` is the BATCH-LEVEL flag — set\nit to accept the per-passport overage charge if the entire\nbatch would exceed the workspace's `maxDpps` quota. Wins\nover any per-item `confirmOverage` (the single-create\nflag, which is also accepted on each item but only relevant\nif a single item would exhaust the budget on its own).\n",
        "properties": {
          "passports": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/CreatePassportInput"
            }
          },
          "confirmOverage": {
            "type": "boolean"
          }
        }
      },
      "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",
          "gtin",
          "serial",
          "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",
        "properties": {
          "passportId": {
            "$ref": "#/components/schemas/ObjectId"
          },
          "gtin": {
            "$ref": "#/components/schemas/GTIN"
          },
          "serial": {
            "type": "string"
          },
          "publicUrl": {
            "type": "string",
            "format": "uri"
          },
          "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"
          },
          "details": {
            "type": "object"
          }
        }
      },
      "OverageRequiredBody": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "overage_required"
            ]
          },
          "planLimit": {
            "type": "integer"
          },
          "currentUsage": {
            "type": "integer"
          },
          "requested": {
            "type": "integer"
          },
          "extraPriceCents": {
            "type": "integer"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "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/ObjectId"
              },
              "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/ObjectId"
                },
                "category": {
                  "type": "string"
                },
                "version": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "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"
            }
          }
        }
      },
      "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"
            }
          }
        }
      },
      "OverageRequired": {
        "description": "DPP quota exhausted; retry with `confirmOverage: true` to\naccept the per-passport charge.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OverageRequiredBody"
            }
          }
        }
      },
      "RateLimit": {
        "description": "Daily v1 write or passport-read budget exhausted. Resets\nat 00:00 UTC.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      }
    }
  }
}
