{
  "openapi": "3.1.0",
  "info": {
    "title": "Recall & Safety Alert API",
    "version": "1.0.0",
    "description": "Normalized product recall and safety alert records from official government sources,\nwith per-record amendment history.\n\nEvery response carries `data_as_of` — the oldest upstream freshness stamp among the\nsources backing it — and the attribution strings the source licences require you to\ndisplay.\n\n## Rate limits and quotas\n\n| Tier | Requests/month | Burst |\n|---|---:|---:|\n| free | 500 | 10/min |\n| starter | 10,000 | 60/min |\n| growth | 100,000 | 300/min |\n| scale | unmetered | 1000/min |\n\nBurst limits are enforced per Cloudflare location and are intentionally permissive;\nthe monthly quota is the figure that governs.\n\n## Caching\n\nResponses carry a weak `ETag`. Send it back as `If-None-Match` and an unchanged\ncorpus returns `304` with no body. The data is rebuilt at most once a day, so this\nis the difference between a cheap integration and an expensive one."
  },
  "servers": [
    {
      "url": "/",
      "description": "Same origin as this document."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key issued to you, sent as `Authorization: Bearer <key>`."
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem detail.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "Stable machine-readable error identifier. Branch on this, not on `title`.",
            "enum": [
              "/problems/unauthorized",
              "/problems/forbidden",
              "/problems/quota-exceeded",
              "/problems/rate-limited",
              "/problems/not-found",
              "/problems/invalid-parameter",
              "/problems/invalid-cursor",
              "/problems/internal"
            ]
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "instance": {
            "type": "string"
          },
          "parameter": {
            "type": "string",
            "description": "The offending parameter, when applicable."
          }
        }
      },
      "Meta": {
        "type": "object",
        "required": [
          "count",
          "data_as_of"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "description": "Rows in this response."
          },
          "limit": {
            "type": "integer"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` for the next page. Null when the walk is complete."
          },
          "total": {
            "type": "integer",
            "description": "Present only when `include_total=true` was requested."
          },
          "data_as_of": {
            "type": [
              "string",
              "null"
            ],
            "description": "Oldest upstream freshness stamp across the sources backing this response — the conservative claim, not the most flattering one."
          },
          "attribution": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Attribution strings the source licences require you to display."
          }
        }
      },
      "Identifier": {
        "type": "object",
        "required": [
          "kind",
          "value",
          "normalized",
          "confidence"
        ],
        "properties": {
          "kind": {
            "type": "string"
          },
          "value": {
            "type": "string",
            "description": "Exactly as the agency published it."
          },
          "normalized": {
            "type": "string",
            "description": "Canonical form used for matching."
          },
          "confidence": {
            "type": "string",
            "enum": [
              "exact",
              "extracted",
              "inferred"
            ],
            "description": "`exact` came from a dedicated field; `extracted` was mined from prose and passed a checksum; `inferred` matched shape only."
          }
        }
      },
      "Recall": {
        "type": "object",
        "required": [
          "id",
          "source",
          "revision",
          "data_as_of"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "source": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "record_id": {
                "type": "string"
              },
              "agency": {
                "type": "string"
              },
              "jurisdiction": {
                "type": "string"
              }
            }
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "summary": {
            "type": [
              "string",
              "null"
            ]
          },
          "hazard": {
            "type": "object",
            "properties": {
              "category": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "text": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The agency's own wording, verbatim."
              }
            }
          },
          "product": {
            "type": "object",
            "properties": {
              "category": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "brands": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "models": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "severity": {
            "type": [
              "string",
              "null"
            ]
          },
          "source_classification": {
            "type": [
              "string",
              "null"
            ],
            "description": "The agency's own class label, unmapped."
          },
          "remedy": {
            "type": [
              "string",
              "null"
            ]
          },
          "recall_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "published_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "url": {
            "type": [
              "string",
              "null"
            ]
          },
          "revision": {
            "type": "integer",
            "description": "Times this record has been observed to change. 1 means never amended."
          },
          "identifiers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Identifier"
            }
          },
          "data_as_of": {
            "type": "string"
          },
          "first_seen_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_seen_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Revision": {
        "type": "object",
        "required": [
          "revision",
          "observed_at",
          "changed_fields"
        ],
        "properties": {
          "revision": {
            "type": "integer"
          },
          "observed_at": {
            "type": "string",
            "format": "date-time",
            "description": "When we observed the change — not when the agency made it."
          },
          "changed_fields": {
            "type": "object",
            "description": "Field name to `{from, to}`.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "from": {},
                "to": {}
              }
            }
          },
          "data_as_of": {
            "type": "string"
          }
        }
      },
      "Source": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "agency": {
            "type": "string"
          },
          "jurisdiction": {
            "type": "string"
          },
          "licence": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "attribution": {
                "type": "string"
              }
            }
          },
          "homepage": {
            "type": "string"
          },
          "cadence": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "last_success_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "data_as_of": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "CheckReceipt": {
        "type": "object",
        "description": "Immutable evidence that a catalogue was checked. Your catalogue itself is not stored — `submitted_digest` lets you prove independently which list produced this receipt.",
        "required": [
          "check_id",
          "performed_at",
          "submitted_count",
          "submitted_digest",
          "match_count"
        ],
        "properties": {
          "check_id": {
            "type": "string"
          },
          "label": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own reference, echoed back."
          },
          "performed_at": {
            "type": "string",
            "format": "date-time"
          },
          "submitted_count": {
            "type": "integer",
            "description": "Identifiers actually checked. Excludes anything in `rejected`."
          },
          "submitted_digest": {
            "type": "string",
            "description": "SHA-256 over the sorted, newline-joined, normalized `kind:value` pairs. Reproduce it from your own catalogue to verify this receipt corresponds to it."
          },
          "rejected_count": {
            "type": "integer"
          },
          "rejected": {
            "type": "array",
            "description": "Identifiers that could not be normalized and were therefore NOT checked. Never silently dropped: a clean receipt over a partly-parsed catalogue is worse than no receipt.",
            "items": {
              "type": "object",
              "properties": {
                "kind": {
                  "type": "string"
                },
                "value": {
                  "type": "string"
                },
                "reason": {
                  "type": "string"
                }
              }
            }
          },
          "match_count": {
            "type": "integer"
          },
          "result_digest": {
            "type": "string",
            "description": "SHA-256 over the sorted matched recall ids. Equal findings, equal digest."
          },
          "corpus_as_of": {
            "type": [
              "string",
              "null"
            ],
            "description": "Freshness of the corpus when the check ran. This is what makes a nil result meaningful — absence of evidence versus evidence of absence."
          },
          "corpus_size": {
            "type": "integer"
          },
          "matches": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "recall_id": {
                  "type": "string"
                },
                "matched_on": {
                  "type": "object",
                  "properties": {
                    "kind": {
                      "type": "string"
                    },
                    "normalized": {
                      "type": "string"
                    }
                  }
                },
                "revision_at_check": {
                  "type": "integer",
                  "description": "The recall revision current when the check ran. Later amendments do not rewrite this receipt."
                },
                "title": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "jurisdiction": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "recall_date": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "severity": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "url": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          }
        }
      }
    }
  },
  "paths": {
    "/v1/recalls": {
      "get": {
        "operationId": "listRecalls",
        "summary": "List recalls",
        "description": "Recalls newest first. Records with no publication date sort last and remain reachable through the same cursor walk.",
        "parameters": [
          {
            "name": "jurisdiction",
            "in": "query",
            "required": false,
            "description": "ISO-ish jurisdiction code as published, e.g. `EU`, `FR`, `US`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "description": "Restrict to one source feed. See `/v1/sources`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "hazard_category",
            "in": "query",
            "required": false,
            "description": "Canonical hazard taxonomy value.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "severity",
            "in": "query",
            "required": false,
            "description": "Normalized severity.",
            "schema": {
              "type": "string",
              "enum": [
                "serious",
                "high",
                "medium",
                "low",
                "unknown"
              ]
            }
          },
          {
            "name": "remedy",
            "in": "query",
            "required": false,
            "description": "Normalized remedy offered to consumers.",
            "schema": {
              "type": "string",
              "enum": [
                "repair",
                "replace",
                "refund",
                "dispose",
                "stop_use",
                "other",
                "unknown"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Earliest `recall_date`, inclusive (YYYY-MM-DD).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Latest `recall_date`, inclusive (YYYY-MM-DD).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page. Default 50, maximum 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor from `meta.next_cursor` of the previous page. Do not construct by hand; its encoding is not part of the contract.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include_total",
            "in": "query",
            "required": false,
            "description": "Include `meta.total`. Off by default because an exact count scans every matching row, which costs materially more than the page itself.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "ETag": {
                "description": "Weak validator. Send back as `If-None-Match` to get a 304.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Recall"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified; your cached copy is current."
          },
          "400": {
            "description": "Invalid parameter or cursor.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "402": {
            "description": "Monthly quota exhausted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Key revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such resource.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/recalls/{id}": {
      "get": {
        "operationId": "getRecall",
        "summary": "Fetch one recall",
        "description": "Returns a single recall including every extracted product identifier.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Stable recall id, as returned in `data[].id`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "ETag": {
                "description": "Weak validator. Send back as `If-None-Match` to get a 304.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Recall"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified; your cached copy is current."
          },
          "400": {
            "description": "Invalid parameter or cursor.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "402": {
            "description": "Monthly quota exhausted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Key revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such resource.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/recalls/{id}/revisions": {
      "get": {
        "operationId": "listRecallRevisions",
        "summary": "Amendment history for a recall",
        "description": "Every observed change to this record, oldest first, with the field-level delta and the date we observed it. Agencies widen recalls after publication — adding lot codes, model numbers, date ranges — and rarely announce it. This endpoint is the evidence that a given widening happened and when it became visible.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Stable recall id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page. Default 50, maximum 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "ETag": {
                "description": "Weak validator. Send back as `If-None-Match` to get a 304.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Revision"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified; your cached copy is current."
          },
          "400": {
            "description": "Invalid parameter or cursor.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "402": {
            "description": "Monthly quota exhausted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Key revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such resource.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/identifiers/{kind}/{value}": {
      "get": {
        "operationId": "lookupByIdentifier",
        "summary": "Find recalls by product identifier",
        "description": "The catalogue-check endpoint. The supplied value is normalized exactly as ingestion normalizes published identifiers — GTINs are check-digit validated and zero-padded to 14, VINs upper-cased — so a barcode matches regardless of the form it is presented in.",
        "parameters": [
          {
            "name": "kind",
            "in": "path",
            "required": true,
            "description": "Identifier type.",
            "schema": {
              "type": "string",
              "enum": [
                "gtin",
                "upc",
                "ean",
                "vin",
                "ndc",
                "udi",
                "model",
                "lot",
                "batch"
              ]
            }
          },
          {
            "name": "value",
            "in": "path",
            "required": true,
            "description": "The identifier as printed on the product or in your catalogue.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page. Default 50, maximum 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "ETag": {
                "description": "Weak validator. Send back as `If-None-Match` to get a 304.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Recall"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "match": {
                                "type": "object",
                                "description": "Which stored identifier matched, and how confident we are in it.",
                                "properties": {
                                  "kind": {
                                    "type": "string"
                                  },
                                  "normalized": {
                                    "type": "string"
                                  },
                                  "confidence": {
                                    "type": "string"
                                  }
                                }
                              }
                            }
                          }
                        ]
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified; your cached copy is current."
          },
          "400": {
            "description": "Invalid parameter or cursor.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "402": {
            "description": "Monthly quota exhausted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Key revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such resource.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sources": {
      "get": {
        "operationId": "listSources",
        "summary": "Data sources, licences and attribution",
        "description": "Every upstream feed with its licence, the attribution string its licence requires, and its observed update cadence.",
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "ETag": {
                "description": "Weak validator. Send back as `If-None-Match` to get a 304.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Source"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified; your cached copy is current."
          },
          "400": {
            "description": "Invalid parameter or cursor.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "402": {
            "description": "Monthly quota exhausted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Key revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such resource.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/checks": {
      "post": {
        "operationId": "runCheck",
        "summary": "Check a catalogue and record the evidence",
        "description": "Submits product identifiers, matches them against the corpus, and records an immutable receipt that the check happened. The receipt — not the match list — is the artefact: it states what was checked, when, against a corpus current to which date, and what was found.\n\nYour catalogue is **not stored**. The receipt carries `submitted_digest`, a SHA-256 over the sorted, newline-joined, normalized `kind:value` pairs. Re-hash your own list the same way to prove a receipt corresponds to it — verification that neither side can forge alone, and no copy of your product data sitting in our database.\n\nIdentifiers that cannot be normalized are returned in `rejected` and are NOT counted as checked. A clean receipt over a partially-parsed catalogue would be worse than useless.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "identifiers"
                ],
                "properties": {
                  "label": {
                    "type": "string",
                    "description": "Your own reference for this check. Echoed back, never interpreted."
                  },
                  "identifiers": {
                    "type": "array",
                    "maxItems": 1000,
                    "description": "Up to 1000 identifiers.",
                    "items": {
                      "type": "object",
                      "required": [
                        "kind",
                        "value"
                      ],
                      "properties": {
                        "kind": {
                          "type": "string",
                          "enum": [
                            "gtin",
                            "upc",
                            "ean",
                            "vin",
                            "ndc",
                            "udi",
                            "model",
                            "lot",
                            "batch"
                          ]
                        },
                        "value": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "headers": {
              "ETag": {
                "description": "Weak validator. Send back as `If-None-Match` to get a 304.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CheckReceipt"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified; your cached copy is current."
          },
          "400": {
            "description": "Invalid parameter or cursor.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "402": {
            "description": "Monthly quota exhausted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Key revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such resource.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listChecks",
        "summary": "Your check history",
        "description": "Every check recorded against your API key, newest first. This is the monitoring record itself: a continuous, timestamped history of having looked, which is what demonstrates diligence over a period rather than on one day.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page. Default 50, maximum 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "ETag": {
                "description": "Weak validator. Send back as `If-None-Match` to get a 304.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CheckReceipt"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified; your cached copy is current."
          },
          "400": {
            "description": "Invalid parameter or cursor.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "402": {
            "description": "Monthly quota exhausted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Key revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such resource.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/checks/{id}": {
      "get": {
        "operationId": "getCheck",
        "summary": "Retrieve one check receipt",
        "description": "The full receipt including the matches frozen as they stood at check time. If a matched recall has since been amended, this still reports the revision that was current when the check ran — the receipt does not change retroactively.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Check id from `POST /v1/checks`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "ETag": {
                "description": "Weak validator. Send back as `If-None-Match` to get a 304.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CheckReceipt"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified; your cached copy is current."
          },
          "400": {
            "description": "Invalid parameter or cursor.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "402": {
            "description": "Monthly quota exhausted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Key revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such resource.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Ingestion freshness",
        "description": "Per-source last successful ingestion and upstream freshness stamp. Unauthenticated: it is the monitoring target and a public trust signal.",
        "security": [],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "ETag": {
                "description": "Weak validator. Send back as `If-None-Match` to get a 304.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "degraded",
                        "unknown"
                      ]
                    },
                    "sources": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "degraded": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified; your cached copy is current."
          },
          "400": {
            "description": "Invalid parameter or cursor.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such resource.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  }
}