Constaia
Integrations

Flask

Validate documents in Flask with the constaia Python SDK, reading the file from request.files and verifying webhooks with request.get_data().

Cette page n'est pas encore traduite dans votre langue. Voici la version anglaise.

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 constaia

Environment variables

.env
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).

app.py
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 "", 204

Notes:

  • upload.stream is a binary file that the SDK reads once; filename=upload.filename keeps 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 202 with status queued or processing and verdict set to None; the result arrives through the webhook.
  • If you use Flask-WTF's CSRFProtect, exempt the webhook with csrf.exempt(constaia_webhook) and send the CSRF token from the widget in its headers attribute.

Form or widget

templates/upload.html
<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

HTTPExceptionTypical codeWhat to do
400, 413, 415, 422InvalidRequestErrorfile_too_large, unsupported_file_type, unreadable_image, invalid_parameterAsk for another file or fix the option
401AuthenticationErrorinvalid_api_keyCheck CONSTAIA_API_KEY
402InsufficientCreditsErrorinsufficient_creditsTop up credits
429RateLimitErrorrate_limitedAlready retried by the SDK; retry_after gives the wait
5xxAPIErrorinternal_error, live_mode_unavailableAlready 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.

tests/test_app.py
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 == 400
CONSTAIA_API_KEY=ck_test_... CONSTAIA_WEBHOOK_SECRET=whsec_... pytest -q

CONSTAIA_WEBHOOK_SECRET must be in whsec_<base64> format. More files in Test mode.

Production checklist

  • MAX_CONTENT_LENGTH of 21 MB in Flask and client_max_body_size 21M; in nginx.
  • Timeouts ≥ 60 s: gunicorn --timeout 90, proxy_read_timeout 90s; and load balancer. Or use async_=True with a webhook, or a Celery task.
  • Upload route with login and rate limiting (for example Flask-Limiter): every live analysis consumes credits.
  • expect and checks fixed in the route; from the browser you accept only the file and language.
  • CONSTAIA_API_KEY=ck_live_... only in production.
  • Webhook with verified signature, CSRF-exempt, deduplicated on webhook-id and answering quickly.
  • Read Storage and privacy to choose storage.

Next steps

Sur cette page