openapi: 3.1.0

info:
  title: IDBird Redaction API
  version: "1.0"
  summary: Stateless ID-document redaction for EU identity documents.
  description: |
    The IDBird Redaction API accepts a scan or photo of an identity document
    (passport or ID card) and returns a copy with the machine-readable zone
    (MRZ), personal identification numbers, the portrait photo and the
    signature destroyed at the pixel level. The document's own number stays
    readable by default and is redacted on request. The caller decides what
    to keep — the service itself stores nothing.

    ## Guarantees

    - **Stateless.** Uploaded documents and produced images are never stored.
      Only request metadata is retained for audit purposes: which *types* of
      information were detected and how often — never the detected values, and
      never the images.
    - **EU processing.** Documents are processed exclusively within the
      European Union.
    - **Destructive redaction.** Removed content is painted out of the image
      pixels. There is no overlay or layer that could be removed afterwards.
    - **PNG output.** Redacted images are always returned as PNG,
      base64-encoded inside a JSON envelope.

    ## The coverage contract — read this first

    An HTTP `200` means the document was **processed**, not that everything
    was found. What was and was not detected is reported explicitly:

    - `findings` lists the detected information types with match counts.
    - `warnings` names every gap in expected coverage. It is **always
      present** — an empty array means full expected coverage.

    **Callers must branch on `warnings` before archiving output.** In
    particular, `nothing_detected` means the returned image is unchanged, and
    `mrz_not_found` / `country_detector_no_match` mean specific content may
    still be visible. Treat these as review queues, not as success.

    ## Country support

    The optional `country_hint` (ISO 3166-1 alpha-2 of the issuing country)
    selects the most precise detection profile:

    | Support | Countries |
    |---|---|
    | Country-specific detection | `NL`, `FR`, `ES`, `AT`, `BE`, `HR`, `CZ`, `DK`, `FI`, `DE`, `IE`, `IT`, `PL`, `PT`, `SE` — most have personal-number detectors; `BE` and `DE` have document-number detectors only |
    | MRZ-level only (responses carry `country_detectors_unavailable`) | `BG`, `CY`, `EE`, `GR` (alias `EL`), `HU`, `LV`, `LT`, `LU`, `MT`, `RO`, `SK`, `SI` |
    | Not supported (rejected with `country_not_supported`) | all other codes |

    Without a hint the request runs in **auto mode**: all fully supported
    countries' detectors are applied together and the response carries the
    `no_country_hint` warning. Provide the hint when you know the issuing
    country — it is more precise.

    ## Input requirements

    - Formats: JPEG, PNG, BMP, or PDF (up to 5 pages, not password-protected).
      The type is detected from file content; the filename and declared
      content type are ignored.
    - Maximum upload size: 10 MiB.
    - Rotated uploads (sideways or upside-down scans) are corrected
      automatically when the orientation can be determined reliably.
    - Detection quality follows image quality: sharp, well-lit, uncropped
      documents detect best. Very large photos are downscaled before
      processing; extremely noisy or low-resolution input surfaces as
      `mrz_not_found` or `nothing_detected` warnings.

    ## Photo redaction modes

    The portrait photo is redacted **by default**. The optional
    `redact_photo` field changes that per request:

    | Mode | Behaviour |
    |---|---|
    | `true` (default) | The portrait is destroyed like everything else. |
    | `false` | The single returned copy keeps the portrait visible; the response carries the `photo_not_redacted` warning. |
    | `both` | Two copies per page: `image` stays the fully-redacted artifact, and `image_photo_visible` carries a sibling with the portrait intact. |

    The MRZ, personal identification numbers and the signature are destroyed
    in **every** returned copy regardless of mode.

    ## Document number redaction

    The document's own number (passport / ID-card number) printed in the
    visual zone is **kept by default** — it is typically needed by the
    receiving party. The optional `redact_documentnumber` field changes that
    per request:

    | Mode | Behaviour |
    |---|---|
    | `false` (default) | The visual-zone document number stays readable; when one was detected the response carries the `document_number_not_redacted` warning. |
    | `true` | The document number is destroyed like everything else. |

    Personal identification numbers (BSN, NIR, DNI, PESEL, CPR, …) are always
    destroyed, and the MRZ — which also encodes the document number — is
    always fully destroyed regardless of this flag. Note: for `BE` and `DE`
    the only country-specific detectors are document-number detectors, so
    default requests there protect the MRZ, portrait and signature.

    ## Multi-page PDFs

    Each page is processed and billed as one document page. Single-page input
    returns the flat `image` field; multi-page PDFs return a `pages` array
    with one redacted image per page and aggregate `warnings`/`findings` at
    the top level. PDFs over the page limit are rejected with
    `too_many_pages`.

    ## Authentication and limits

    Every request requires a bearer API key, provisioned by the IDBird team.
    Keys can be revoked at any time, effective immediately. Requests are rate
    limited (by default 30 per minute per key; the service may additionally
    shed load under global pressure) — a `429` response carries a
    `Retry-After` header. Use exponential backoff.

    ## Daily allowance (billing)

    Each key carries a **daily allowance of API calls**, counted per
    **UTC day**. One fully-processed request — to either endpoint — is one
    billable call; a multi-page PDF still counts as a single call. When
    the allowance is exhausted, requests are rejected with `429` and code
    `daily_quota_exceeded`, whose `Retry-After` header points at the next
    UTC midnight. Rejected and failed requests are never billed. New keys
    start with a modest allowance; contact the IDBird team to have it
    raised.

    ## Errors

    Every error uses one envelope: `{"error": {"code": "...", "message":
    "..."}}` with a stable machine-readable `code` and a static human-readable
    message. On any backend failure (`5xx`) no image is ever returned.

