Flask
Validate documents in Flask with the constaia Python SDK, reading the file from request.files and verifying webhooks with request.get_data().
This guide integrates Constaia into a Flask application with the official constaia SDK: a route that receives the
file from request.files (from a form or the widget), a webhook that verifies the signature with the
raw body and tests with Flask's test client. SDK details are in the Python guide.
Requirements
- Flask 2.2 or later and Python ≥ 3.10 (the examples use
match). - A test key
ck_test_…from the dashboard. In test mode no credits are consumed and the result depends on the file name.
Installation
pip install flask constaiaEnvironment variables
CONSTAIA_API_KEY=ck_test_...
CONSTAIA_WEBHOOK_SECRET=whsec_...Constaia() reads CONSTAIA_API_KEY from the environment. The key stays on the server.
Application
The server sets expect and checks; from the browser only the file and the language are accepted.
MAX_CONTENT_LENGTH makes Flask reject requests over 21 MB (a 20 MB file plus the rest of the form).
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": "The file is larger than 20 MB.",
"unsupported_file_type": "Upload an image (JPG, PNG, WEBP, HEIC) or a PDF.",
"unreadable_image": "We can't read the image. Take another photo in good light.",
"unreadable_pdf": "We can't read the PDF.",
}
def error(message: str, status: int):
return jsonify({"error": {"message": message}}), status
@app.errorhandler(413)
def too_large(_):
return error("The file is larger than 20 MB.", 413)
@app.post("/documents")
def documents():
# Protect this route with your login (for example flask_login.login_required): every live analysis consumes credits.
upload = request.files.get("file")
if upload is None or not upload.filename:
return error("No document received.", 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 "", "The document could not be processed."), 422)
except (InsufficientCreditsError, RateLimitError) as exc:
logger.error("Constaia %s %s request_id=%s", exc.status, exc.code, exc.request_id)
return error("The service is busy. Try again in a few minutes.", 503)
except ConstaiaError as exc:
logger.error("Constaia %s %s request_id=%s", exc.status, exc.code, exc.request_id)
return error("We could not check the document. Please try again later.", 502)
# Store analysis["id"] and the verdict in your database here.
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)
# Deduplicate on request.headers["webhook-id"] (Redis, database) and queue heavy work.
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 "", 204Notes:
upload.streamis a binary file that the SDK reads once;filename=upload.filenamekeeps the original name, which decides the response in test mode.- The response contains only what the widget needs (
verdict.status,verdict.reasons[].message,warnings); extracted fields (analysis["fields"]) stay on the server. - If the analysis takes longer than 30 s the API answers
202withstatusqueuedorprocessingandverdictset toNone; the result arrives through the webhook. - If you use Flask-WTF's
CSRFProtect, exempt the webhook withcsrf.exempt(constaia_webhook)and send the CSRF token from the widget in itsheadersattribute.
Form or 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="en"></constaia-upload>A classic form works too: <form method="post" enctype="multipart/form-data"> with an <input type="file" name="file">
posted to /documents.
Webhook
The /webhooks/constaia route verifies the signature with request.get_data(), the raw body as bytes, before parsing
anything. Don't use request.get_json() to verify: re-serialised JSON doesn't match the signature. Create the endpoint
in the dashboard with the URL https://your-domain.com/webhooks/constaia, store the secret in
CONSTAIA_WEBHOOK_SECRET and answer 2xx within 15 s; for heavy work use Celery. Format and
retries in Webhooks.
Errors
| HTTP | Exception | Typical code | What to do |
|---|---|---|---|
| 400, 413, 415, 422 | InvalidRequestError | file_too_large, unsupported_file_type, unreadable_image, invalid_parameter | Ask for another file or fix the option |
| 401 | AuthenticationError | invalid_api_key | Check CONSTAIA_API_KEY |
| 402 | InsufficientCreditsError | insufficient_credits | Top up credits |
| 429 | RateLimitError | rate_limited | Already retried by the SDK; retry_after gives the wait |
| 5xx | APIError | internal_error, live_mode_unavailable | Already retried; live_mode_unavailable: use ck_test_ |
Always log exc.request_id. Full list in Errors.
Tests
With CONSTAIA_API_KEY=ck_test_... the API answers based on the file name and charges nothing. Copy any real JPEG into
tests/fixtures/ as dni_valid.jpg, dni_expired.jpg and 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 must be in whsec_<base64> format. More files in Test mode.
Production checklist
MAX_CONTENT_LENGTHof 21 MB in Flask andclient_max_body_size 21M;in nginx.- Timeouts ≥ 60 s:
gunicorn --timeout 90,proxy_read_timeout 90s;and load balancer. Or useasync_=Truewith a webhook, or a Celery task. - Upload route with login and rate limiting (for example Flask-Limiter): every live analysis consumes credits.
expectandchecksfixed in the route; from the browser you accept only the file andlanguage.CONSTAIA_API_KEY=ck_live_...only in production.- Webhook with verified signature, CSRF-exempt, deduplicated on
webhook-idand answering quickly. - Read Storage and privacy to choose
storage.
Next steps
Django
Validate documents in Django with the constaia SDK, with a FileField form, a Django REST Framework view and a verified csrf_exempt webhook.
FastAPI
Validate documents in FastAPI with the async constaia client, UploadFile, pydantic response models, BackgroundTasks and a webhook that verifies the signature.