API REST · OpenAPI 3.1 · clave gratis

Documentos dentro, JSON fuera

Envía un PDF o una foto de una factura, ticket, extracto bancario, DNI o tarjeta de visita y lee los campos extraídos en JSON. Pensada para scripts y para agentes de IA. La clave está incluida en todos los planes, también en el gratuito.

Empezar en 3 comandos

Primero crea la clave en la app, en el apartado API.

# 1. Create your key in the app (every plan, free included): https://app.whappscan.com -> API
export WHAPPSCAN_KEY=wsk_xxxxxxxxxxxxxxxx
BASE=https://api.whappscan.com

# 2. Upload a PDF or a photo -> HTTP 202 and a result_id
curl -s -X POST "$BASE/api/v1/process" \
  -H "X-API-Key: $WHAPPSCAN_KEY" \
  -F "file=@invoice.pdf;type=application/pdf"

# 3. Poll until "status" is "done" (every 2-3 seconds)
curl -s "$BASE/api/v1/results/RESULT_ID" -H "X-API-Key: $WHAPPSCAN_KEY"

Autenticación

Envía la clave en la cabecera X-API-Key en cada petición. Las claves empiezan por wsk_ y se crean (o se regeneran, lo que invalida la anterior al instante) en la app, en API. Todos los planes tienen clave; el plan gratuito usa sus 20 páginas al mes, compartidas con la web, WhatsApp, el correo y Telegram.

Cómo funciona

El proceso es asíncrono: la subida responde enseguida con un result_id y consultas el resultado.

  1. 1. Subir

    POST /api/v1/process deja que WhappScan elija la plantilla adecuada; POST /api/v1/scanners/{id}/upload usa la que tú elijas. Formulario multipart con un campo llamado file. Responde HTTP 202 con un result_id.

  2. 2. Consultar

    GET /api/v1/results/{result_id} cada 2-3 segundos. El status pasa de queued a processing y a done. Lo normal son 3-20 segundos; un extracto bancario largo puede tardar unos 2 minutos.

  3. 3. Leer

    Cuando status es done, extracted_data.fields[] trae un elemento por campo con value, confidence (0-1) y source_fragment. Los campos de tabla (líneas de factura, movimientos) llegan como un array de filas. error y rejected llevan error_code y error_message; rejected significa que no se cobró ninguna página.

Estado del resultado

status
queued / processingSigue consultando.
doneDatos disponibles en extracted_data.
errorEl proceso falló (mira error_code).
rejectedRechazado antes de procesar, sin cobro (por ejemplo NO_TEXT: no hay nada legible en el archivo).

Un resultado terminado

{
  "success": true,
  "result_id": "bd929a0f-de95-4321-97a4-abdc5d078da2",
  "status": "done",
  "scanner_name": "Business card",
  "tokens_used": 1,
  "confidence": 0.95,
  "extracted_data": {
    "fields": [
      {
        "name": "contacts",
        "value_type": "table",
        "confidence": 0.95,
        "value": [
          { "full_name": "Sofia Wong", "company_name": "Sunway Packaging",
            "job_title": "Procurement Lead", "phone": "+60 17-716 7035",
            "email": "sofia.wong@sunwaypackaging.example", "website": null }
        ]
      }
    ]
  }
}

Límites

Plan gratuitoPlanes de pago
Páginas al mes20 (se renuevan cada mes)de 200 a 15.000, más paquetes opcionales
Peticiones por minuto y clave60120
Tamaño de archivounos 4,4 MB (por encima, 413)unos 4,4 MB
Páginas por documento5050
Plantillas10 en total (las cuentas nuevas empiezan con 8)según el plan
FormatosPDF, JPEG, PNGPDF, JPEG, PNG

Cada página de un documento gasta una página de la cuota (una foto cuenta como una). Las respuestas llevan X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset; ante un HTTP 429 espera los segundos de Retry-After. En fotos, 2.600 píxeles en el lado largo bastan para el OCR y el archivo pesa poco.

Plantillas (scanners)

Una plantilla define qué campos se extraen. Las cuentas nuevas traen plantillas listas (factura, ticket, albarán, presupuesto, pedido, DNI y pasaporte, tarjeta de visita, extracto bancario); lístalas con GET /api/v1/scanners o crea las tuyas. Escribe una descripción corta por campo diciendo dónde aparece y cómo darle formato: la lee el modelo.

Crear una plantilla

curl -s -X POST "$BASE/api/v1/scanners" \
  -H "X-API-Key: $WHAPPSCAN_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Supplier invoice",
    "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"}]}
    ]}'

Errores

Los errores son JSON con success false, un error_code estable y un message. Los casos que dependen de qué hagas después llevan un next_action.

HTTPerror_code
401API_KEY_REQUIRED / INVALID_API_KEY
402INSUFFICIENT_TOKENS / PAYMENT_REQUIRED
429RATE_LIMITED
400UNSUPPORTED_FILE_TYPE / NO_FILE / DOCUMENT_TOO_LONG / VALIDATION_ERROR / INVALID_JSON
403SCANNER_LIMIT
404SCANNER_NOT_FOUND / RESULT_NOT_FOUND
413—
503BUSY
500INTERNAL_ERROR

El 413 lo devuelve la pasarela antes de que se ejecute la API y su cuerpo es {"message":"Request Too Long"}. Una URL que no existe responde 403 con {"message":"Missing Authentication Token"}.

