{
  "openapi": "3.1.0",
  "info": {
    "title": "WhappScan API",
    "version": "1.0.0",
    "summary": "Turn invoices, receipts, bank statements, IDs and other documents into structured JSON.",
    "description": "Send a PDF or a photo, get back the fields of a template (invoice number, totals, lines, MRZ data, contacts…) as JSON.\n\n**Flow (asynchronous):** 1) `POST /api/v1/process` (let WhappScan pick the template) or `POST /api/v1/scanners/{scannerId}/upload` (use a template you choose) → HTTP 202 with a `result_id`. 2) Poll `GET /api/v1/results/{resultId}` every 2-3 seconds until `status` is `done` (typically 3-20 s; up to ~2 min for long statements), `error` or `rejected`. 3) Read `extracted_data.fields[]`.\n\n**Authentication:** every request needs the header `X-API-Key: wsk_...`. Create the key in the WhappScan app (https://app.whappscan.com → API). **All plans, including the free plan, can create a key.**\n\n**Quota:** each page processed uses one page of the account's quota (free plan: 20 pages per month, shared with the web app, WhatsApp, email and Telegram; paid plans: 200 to 15,000 pages per month plus optional packs). When the quota is exhausted the upload returns HTTP 402 `INSUFFICIENT_TOKENS` with a `next_action`.\n\n**Limits:** 60 requests per minute per key on the free plan and 120 on paid plans (HTTP 429 with `Retry-After`); about 4.4 MB per file; up to 50 pages per document; up to 10 templates on the free plan.\n\nHuman-readable docs: https://whappscan.com/en/api · Machine-readable index: https://whappscan.com/llms.txt",
    "contact": {
      "name": "WhappScan",
      "url": "https://whappscan.com",
      "email": "hola@whappscan.com"
    },
    "termsOfService": "https://whappscan.com/en/terms"
  },
  "servers": [
    {
      "url": "https://api.whappscan.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Documents",
      "description": "Upload a document and read the extracted data."
    },
    {
      "name": "Templates",
      "description": "Templates (called scanners) define which fields are extracted. Create your own or use the ones in your account."
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "paths": {
    "/api/v1/process": {
      "post": {
        "operationId": "processDocument",
        "tags": [
          "Documents"
        ],
        "summary": "Upload a document and let WhappScan choose the template",
        "description": "Use this when you do not know which template fits (a mixed pile of invoices, receipts, IDs…). WhappScan reads the document, picks the best matching template of the account and extracts its fields. Returns immediately with HTTP 202 and a `result_id`; poll `getResult` until `status` is `done`. Costs one page of quota per page of the document (images count as 1).",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The document: a PDF or a JPEG/PNG image (other formats such as WebP or HEIC are not guaranteed to be readable). Keep it under about 4.4 MB; larger requests are rejected by the gateway with HTTP 413."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted: the document is queued. Poll getResult with `result_id`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadAccepted"
                },
                "example": {
                  "success": true,
                  "result_id": "9b87b269-0357-453a-9325-b2373f1a8516",
                  "request_id": "d8150c49-c68b-4985-b695-42bf75732059",
                  "scanner_id": "auto",
                  "scanner_name": "Auto-detecting...",
                  "filename": "invoice.pdf",
                  "status": "queued",
                  "tokens_required": 1,
                  "tokens_remaining": 19,
                  "queued_at": "2026-10-02T09:22:44.021Z"
                }
              }
            }
          },
          "400": {
            "description": "No file, unsupported type (only PDF and images) or too many pages.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "UNSUPPORTED_FILE_TYPE",
                  "message": "Only images and PDF files are supported"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "413": {
            "$ref": "#/components/responses/FileTooLarge"
          },
          "503": {
            "description": "Too many simultaneous uploads for this account; retry in a few seconds (`retryable` is true).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "BUSY",
                  "message": "Too many simultaneous uploads for this account. Please retry in a few seconds.",
                  "retryable": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "INVALID_API_KEY",
                  "message": "Invalid API key"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/scanners/{scannerId}/upload": {
      "post": {
        "operationId": "uploadDocumentToScanner",
        "tags": [
          "Documents"
        ],
        "summary": "Upload a document to a specific template",
        "description": "Use this when you know the template (for example an invoice template you created, or one returned by listScanners). Same asynchronous flow as processDocument: HTTP 202 with a `result_id`, then poll getResult.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ScannerId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The document: a PDF or a JPEG/PNG image (other formats such as WebP or HEIC are not guaranteed to be readable). Keep it under about 4.4 MB; larger requests are rejected by the gateway with HTTP 413."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted: the document is queued.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadAccepted"
                }
              }
            }
          },
          "400": {
            "description": "No file, unsupported type or too many pages.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "UNSUPPORTED_FILE_TYPE",
                  "message": "Only images and PDF files are supported"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "description": "The template does not exist in this account.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "SCANNER_NOT_FOUND",
                  "message": "Scanner not found"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/FileTooLarge"
          },
          "503": {
            "description": "Too many simultaneous uploads for this account; retry in a few seconds.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "BUSY",
                  "message": "Too many simultaneous uploads for this account. Please retry in a few seconds.",
                  "retryable": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "INVALID_API_KEY",
                  "message": "Invalid API key"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/results/{resultId}": {
      "get": {
        "operationId": "getResult",
        "tags": [
          "Documents"
        ],
        "summary": "Get the status and extracted data of a document",
        "description": "Poll this endpoint after an upload. Wait 2-3 seconds between calls (each call counts toward the rate limit) and give up after a few minutes. While `status` is `queued` or `processing` there is no data yet. When `status` is `done`, `extracted_data.fields[]` holds one entry per template field: `value` (a string, a number, null, or for table fields an array of row objects), `confidence` (0-1) and `source_fragment` (the text it was read from). `error` and `rejected` carry `error_code` and `error_message`; `rejected` means no pages were consumed (for example `NO_TEXT`: nothing readable in the file).",
        "parameters": [
          {
            "name": "resultId",
            "in": "path",
            "required": true,
            "description": "The `result_id` returned by the upload.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The current state of the document.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                }
              }
            }
          },
          "404": {
            "description": "No result with that id in this account.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "RESULT_NOT_FOUND",
                  "message": "Result not found"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "INVALID_API_KEY",
                  "message": "Invalid API key"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/scanners": {
      "get": {
        "operationId": "listScanners",
        "tags": [
          "Templates"
        ],
        "summary": "List the templates of the account",
        "description": "Returns every template (scanner) with its fields. Use the `scanner_id` with uploadDocumentToScanner. New accounts start with ready-made templates for invoices, receipts, delivery notes, quotes, purchase orders, IDs and passports, business cards and bank statements.",
        "responses": {
          "200": {
            "description": "The templates.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScannerList"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "INVALID_API_KEY",
                  "message": "Invalid API key"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "createScanner",
        "tags": [
          "Templates"
        ],
        "summary": "Create a template",
        "description": "Define the fields you want extracted. Field types: string, number, date, boolean, currency, email, phone and table (a repeating group such as invoice lines; it needs `item_fields`, one level deep). Write a short `description` per field saying where it appears and how to format it: it is what the extraction model reads. The free plan allows up to 10 templates in total, including the 8 ready-made ones (HTTP 403 `SCANNER_LIMIT`). Limits: up to 100 fields and 100 table columns, definition up to 100 KB; `name` up to 100 characters, `label` 200, `description` 2,000, `format_hint` 500 (HTTP 400 `VALIDATION_ERROR` above that).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScannerInput"
              },
              "example": {
                "name": "Supplier invoice",
                "description": "Invoices received from suppliers",
                "fields": [
                  {
                    "name": "invoice_number",
                    "type": "string",
                    "description": "Invoice number as printed"
                  },
                  {
                    "name": "total_amount",
                    "type": "currency"
                  },
                  {
                    "name": "lines",
                    "type": "table",
                    "item_fields": [
                      {
                        "name": "description",
                        "type": "string"
                      },
                      {
                        "name": "quantity",
                        "type": "number"
                      },
                      {
                        "name": "amount",
                        "type": "currency"
                      }
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Template created.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScannerCreated"
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON or invalid fields.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "VALIDATION_ERROR",
                  "message": "Field 'name' is required and must be a non-empty string"
                }
              }
            }
          },
          "403": {
            "description": "The free plan allows 10 templates in total, counting the ready-made ones every new account starts with (8): you can create 2 more.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "SCANNER_LIMIT",
                  "message": "The free plan allows up to 10 templates. Delete one or upgrade.",
                  "limit": 10,
                  "next_action": {
                    "type": "upgrade",
                    "pricing_url": "https://app.whappscan.com/pricing"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "INVALID_API_KEY",
                  "message": "Invalid API key"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/scanners/{scannerId}": {
      "get": {
        "operationId": "getScanner",
        "tags": [
          "Templates"
        ],
        "summary": "Get one template",
        "parameters": [
          {
            "$ref": "#/components/parameters/ScannerId"
          }
        ],
        "responses": {
          "200": {
            "description": "The template with all its field definitions.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScannerDetail"
                }
              }
            }
          },
          "404": {
            "description": "Template not found.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "SCANNER_NOT_FOUND",
                  "message": "Scanner not found"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "INVALID_API_KEY",
                  "message": "Invalid API key"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateScanner",
        "tags": [
          "Templates"
        ],
        "summary": "Change a template",
        "description": "Send at least one of `name`, `description`, `fields`. Sending `fields` replaces the whole list. Documents already processed keep their data.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ScannerId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScannerUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated template.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScannerDetail"
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON, nothing to update or invalid fields.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "VALIDATION_ERROR",
                  "message": "Provide at least one of: name, description, fields"
                }
              }
            }
          },
          "404": {
            "description": "Template not found.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "SCANNER_NOT_FOUND",
                  "message": "Scanner not found"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "INVALID_API_KEY",
                  "message": "Invalid API key"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "deleteScanner",
        "tags": [
          "Templates"
        ],
        "summary": "Delete a template",
        "description": "Deletes the template; documents already processed with it are kept.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ScannerId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "scanner_id": {
                      "type": "string"
                    },
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "scanner_id": "a8e5357b-aff0-4998-807b-79aae5764674",
                  "deleted": true
                }
              }
            }
          },
          "404": {
            "description": "Template not found.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "SCANNER_NOT_FOUND",
                  "message": "Scanner not found"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "success": false,
                  "error_code": "INVALID_API_KEY",
                  "message": "Invalid API key"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "API key starting with `wsk_`. Create or regenerate it in the WhappScan app (https://app.whappscan.com → API). Available on every plan. Regenerating a key invalidates the previous one immediately."
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "Requests allowed per minute for this key (60 free plan, 120 paid plans).",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Requests left in the current one-minute window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "description": "Seconds until the current window ends.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "parameters": {
      "ScannerId": {
        "name": "scannerId",
        "in": "path",
        "required": true,
        "description": "Template id (`scanner_id`) from listScanners or createScanner.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "RateLimited": {
        "description": "Too many requests for this key in the current minute. Wait `Retry-After` seconds.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "error_code": "RATE_LIMITED",
              "message": "Too many requests: limit is 60 per minute per API key. Retry after 29 seconds.",
              "limit_per_minute": 60,
              "retry_after_seconds": 29
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "No pages left in the account's quota (`INSUFFICIENT_TOKENS`) or an unpaid invoice (`PAYMENT_REQUIRED`). `next_action` says what to do.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "error_code": "INSUFFICIENT_TOKENS",
              "message": "Insufficient tokens. Required: 1, Available: 0",
              "tokens_required": 1,
              "tokens_available": 0,
              "plan": "free",
              "next_action": {
                "type": "upgrade_or_wait",
                "renews_at": "2026-11-01T00:00:00.000Z",
                "pricing_url": "https://app.whappscan.com/pricing",
                "message": "The free plan renews 20 pages every month. Upgrade or buy a page pack to continue now."
              }
            }
          }
        }
      },
      "FileTooLarge": {
        "description": "The request is too big for the gateway (about 4.4 MB of file; the body is `{\"message\":\"Request Too Long\"}` and has no `error_code`). Reduce the image resolution (2,600 px on the long side is enough for OCR), recompress it, or compress the PDF.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "message": {
                  "type": "string"
                }
              }
            },
            "example": {
              "message": "Request Too Long"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "success",
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error_code": {
            "type": "string",
            "description": "Stable machine-readable code: API_KEY_REQUIRED, INVALID_API_KEY, RATE_LIMITED, INSUFFICIENT_TOKENS, PAYMENT_REQUIRED, BUSY, SCANNER_NOT_FOUND, RESULT_NOT_FOUND, ROUTE_NOT_FOUND, NO_FILE, UNSUPPORTED_FILE_TYPE, DOCUMENT_TOO_LONG, UPLOAD_PARSE_ERROR, INVALID_JSON, VALIDATION_ERROR, SCANNER_LIMIT, COMPANY_NOT_FOUND, INTERNAL_ERROR (HTTP 500: retry later; quote `request_id` if you contact support)."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation (English)."
          },
          "next_action": {
            "$ref": "#/components/schemas/NextAction"
          }
        },
        "additionalProperties": true
      },
      "NextAction": {
        "type": "object",
        "description": "What the caller can do about the error.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "upgrade_or_wait",
              "buy_pages",
              "upgrade",
              "billing_portal"
            ]
          },
          "renews_at": {
            "type": "string",
            "format": "date-time",
            "description": "Free plan: when the 20 monthly pages renew."
          },
          "pricing_url": {
            "type": "string",
            "format": "uri"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "message": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "UploadAccepted": {
        "type": "object",
        "required": [
          "success",
          "result_id",
          "status"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "result_id": {
            "type": "string",
            "format": "uuid",
            "description": "Use it with getResult."
          },
          "request_id": {
            "type": "string",
            "format": "uuid"
          },
          "scanner_id": {
            "type": "string",
            "description": "The template id, or `auto` when WhappScan will choose."
          },
          "scanner_name": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "const": "queued"
          },
          "tokens_required": {
            "type": "integer",
            "description": "Pages that were reserved from the quota (1 per page of the document)."
          },
          "tokens_remaining": {
            "type": "integer",
            "description": "Pages left in the quota after this upload."
          },
          "queued_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Result": {
        "type": "object",
        "required": [
          "success",
          "result_id",
          "status"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "result_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "processing",
              "done",
              "error",
              "rejected"
            ],
            "description": "`queued` and `processing`: keep polling. `done`: data available. `error`: processing failed. `rejected`: refused before processing (no pages used)."
          },
          "scanner_id": {
            "type": "string"
          },
          "scanner_name": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "file_type": {
            "type": "string"
          },
          "tokens_used": {
            "type": [
              "integer",
              "string",
              "null"
            ],
            "description": "Pages consumed."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "source_type": {
            "type": "string",
            "const": "api"
          },
          "extracted_data": {
            "$ref": "#/components/schemas/ExtractedData"
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Average confidence of the extraction."
          },
          "error_code": {
            "type": "string",
            "description": "Only for `error` and `rejected`. Example: NO_TEXT (nothing readable)."
          },
          "error_message": {
            "type": "string"
          }
        }
      },
      "ExtractedData": {
        "type": "object",
        "description": "Present only when `status` is `done`. Templates with extra checks (invoice compliance, bank-statement balance check, automatic mode) add more blocks such as `compliance`, `balance_check` or `auto`.",
        "properties": {
          "status": {
            "type": "string",
            "description": "`ok` when the extraction ran."
          },
          "scanner_name": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "extracted_at": {
            "type": "string"
          },
          "summary": {
            "type": "object",
            "properties": {
              "fields_with_values": {
                "type": "integer"
              },
              "fields_null": {
                "type": "integer"
              }
            }
          },
          "doc_metadata": {
            "type": "object",
            "properties": {
              "num_paginas": {
                "type": "integer",
                "description": "Page count."
              },
              "tipo_archivo": {
                "type": "string"
              },
              "ocr_provider": {
                "type": "string"
              },
              "idioma_detectado_ocr": {
                "type": "string"
              }
            },
            "additionalProperties": true
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ExtractedField"
            }
          }
        },
        "additionalProperties": true
      },
      "ExtractedField": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "value_type": {
            "type": "string"
          },
          "value": {
            "description": "The extracted value: string, number, boolean, null (not found) or, for `table` fields, an array of row objects keyed by the item field names.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "type": "boolean"
              },
              {
                "type": "null"
              },
              {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            ]
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "source_fragment": {
            "type": "string",
            "description": "The text the value was read from."
          },
          "raw_candidates": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "normalization_notes": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "Field": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Key of the field in the output (snake_case recommended)."
          },
          "label": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "string",
              "number",
              "date",
              "boolean",
              "currency",
              "email",
              "phone",
              "table"
            ],
            "description": "Alias: `value_type`."
          },
          "value_type": {
            "type": "string",
            "enum": [
              "string",
              "number",
              "date",
              "boolean",
              "currency",
              "email",
              "phone",
              "table"
            ]
          },
          "required": {
            "type": "boolean"
          },
          "description": {
            "type": "string",
            "description": "Tells the model where to look and how to format the value."
          },
          "format_hint": {
            "type": "string"
          },
          "examples": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "string"
            },
            "description": "Use fictitious, format-only examples: the model may copy them."
          },
          "item_fields": {
            "type": "array",
            "description": "Required for `table` fields: the columns of each row (no nested tables).",
            "items": {
              "$ref": "#/components/schemas/Field"
            }
          }
        }
      },
      "ScannerInput": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200
          },
          "description": {
            "type": "string",
            "maxLength": 2000
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Field"
            }
          }
        }
      },
      "ScannerUpdate": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200
          },
          "description": {
            "type": "string",
            "maxLength": 2000
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Field"
            },
            "description": "Replaces the whole list."
          }
        }
      },
      "ScannerSummary": {
        "type": "object",
        "properties": {
          "scanner_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Field"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ScannerList": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "scanners": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScannerSummary"
            }
          }
        }
      },
      "ScannerCreated": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "scanner_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Field"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ScannerDetail": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "scanner_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Field"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  }
}