servers:
  - url: https://api.idbird.eu
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Redaction
    description: Redact or inspect identity documents.

paths:
  /v1/redact:
    post:
      tags: [Redaction]
      operationId: redactDocument
      summary: Redact an identity document
      description: |
        Destroys the MRZ, personal identification numbers, portrait photo and
        signature in the uploaded document and returns the redacted copy as
        base64-encoded PNG. The document's own number is kept unless
        `redact_documentnumber=true`. Check `warnings` before archiving the
        result.
      requestBody:
        $ref: "#/components/requestBodies/Document"
      responses:
        "200":
          description: |
            Document processed. Single-page input returns `image`; multi-page
            PDFs return `pages`. `warnings` and `findings` are always present.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/RedactedDocument"
                  - $ref: "#/components/schemas/RedactedPages"
              examples:
                singlePage:
                  summary: Single-page document, full coverage
                  value:
                    image: "iVBORw0KGgoAAAANSUhEUgAA…"
                    warnings: []
                    findings:
                      MRZ_TD3_LINE_1: 1
                      MRZ_TD3_LINE_2: 1
                      NETHERLANDS_PASSPORT: 1
                      OBJECT_TYPE/PERSON/FACE: 2
                      OBJECT_TYPE/PERSON/SIGNATURE: 1
                withWarnings:
                  summary: ID-card front — no MRZ present on this side
                  value:
                    image: "iVBORw0KGgoAAAANSUhEUgAA…"
                    warnings: [no_country_hint, mrz_not_found]
                    findings:
                      NETHERLANDS_PASSPORT: 1
                      OBJECT_TYPE/PERSON/FACE: 2
                      OBJECT_TYPE/PERSON/SIGNATURE: 1
                multiPage:
                  summary: Two-page PDF
                  value:
                    pages:
                      - page: 1
                        image: "iVBORw0KGgoAAAANSUhEUgAA…"
                        findings:
                          OBJECT_TYPE/PERSON/FACE: 1
                      - page: 2
                        image: "iVBORw0KGgoAAAANSUhEUgAA…"
                        findings:
                          MRZ_TD3_LINE_2: 1
                          NETHERLANDS_BSN_NUMBER: 1
                    warnings: []
                    findings:
                      MRZ_TD3_LINE_2: 1
                      NETHERLANDS_BSN_NUMBER: 1
                      OBJECT_TYPE/PERSON/FACE: 1
        "401": { $ref: "#/components/responses/Unauthorized" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "422": { $ref: "#/components/responses/UnprocessableDocument" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
        "502": { $ref: "#/components/responses/BackendUnavailable" }

  /v1/inspect:
    post:
      tags: [Redaction]
      operationId: inspectDocument
      summary: Inspect an identity document without receiving an image
      description: |
        Runs the identical detection as `/v1/redact` and returns only the
        detected information types with match counts — never the detected
        values, and no image. Useful as a dry run: empty `findings` tells you
        a redaction of this document would find nothing. Billed and rate
        limited like a redaction.
      requestBody:
        $ref: "#/components/requestBodies/Document"
      responses:
        "200":
          description: |
            Detection report. Multi-page PDFs additionally carry per-page
            findings in `pages`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InspectionReport"
              examples:
                report:
                  value:
                    findings:
                      MRZ_TD3_LINE_2: 1
                      NETHERLANDS_BSN_NUMBER: 1
                    warnings: [no_country_hint]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "422": { $ref: "#/components/responses/UnprocessableDocument" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
        "502": { $ref: "#/components/responses/BackendUnavailable" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key provisioned by the IDBird team. Revocation is effective immediately.

  requestBodies:
    Document:
      required: true
      content:
        multipart/form-data:
          schema:
            type: object
            required: [document]
            properties:
              document:
                type: string
                format: binary
                description: |
                  The document: JPEG, PNG, BMP, or PDF up to 5 pages, max
                  10 MiB. The type is detected from content — the filename and
                  declared content type are ignored.
              country_hint:
                type: string
                pattern: "^[A-Za-z]{2}$"
                example: NL
                description: |
                  Optional ISO 3166-1 alpha-2 code of the issuing country.
                  Omit to run in auto mode (all fully supported countries'
                  detectors, plus the `no_country_hint` warning). `EL` is
                  accepted as an alias for `GR`.
              redact_photo:
                type: string
                enum: ["true", "false", "both"]
                default: "true"
                description: |
                  Portrait handling — see "Photo redaction modes". `true`
                  (default) destroys the portrait; `false` returns one copy
                  with it visible (warned); `both` returns the fully
                  redacted image plus an `image_photo_visible` sibling. All
                  other redactions apply in every copy.
              redact_documentnumber:
                type: string
                enum: ["false", "true"]
                default: "false"
                description: |
                  Visual-zone document-number handling — see "Document number
                  redaction". `false` (default) keeps the document's own
                  number readable (warned when one was detected); `true`
                  destroys it. Personal identification numbers and the MRZ
                  are always destroyed regardless.

  schemas:
    Findings:
      type: object
      description: |
        Detected information types with match counts. Keys are detector names
        (for example `NETHERLANDS_BSN_NUMBER`, `MRZ_TD3_LINE_2`,
        `OBJECT_TYPE/PERSON/FACE`); values are how often each matched.
        Detected values themselves are never returned.
      additionalProperties:
        type: integer
        minimum: 1
      example:
        MRZ_TD3_LINE_2: 1
        NETHERLANDS_BSN_NUMBER: 1

    Warning:
      type: string
      description: |
        Coverage warnings. The `warnings` array is always present; empty means
        full expected coverage.

        | Code | Meaning | Caller action |
        |---|---|---|
        | `nothing_detected` | Nothing recognizable was found; the image is returned **unchanged**. | Do not archive as redacted without review. |
        | `no_country_hint` | Auto mode ran (no `country_hint` given). | Provide the hint for more precise detection. |
        | `country_detectors_unavailable` | No country-specific number detection exists for the hinted country; MRZ-level redaction only. | The printed national number may remain visible — review. |
        | `country_detector_no_match` | No national identifier was detected. | If the document visibly carries one, it may remain — review. |
        | `mrz_not_found` | No machine-readable zone was detected (absent on this side, cropped, or unreadable). | Expected for ID-card fronts; otherwise consider a better scan. |
        | `missing_face_finding` | No portrait photo was detected. | Usually fine for text-only pages. |
        | `missing_signature_finding` | No signature was detected. | Usually fine for pages without one. |
        | `photo_not_redacted` | `redact_photo=false` was requested: the returned copy keeps the portrait visible. | Handle the artifact accordingly. |
        | `document_number_not_redacted` | A document number was detected and left visible (the default). | Send `redact_documentnumber=true` if you need it destroyed. |
      enum:
        - nothing_detected
        - no_country_hint
        - country_detectors_unavailable
        - country_detector_no_match
        - mrz_not_found
        - missing_face_finding
        - missing_signature_finding
        - photo_not_redacted
        - document_number_not_redacted

    Warnings:
      type: array
      description: Always present. Empty array = full expected coverage.
      items:
        $ref: "#/components/schemas/Warning"

    RedactedDocument:
      type: object
      description: Single-page result.
      required: [image, warnings, findings]
      properties:
        image:
          type: string
          contentEncoding: base64
          description: |
            The redacted page as base64-encoded PNG — always the
            most-redacted artifact of the request.
        image_photo_visible:
          type: string
          contentEncoding: base64
          description: |
            Present only with `redact_photo=both`: the sibling copy with
            the portrait intact and every other redaction applied.
        warnings:
          $ref: "#/components/schemas/Warnings"
        findings:
          $ref: "#/components/schemas/Findings"

    RedactedPage:
      type: object
      required: [page, image, findings]
      properties:
        page:
          type: integer
          minimum: 1
          description: 1-based page number.
        image:
          type: string
          contentEncoding: base64
          description: |
            The redacted page as base64-encoded PNG — always the
            most-redacted artifact of the request.
        image_photo_visible:
          type: string
          contentEncoding: base64
          description: |
            Present only with `redact_photo=both`: the sibling copy with
            the portrait intact and every other redaction applied.
        findings:
          $ref: "#/components/schemas/Findings"

    RedactedPages:
      type: object
      description: Multi-page PDF result.
      required: [pages, warnings, findings]
      properties:
        pages:
          type: array
          minItems: 2
          items:
            $ref: "#/components/schemas/RedactedPage"
        warnings:
          $ref: "#/components/schemas/Warnings"
        findings:
          allOf:
            - $ref: "#/components/schemas/Findings"
          description: Aggregate counts across all pages.

    InspectionReport:
      type: object
      required: [findings, warnings]
      properties:
        findings:
          $ref: "#/components/schemas/Findings"
        warnings:
          $ref: "#/components/schemas/Warnings"
        pages:
          type: array
          description: Present for multi-page PDFs only.
          items:
            type: object
            required: [page, findings]
            properties:
              page:
                type: integer
                minimum: 1
              findings:
                $ref: "#/components/schemas/Findings"

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Stable machine-readable reason code.
              enum:
                - invalid_api_key
                - not_found
                - method_not_allowed
                - payload_too_large
                - unsupported_media_type
                - country_hint_invalid
                - country_not_supported
                - file_required
                - redact_photo_invalid
                - redact_documentnumber_invalid
                - too_many_pages
                - pdf_encrypted
                - unreadable_image
                - image_normalization_failed
                - rate_limited
                - daily_quota_exceeded
                - internal_error
                - dlp_unavailable
            message:
              type: string
              description: Static human-readable explanation.

  responses:
    Unauthorized:
      description: The bearer API key is missing, unknown, or revoked (`invalid_api_key`).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              code: invalid_api_key
              message: The bearer API key is missing, unknown, or revoked.
    PayloadTooLarge:
      description: The document exceeds the 10 MiB limit (`payload_too_large`).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    UnsupportedMediaType:
      description: The content is not a JPEG, PNG, BMP or PDF (`unsupported_media_type`). Detection is content-based.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    UnprocessableDocument:
      description: |
        The request is well-formed but the document cannot be processed.
        Codes: `country_hint_invalid`, `country_not_supported`,
        `file_required`, `redact_photo_invalid`,
        `redact_documentnumber_invalid`, `too_many_pages`,
        `pdf_encrypted`, `unreadable_image`, `image_normalization_failed`.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              code: too_many_pages
              message: The PDF exceeds the 5-page limit; split it and submit the pages separately.
    RateLimited:
      description: |
        Too many requests. Either the per-minute rate limit
        (`rate_limited` — back off seconds) or the key's daily call
        allowance (`daily_quota_exceeded` — `Retry-After` points at the
        next UTC midnight; contact the IDBird team to raise the
        allowance). Honor `Retry-After` in both cases.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    InternalError:
      description: Unexpected server error (`internal_error`). No image is returned.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    BackendUnavailable:
      description: The redaction backend is temporarily unavailable (`dlp_unavailable`). No image is returned — retry with backoff.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