Para agentes de IA y scripts

Todo lo que necesita un agente es legible por máquina: la especificación OpenAPI 3.1 en /openapi.json tiene un operationId por acción (processDocument, getResult, listScanners…) y descripciones escritas para modelos, y /llms.txt indexa el sitio. La receta de abajo sube, consulta con espera y maneja 402, 413 y 429.

Pega esto en tu agente (Claude Code, Cursor, un agente propio…)

Use the WhappScan API to extract data from documents.
Spec: https://whappscan.com/openapi.json  (docs: https://whappscan.com/en/api)
Auth: header X-API-Key from the env var WHAPPSCAN_KEY.
Flow: POST /api/v1/process (multipart, field "file") -> result_id -> poll GET /api/v1/results/{id} every 2-3 s until status is "done".
Read extracted_data.fields[].value. On 402 tell me the next_action; on 429 wait Retry-After; files must be under ~4.4 MB.

Python

import mimetypes, os, time, requests

BASE = "https://api.whappscan.com"
HEADERS = {"X-API-Key": os.environ["WHAPPSCAN_KEY"]}

def extract(path, scanner_id=None, timeout=180):
    """Upload a document and return the extracted fields as a dict {name: value}."""
    url = f"{BASE}/api/v1/scanners/{scanner_id}/upload" if scanner_id else f"{BASE}/api/v1/process"
    mime = mimetypes.guess_type(path)[0] or "application/pdf"  # the API needs the part's content type
    with open(path, "rb") as f:
        r = requests.post(url, headers=HEADERS, files={"file": (os.path.basename(path), f, mime)})
    if r.status_code == 402:   # no pages left: r.json()["next_action"] says what to do
        raise RuntimeError(r.json()["next_action"])
    if r.status_code == 413:   # over ~4.4 MB: lower the resolution or compress the PDF
        raise RuntimeError("file too large")
    r.raise_for_status()
    result_id = r.json()["result_id"]

    deadline = time.time() + timeout
    while time.time() < deadline:
        time.sleep(2.5)
        res = requests.get(f"{BASE}/api/v1/results/{result_id}", headers=HEADERS)
        if res.status_code == 429:  # rate limited: wait what the server says
            time.sleep(int(res.headers.get("Retry-After", 5)))
            continue
        res.raise_for_status()
        data = res.json()
        if data["status"] == "done":
            return {f["name"]: f["value"] for f in data["extracted_data"]["fields"]}
        if data["status"] in ("error", "rejected"):
            raise RuntimeError(f'{data.get("error_code")}: {data.get("error_message")}')
    raise TimeoutError("still processing")

print(extract("invoice.pdf"))

Node.js

// Node 18+ (fetch, FormData and Blob are built in)
import { readFile } from "node:fs/promises";
import { basename } from "node:path";

const BASE = "https://api.whappscan.com";
const headers = { "X-API-Key": process.env.WHAPPSCAN_KEY };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export async function extract(path, mime = "application/pdf", scannerId) {
  const form = new FormData();
  form.append("file", new Blob([await readFile(path)], { type: mime }), basename(path));
  const url = scannerId ? `${BASE}/api/v1/scanners/${scannerId}/upload` : `${BASE}/api/v1/process`;
  const up = await fetch(url, { method: "POST", headers, body: form });
  if (up.status === 402) throw new Error(JSON.stringify((await up.json()).next_action));
  if (!up.ok) throw new Error(`upload failed: ${up.status} ${await up.text()}`);
  const { result_id } = await up.json();

  for (let i = 0; i < 80; i++) {
    await sleep(2500);
    const res = await fetch(`${BASE}/api/v1/results/${result_id}`, { headers });
    if (res.status === 429) { await sleep(Number(res.headers.get("retry-after") || 5) * 1000); continue; }
    const data = await res.json();
    if (data.status === "done") return Object.fromEntries(data.extracted_data.fields.map((f) => [f.name, f.value]));
    if (["error", "rejected"].includes(data.status)) throw new Error(`${data.error_code}: ${data.error_message}`);
  }
  throw new Error("still processing");
}

console.log(await extract("invoice.pdf"));

openapi.json · llms.txt

Preguntas

¿La API es gratis?

La clave está incluida en todos los planes. El plan gratuito usa su cuota de 20 páginas al mes y 60 peticiones por minuto; los planes de pago tienen más páginas y 120 peticiones por minuto.

¿Cuánto tarda un documento?

Normalmente de 3 a 20 segundos. Los extractos bancarios largos pueden tardar unos 2 minutos. Consulta cada 2-3 segundos.

¿Puedo subir un archivo de más de 4,4 MB?

No por este endpoint: la pasarela lo rechaza con HTTP 413. Baja la resolución de las fotos (2.600 px en el lado largo bastan) o comprime el PDF. La app web lo hace sola con las fotos.

¿Hay webhook en vez de consultar?

Todavía no. Consulta GET /api/v1/results/{id} cada 2-3 segundos.

¿Qué pasa con mis archivos?

El archivo se guarda en un bucket temporal de AWS en la UE (Fráncfort) solo mientras se procesa y después se borra; pase lo que pase, una regla de seguridad elimina lo que quede a los 7 días. Los datos extraídos quedan en tu cuenta. La extracción usa proveedores de modelos de IA; la política de privacidad tiene los detalles.