Flask
Valida documentos en Flask con el SDK de Python constaia, leyendo el fichero de request.files y verificando los webhooks con request.get_data().
Esta guía integra Constaia en una aplicación Flask con el SDK oficial constaia: una ruta que recibe el fichero de
request.files (desde un formulario o el widget), un webhook que verifica la firma con el cuerpo
crudo y tests con el cliente de pruebas de Flask. Los detalles del SDK están en la guía de
Python.
Requisitos
- Flask 2.2 o posterior y Python ≥ 3.10 (los ejemplos usan
match). - Una clave de test
ck_test_…del panel. En modo test no se consumen créditos y el resultado depende del nombre del fichero.
Instalación
pip install flask constaiaVariables de entorno
CONSTAIA_API_KEY=ck_test_...
CONSTAIA_WEBHOOK_SECRET=whsec_...Constaia() lee CONSTAIA_API_KEY del entorno. La clave se queda en el servidor.
Aplicación
expect y checks los fija el servidor; del navegador solo se aceptan el fichero y el idioma. MAX_CONTENT_LENGTH
corta en Flask las peticiones de más de 21 MB (20 MB de fichero más el resto del formulario).
import json
import logging
import os
from flask import Flask, abort, jsonify, request
import constaia
from constaia import Constaia, ConstaiaError, InsufficientCreditsError, InvalidRequestError, RateLimitError
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 21 * 1024 * 1024
app.config["CONSTAIA_WEBHOOK_SECRET"] = os.environ["CONSTAIA_WEBHOOK_SECRET"]
client = Constaia(timeout=60.0, max_retries=2)
logger = logging.getLogger(__name__)
ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "webp", "heic", "pdf"}
USER_MESSAGES = {
"file_too_large": "El archivo supera los 20 MB.",
"unsupported_file_type": "Sube una imagen (JPG, PNG, WEBP, HEIC) o un PDF.",
"unreadable_image": "No se puede leer la imagen. Haz otra foto con buena luz.",
"unreadable_pdf": "No se puede leer el PDF.",
}
def error(message: str, status: int):
return jsonify({"error": {"message": message}}), status
@app.errorhandler(413)
def too_large(_):
return error("El archivo supera los 20 MB.", 413)
@app.post("/documents")
def documents():
# Protege esta ruta con tu login (por ejemplo flask_login.login_required): cada análisis live consume créditos.
upload = request.files.get("file")
if upload is None or not upload.filename:
return error("No se ha recibido el documento.", 400)
if upload.filename.rsplit(".", 1)[-1].lower() not in ALLOWED_EXTENSIONS:
return error(USER_MESSAGES["unsupported_file_type"], 400)
try:
language = json.loads(request.form.get("options") or "{}").get("language", "es")
except (ValueError, AttributeError):
language = "es"
try:
analysis = client.analyze(
upload.stream,
filename=upload.filename,
expect=["es_dni", "es_nie", "passport"],
checks={"not_expired": True, "min_age_years": 18},
storage="none",
language=language if language in ("es", "en", "pt", "fr") else "es",
)
except InvalidRequestError as exc:
return error(USER_MESSAGES.get(exc.code or "", "El documento no se ha podido procesar."), 422)
except (InsufficientCreditsError, RateLimitError) as exc:
logger.error("Constaia %s %s request_id=%s", exc.status, exc.code, exc.request_id)
return error("El servicio está ocupado. Inténtalo en unos minutos.", 503)
except ConstaiaError as exc:
logger.error("Constaia %s %s request_id=%s", exc.status, exc.code, exc.request_id)
return error("No hemos podido comprobar el documento. Inténtalo más tarde.", 502)
# Guarda aquí analysis["id"] y el veredicto en tu base de datos.
return jsonify({key: analysis.get(key) for key in ("id", "object", "status", "document", "verdict", "warnings")})
@app.post("/webhooks/constaia")
def constaia_webhook():
try:
event = constaia.webhooks.verify(request.get_data(), request.headers, app.config["CONSTAIA_WEBHOOK_SECRET"])
except constaia.WebhookVerificationError:
abort(400)
# Deduplica por request.headers["webhook-id"] (Redis, base de datos) y encola el trabajo pesado.
match event["type"]:
case "analysis.completed" | "analysis.review_required" | "analysis.failed":
analysis = event["data"]
logger.info("Constaia %s: %s", analysis["id"], (analysis.get("verdict") or {}).get("status"))
case "credits.low":
logger.warning("Constaia credits low: %s", event["data"]["credits_available"])
return "", 204Notas:
upload.streames un fichero binario que el SDK lee una sola vez;filename=upload.filenameconserva el nombre original, que en modo test decide la respuesta.- La respuesta incluye solo lo que el widget necesita (
verdict.status,verdict.reasons[].message,warnings); los campos extraídos (analysis["fields"]) se quedan en el servidor. - Si el análisis tarda más de 30 s la API responde
202constatusqueuedoprocessingyverdictaNone; el resultado llega por el webhook. - Si usas
CSRFProtectde Flask-WTF, exime el webhook concsrf.exempt(constaia_webhook)y envía el token CSRF desde el widget en su atributoheaders.
Formulario o widget
<script type="module" src="https://cdn.jsdelivr.net/npm/@constaia/widget@0.1"></script>
<constaia-upload endpoint="{{ url_for('documents') }}" document="es_dni" lang="es"></constaia-upload>Un formulario clásico también sirve: <form method="post" enctype="multipart/form-data"> con un
<input type="file" name="file"> enviado a /documents.
Webhook
La ruta /webhooks/constaia verifica la firma con request.get_data(), el cuerpo crudo en bytes, antes de parsear
nada. No uses request.get_json() para verificar: el JSON re-serializado no coincide con la firma. Crea el endpoint en
el panel con la URL https://tu-dominio.com/webhooks/constaia, guarda el secret en CONSTAIA_WEBHOOK_SECRET y
responde 2xx en menos de 15 s; para trabajo pesado usa Celery. Formato y reintentos en
Webhooks.
Errores
| HTTP | Excepción | code habitual | Qué hacer |
|---|---|---|---|
| 400, 413, 415, 422 | InvalidRequestError | file_too_large, unsupported_file_type, unreadable_image, invalid_parameter | Pide otro fichero o corrige la opción |
| 401 | AuthenticationError | invalid_api_key | Revisa CONSTAIA_API_KEY |
| 402 | InsufficientCreditsError | insufficient_credits | Recarga créditos |
| 429 | RateLimitError | rate_limited | Ya reintentado por el SDK; retry_after indica la espera |
| 5xx | APIError | internal_error, live_mode_unavailable | Ya reintentado; live_mode_unavailable: usa ck_test_ |
Registra siempre exc.request_id. Lista completa en Errores.
Tests
Con CONSTAIA_API_KEY=ck_test_... la API responde según el nombre del fichero y no cobra. Copia cualquier JPEG real a
tests/fixtures/ como dni_valid.jpg, dni_expired.jpg y blurry.jpg.
import io
import json
import os
from pathlib import Path
import pytest
import constaia
from app import app
FIXTURES = Path(__file__).parent / "fixtures"
needs_test_key = pytest.mark.skipif(
not os.environ.get("CONSTAIA_API_KEY", "").startswith("ck_test_"), reason="needs a ck_test_ key"
)
@pytest.fixture
def http():
return app.test_client()
def post_file(http, name: str):
data = {"file": (io.BytesIO((FIXTURES / name).read_bytes()), name)}
return http.post("/documents", data=data, content_type="multipart/form-data")
@needs_test_key
@pytest.mark.parametrize("name, status", [("dni_valid.jpg", "valid"), ("dni_expired.jpg", "invalid"), ("blurry.jpg", "review")])
def test_verdicts(http, name, status):
response = post_file(http, name)
assert response.status_code == 200
assert response.get_json()["verdict"]["status"] == status
def test_webhook_signature(http):
body = json.dumps({"type": "analysis.completed", "data": {"id": "an_test", "status": "completed"}})
headers = constaia.webhooks.sign(body, app.config["CONSTAIA_WEBHOOK_SECRET"])
assert http.post("/webhooks/constaia", data=body, headers=headers, content_type="application/json").status_code == 204
headers["webhook-signature"] = "v1,bad"
assert http.post("/webhooks/constaia", data=body, headers=headers, content_type="application/json").status_code == 400CONSTAIA_API_KEY=ck_test_... CONSTAIA_WEBHOOK_SECRET=whsec_... pytest -qCONSTAIA_WEBHOOK_SECRET debe tener el formato whsec_<base64>. Más ficheros en Modo test.
Checklist de producción
MAX_CONTENT_LENGTHde 21 MB en Flask yclient_max_body_size 21M;en nginx.- Timeouts ≥ 60 s:
gunicorn --timeout 90,proxy_read_timeout 90s;y balanceador. O usaasync_=Truecon webhook, o una tarea de Celery. - Ruta de subida con login y límite de frecuencia (por ejemplo Flask-Limiter): cada análisis live consume créditos.
expectychecksfijos en la ruta; del navegador solo aceptas el fichero ylanguage.CONSTAIA_API_KEY=ck_live_...solo en producción.- Webhook con firma verificada, exento de CSRF, deduplicado por
webhook-idy con respuesta rápida. - Revisa Almacenamiento y privacidad para elegir
storage.
Siguientes pasos
Django
Valida documentos en Django con el SDK constaia, con un formulario FileField, una vista de Django REST Framework y un webhook csrf_exempt verificado.
FastAPI
Valida documentos en FastAPI con el cliente async de constaia, UploadFile, modelos pydantic de respuesta, BackgroundTasks y un webhook que verifica la firma.