{
  "openapi": "3.1.0",
  "info": {
    "title": "Billver Integration API (werknaam)",
    "version": "1.6.0",
    "description": "Contract afgeleid van de huidige code (src/routes/api/public/v1), v1.2 (gevalideerd met echte integratietests, zie dossier §00). Geen productiedomein. `protected` wordt altijd afgeleid uit de actuele server-side staat (status VERIFIED, actieve link, niet vervangen, geen open BLOCKED finding); elk foutantwoord bevat protected=false. Rate limiting is fail-closed (503).\n\nv1.6: Mail Verify inbound webhook + retention job (paths below are implemented; provider credentials/domain not configured)."
  },
  "servers": [
    {
      "url": "https://{verificatiedomein}/api/public/v1",
      "variables": {
        "verificatiedomein": {
          "default": "example.invalid"
        }
      }
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "igk_live_<base64url>. Alleen server-side. Scopes: invoices:write, invoices:read."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 128
        }
      },
      "XSource": {
        "name": "X-Source",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "pattern": "^[a-z0-9_-]{2,32}$"
        }
      }
    },
    "schemas": {
      "GuardStatus": {
        "type": "string",
        "enum": [
          "VERIFIED",
          "REVIEW",
          "BLOCKED"
        ]
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "unauthorized",
              "forbidden",
              "rate_limited",
              "service_unavailable",
              "idempotency_conflict",
              "idempotency_in_progress",
              "verification_domain_not_configured",
              "multipart_expected",
              "file_missing",
              "invalid_metadata",
              "invalid_reservation_id",
              "registration_failed",
              "not_found"
            ]
          },
          "message": {
            "type": "string"
          },
          "issues": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "protected": {
            "const": false
          }
        }
      },
      "Metadata": {
        "type": "object",
        "required": [
          "invoice_number",
          "supplier_name",
          "supplier_vat",
          "amount",
          "currency",
          "iban"
        ],
        "properties": {
          "invoice_number": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "supplier_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "supplier_vat": {
            "type": "string",
            "minLength": 4,
            "maxLength": 32
          },
          "customer_name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "customer_vat": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 32
          },
          "invoice_date": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "due_date": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "amount": {
            "type": [
              "number",
              "string"
            ],
            "description": "Totaal incl. btw in hoofdeenheden; server rondt af op centen."
          },
          "amount_excl_vat": {
            "type": [
              "number",
              "string",
              "null"
            ]
          },
          "vat_amount": {
            "type": [
              "number",
              "string",
              "null"
            ]
          },
          "currency": {
            "type": "string",
            "pattern": "^[A-Za-z]{3}$",
            "description": "ISO 4217, verplicht, nooit aangenomen."
          },
          "iban": {
            "type": "string",
            "minLength": 15,
            "maxLength": 40
          },
          "bic": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 11
          },
          "structured_reference": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "external_ref": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 128
          }
        }
      },
      "PrepareResponse": {
        "type": "object",
        "required": [
          "reservation_id",
          "expires_at",
          "verification_url",
          "qr_svg",
          "verification_url_production_ready"
        ],
        "properties": {
          "reservation_id": {
            "type": "string",
            "format": "uuid"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "7 dagen na aanmaak"
          },
          "verification_url": {
            "type": "string",
            "format": "uri"
          },
          "qr_svg": {
            "type": "string"
          },
          "verification_url_production_ready": {
            "type": "boolean"
          },
          "verification_url_warning": {
            "type": [
              "string",
              "null"
            ]
          },
          "note": {
            "type": "string"
          }
        }
      },
      "RegisterCreated": {
        "type": "object",
        "required": [
          "duplicate",
          "invoice_id",
          "sha256",
          "status",
          "protected"
        ],
        "properties": {
          "duplicate": {
            "const": false
          },
          "invoice_id": {
            "type": "string",
            "format": "uuid"
          },
          "sha256": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$"
          },
          "status": {
            "$ref": "#/components/schemas/GuardStatus"
          },
          "findings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "rule": {
                  "type": "string"
                },
                "severity": {
                  "$ref": "#/components/schemas/GuardStatus"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          },
          "layers": {
            "type": "object"
          },
          "qr": {
            "type": "object"
          },
          "verification_token": {
            "type": "string"
          },
          "verification_url": {
            "type": "string",
            "format": "uri"
          },
          "verification_qr_embedded": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "qr_list": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "verification_url_production_ready": {
            "type": "boolean"
          },
          "verification_url_warning": {
            "type": [
              "string",
              "null"
            ]
          },
          "protected": {
            "type": "boolean",
            "description": "Afgeleid uit actuele server-side staat; fail-closed."
          },
          "protection_reasons": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "status_review",
                "status_blocked",
                "status_unknown",
                "verification_link_inactive",
                "superseded",
                "supersede_state_unknown",
                "open_blocked_finding",
                "findings_unknown",
                "state_unavailable"
              ]
            }
          }
        }
      },
      "RegisterDuplicate": {
        "type": "object",
        "required": [
          "duplicate",
          "invoice_id",
          "sha256",
          "protected"
        ],
        "properties": {
          "duplicate": {
            "const": true
          },
          "idempotent_replay": {
            "type": "boolean"
          },
          "invoice_id": {
            "type": "string",
            "format": "uuid"
          },
          "sha256": {
            "type": "string"
          },
          "link": {
            "type": "object"
          },
          "protected": {
            "type": "boolean",
            "description": "Afgeleid uit actuele server-side staat; fail-closed."
          },
          "protection_reasons": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "status_review",
                "status_blocked",
                "status_unknown",
                "verification_link_inactive",
                "superseded",
                "supersede_state_unknown",
                "open_blocked_finding",
                "findings_unknown",
                "state_unavailable"
              ]
            }
          }
        }
      },
      "InvoiceStatus": {
        "type": "object",
        "properties": {
          "invoice_id": {
            "type": "string",
            "format": "uuid"
          },
          "invoice_number": {
            "type": "string"
          },
          "external_ref": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/GuardStatus"
          },
          "protected": {
            "type": "boolean",
            "description": "Afgeleid uit actuele server-side staat; fail-closed."
          },
          "sha256": {
            "type": "string"
          },
          "amount_cents": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "iban_masked": {
            "type": "string"
          },
          "structured_reference": {
            "type": [
              "string",
              "null"
            ]
          },
          "revision": {
            "type": "integer"
          },
          "supersedes": {
            "type": [
              "string",
              "null"
            ]
          },
          "superseded_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "qr_status": {
            "type": "string"
          },
          "verification_link": {
            "type": "object",
            "properties": {
              "active": {
                "type": "boolean"
              },
              "url": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "checks": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "blocked": {
                "type": "integer"
              },
              "review": {
                "type": "integer"
              },
              "last_at": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "open_findings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "rule": {
                  "type": "string"
                },
                "severity": {
                  "type": "string"
                },
                "created_at": {
                  "type": "string"
                }
              }
            }
          },
          "protection_reasons": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "status_review",
                "status_blocked",
                "status_unknown",
                "verification_link_inactive",
                "superseded",
                "supersede_state_unknown",
                "open_blocked_finding",
                "findings_unknown",
                "state_unavailable"
              ]
            }
          }
        }
      },
      "EventEnvelope": {
        "type": "object",
        "required": [
          "id",
          "sequence",
          "type",
          "created_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Gelijk aan header x-event-id; dedupe-sleutel."
          },
          "sequence": {
            "type": "integer",
            "description": "Monotone cursor voor /events?after="
          },
          "type": {
            "type": "string",
            "enum": [
              "invoice.registration_accepted",
              "invoice.registration_rejected",
              "finding.created",
              "verification.checked",
              "verification_token.rotated",
              "verification_token.revoked"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "invoice_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "data": {
            "type": "object",
            "description": "Nooit een volledig IBAN; alleen iban_last4."
          }
        }
      }
    },
    "responses": {
      "Err": {
        "description": "Fout",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/invoices/prepare": {
      "post": {
        "summary": "Stap 1: reserveer invoice-id + verificatie-URL + QR",
        "description": "Scope invoices:write. Sleutel: Idempotency-Key, anders body.idempotency_key, anders body.external_ref (verplicht).",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/XSource"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "idempotency_key": {
                    "type": "string"
                  },
                  "external_ref": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Gereserveerd (of bestaande reservering)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PrepareResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Err"
          },
          "401": {
            "$ref": "#/components/responses/Err"
          },
          "403": {
            "$ref": "#/components/responses/Err"
          },
          "409": {
            "$ref": "#/components/responses/Err"
          },
          "429": {
            "description": "rate_limited (Retry-After)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service_unavailable (rate limiter niet beschikbaar, fail-closed) of verification_domain_not_configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/invoices": {
      "post": {
        "summary": "Registreer / finalize de definitieve PDF",
        "description": "Scope invoices:write. SHA-256 over exact de geüploade bytes is canoniek bewijs. Met reservation_id wordt de gereserveerde id/URL actief.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/XSource"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file",
                  "metadata"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "PDF, max 15 MB"
                  },
                  "metadata": {
                    "type": "string",
                    "description": "JSON-string volgens #/components/schemas/Metadata"
                  },
                  "reservation_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "source": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Nieuwe canonieke factuur",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegisterCreated"
                }
              }
            }
          },
          "200": {
            "description": "Duplicaat / idempotente replay",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegisterDuplicate"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Err"
          },
          "401": {
            "$ref": "#/components/responses/Err"
          },
          "403": {
            "$ref": "#/components/responses/Err"
          },
          "409": {
            "description": "idempotency_conflict (zelfde sleutel/reservering, ander bestand of concurrente revisie) of idempotency_in_progress (Retry-After: 5)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Err"
          },
          "429": {
            "description": "rate_limited (Retry-After)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service_unavailable (rate limiter niet beschikbaar, fail-closed) of verification_domain_not_configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/invoices/{id}": {
      "get": {
        "summary": "Actuele status van één factuur (eigen tenant)",
        "description": "Scope invoices:read. Bron van waarheid voor protected.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Err"
          },
          "403": {
            "$ref": "#/components/responses/Err"
          },
          "404": {
            "$ref": "#/components/responses/Err"
          },
          "429": {
            "description": "rate_limited (Retry-After)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service_unavailable (rate limiter niet beschikbaar, fail-closed) of verification_domain_not_configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/events": {
      "get": {
        "summary": "Pull-fallback: events na cursor",
        "description": "Scope invoices:read. Verliest niets; gebruik naast webhooks.",
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 100
            }
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-z_.]{3,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "events": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EventEnvelope"
                      }
                    },
                    "next_after": {
                      "type": "integer"
                    },
                    "has_more": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Err"
          },
          "403": {
            "$ref": "#/components/responses/Err"
          },
          "429": {
            "description": "rate_limited (Retry-After)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "service_unavailable (rate limiter niet beschikbaar, fail-closed) of verification_domain_not_configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/invoices/{id}/consumer-email": {
      "get": {
        "summary": "Consumer Verify e-mail renderen (verstuurt niets)",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Gerenderde e-mail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invoice_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "VERIFIED",
                        "REVIEW",
                        "BLOCKED"
                      ]
                    },
                    "protected": {
                      "type": "boolean"
                    },
                    "protection_reasons": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "claim": {
                      "const": "registered_payment_data"
                    },
                    "subject": {
                      "type": "string"
                    },
                    "html": {
                      "type": "string"
                    },
                    "text": {
                      "type": "string"
                    },
                    "cta_url": {
                      "type": "string"
                    },
                    "demo": {
                      "type": "boolean"
                    },
                    "sent": {
                      "const": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "unauthorized"
          },
          "403": {
            "description": "forbidden"
          },
          "404": {
            "description": "not_found"
          },
          "409": {
            "description": "no_active_link | verify_base_not_configured | cta_url_not_https"
          },
          "429": {
            "description": "rate_limited"
          },
          "503": {
            "description": "limiter unavailable (fail-closed)"
          }
        }
      }
    },
    "/api/public/inbound/mail/{provider}": {
      "post": {
        "summary": "Inbound mail webhook (Mail Verify)",
        "description": "provider ∈ mailgun|postmark|sandbox. mailgun: multipart form (timestamp, token, signature=HMAC-SHA256(signing key, timestamp+token), recipient, body-mime), 300 s window. postmark: JSON with RawEmail, HTTP Basic auth. sandbox: raw RFC 5322 body, X-IG-Signature t=..,v1=HMAC(t.body), X-IG-Recipient; development mode only. Idempotent on (provider, message id). Drops (unknown/expired alias, session limit) answer 200 without detail.",
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "mailgun",
                "postmark",
                "sandbox"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Accepted, duplicate or silently dropped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "duplicate": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "malformed"
          },
          "401": {
            "description": "signature_missing | signature_invalid | timestamp_outside_window | basic_auth_invalid"
          },
          "404": {
            "description": "unknown provider, or sandbox outside development"
          },
          "413": {
            "description": "request > 30 MB"
          },
          "422": {
            "description": "no_raw_mime | no_message_id"
          },
          "429": {
            "description": "rate_limited (600/min/provider)"
          },
          "503": {
            "description": "provider_not_configured | service_unavailable (fail-closed)"
          }
        }
      }
    },
    "/api/public/cron/retention": {
      "post": {
        "summary": "Retention cleanup job",
        "description": "Bearer = server cron secret, or scheduler publishable key (limited 6/min). Deletes only data past retention.",
        "responses": {
          "200": {
            "description": "counts per category"
          },
          "401": {
            "description": "unauthorized"
          },
          "429": {
            "description": "rate_limited"
          },
          "503": {
            "description": "cleanup_failed"
          }
        }
      }
    }
  },
  "webhooks": {
    "billverEvent": {
      "post": {
        "summary": "Ondertekend event naar het geregistreerde https-endpoint",
        "description": "Headers: x-event-id, x-event-type, x-signature: t=<unix>,v1=<hex HMAC-SHA256(secret, t + '.' + rawBody)>, user-agent Billver-Webhooks/1. Ontvanger weigert |now-t| > 300 s. 2xx = geleverd; timeout 5 s; redirects = mislukt; max 8 pogingen, backoff min(60*2^(n-1), 21600) s. Retries draaien alleen bij een volgend event of handmatige retry (geen planner). At-least-once; dedupe op x-event-id.",
        "parameters": [
          {
            "name": "x-signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          },
          {
            "name": "x-event-id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "x-event-type",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EventEnvelope"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ontvangen"
          }
        }
      }
    }
  }
}