Edge Functions
Usa Constaia en Vercel Edge Functions y Netlify Edge Functions: subida de documentos, webhook con WebCrypto y límites de runtime a revisar.
El SDK de JavaScript funciona en runtimes edge porque solo usa fetch, FormData y WebCrypto. Esta guía cubre:
- Vercel Edge Functions (función con
runtime: "edge"). - Netlify Edge Functions (Deno).
En ambos casos creas dos funciones: POST /api/verify-dni para subir el documento y POST /webhooks/constaia para los eventos firmados.
Revisa los límites de tu plataforma
Los runtimes edge limitan el tamaño del cuerpo de la petición, el tiempo hasta la primera respuesta y la memoria, y esos límites cambian según el plan. Un análisis síncrono puede tardar hasta 30 s y los ficheros pueden pesar hasta 20 MB. Consulta la documentación de tu plataforma antes de ir a producción. Si los límites no alcanzan, usa async: true con webhooks, sube el fichero a un almacenamiento y pasa una URL firmada como fileUrl, o usa una función con runtime de Node.js (por ejemplo, la guía de Next.js).
Instalación
npm i @constaia/sdkEn Netlify, instala también los tipos: npm i -D @netlify/edge-functions.
Variables de entorno
Define CONSTAIA_API_KEY y CONSTAIA_WEBHOOK_SECRET en las variables de entorno del proyecto (panel de Vercel o Netlify), nunca en el código.
- Son solo de servidor: no uses prefijos que las expongan al navegador (
NEXT_PUBLIC_,VITE_…). - En Vercel se leen con
process.env; en Netlify, conNetlify.env.get(). fromPath(de@constaia/sdk/node) no está disponible: en edge no hay sistema de ficheros. Usa elFiledelFormData,{ fileUrl }o{ base64, filename }.
Utilidades compartidas
import {
AuthenticationError,
ConstaiaError,
InsufficientCreditsError,
InvalidRequestError,
PermissionError,
RateLimitError,
type Analysis,
type WebhookEvent,
} from "@constaia/sdk";
// Lo que devuelves al navegador (y lo que pinta el widget): sin los campos extraídos.
export function publicResult(analysis: Analysis) {
const { id, object, status, document, verdict, warnings } = analysis;
return { id, object, status, document, verdict, warnings };
}
const httpError = (status: number, code: string, message: string, retryAfter?: number) =>
Response.json(
{ error: { code, message } },
{ status, headers: retryAfter ? { "Retry-After": String(retryAfter) } : undefined },
);
export function errorResponse(error: unknown): Response {
if (error instanceof InvalidRequestError) {
// Fichero ilegible, formato no admitido, demasiadas páginas…: el usuario puede corregirlo.
return httpError(error.status === 413 ? 413 : 422, error.code ?? "invalid_request", error.message);
}
if (error instanceof RateLimitError) {
const wait = Math.ceil(error.retryAfter ?? 1);
return httpError(429, "rate_limited", "Demasiadas peticiones. Inténtalo en unos segundos.", wait);
}
if (error instanceof InsufficientCreditsError) {
console.error("[constaia] Sin créditos. Recarga en https://app.constaia.com", error.requestId);
return httpError(503, "unavailable", "La validación no está disponible ahora mismo.");
}
if (error instanceof AuthenticationError || error instanceof PermissionError) {
console.error("[constaia] Revisa CONSTAIA_API_KEY", error.code, error.requestId);
return httpError(500, "misconfigured", "Error de configuración del servidor.");
}
if (error instanceof ConstaiaError) {
// APIError (5xx), APIConnectionError, APITimeoutError
console.error("[constaia]", error.name, error.code, error.requestId);
return httpError(502, "upstream_error", "No se ha podido analizar el documento. Inténtalo de nuevo.");
}
console.error(error);
return httpError(500, "internal_error", "Error interno.");
}
// Deduplicación por webhook-id. En producción, una tabla con clave única.
const processed = new Set<string>();
export const alreadyProcessed = (webhookId: string) => processed.has(webhookId);
export const markProcessed = (webhookId: string) => void processed.add(webhookId);
export async function handleEvent(event: WebhookEvent) {
switch (event.type) {
case "analysis.completed":
// event.data es el análisis completo (con fields). Guárdalo por event.data.id.
console.log("analysis.completed", event.data.id, event.data.verdict?.status);
break;
case "analysis.review_required":
console.log("A revisión manual", event.data.id);
break;
case "analysis.failed":
console.warn("analysis.failed", event.data.id, event.data.error?.code);
break;
case "credits.low":
console.warn("Quedan pocos créditos: recarga en https://app.constaia.com");
break;
}
}La deduplicación en memoria no sirve en edge: cada petición puede ejecutarse en una instancia distinta. Sustituye alreadyProcessed y markProcessed por un almacén compartido (tu base de datos, un KV o Netlify Blobs).
Ruta de subida
import { Constaia } from "@constaia/sdk";
import { errorResponse, publicResult } from "../lib/constaia";
export const config = { runtime: "edge" };
const constaia = new Constaia({ apiKey: process.env.CONSTAIA_API_KEY });
// Aquí va tu autenticación: solo usuarios con sesión deberían gastar créditos.
export default async function handler(request: Request): Promise<Response> {
if (request.method !== "POST") return new Response("Method not allowed", { status: 405 });
const form = await request.formData().catch(() => null);
const file = form?.get("file");
if (!(file instanceof File) || file.size === 0) {
return Response.json({ error: { code: "missing_file", message: "Falta el fichero." } }, { status: 400 });
}
const fullName = String(form?.get("full_name") ?? "").trim();
try {
const analysis = await constaia.analyze(file, {
expect: "es_dni",
checks: { notExpired: true, ...(fullName ? { holder: { fullName } } : {}) },
storage: "none",
language: "es",
});
return Response.json(publicResult(analysis), { status: analysis.status === "completed" ? 200 : 202 });
} catch (error) {
return errorResponse(error);
}
}En un proyecto Next.js, el mismo código va en un route handler (app/api/verify-dni/route.ts) con export const runtime = "edge" y export async function POST(request: Request).
El servidor fija expect y checks; nunca los tomes del cliente. Si el análisis sigue en queued o processing tras 30 s, la función responde 202 y el resultado llega por webhook.
Webhook
import { Constaia, WebhookVerificationError } from "@constaia/sdk";
import { alreadyProcessed, handleEvent, markProcessed } from "../../lib/constaia";
export const config = { runtime: "edge" };
const constaia = new Constaia({ apiKey: process.env.CONSTAIA_API_KEY });
export default async function handler(request: Request): Promise<Response> {
if (request.method !== "POST") return new Response("Method not allowed", { status: 405 });
const rawBody = await request.text();
let event;
try {
event = await constaia.webhooks.verify(rawBody, request.headers, process.env.CONSTAIA_WEBHOOK_SECRET!);
} catch (error) {
if (error instanceof WebhookVerificationError) return new Response("Invalid signature", { status: 400 });
throw error;
}
const webhookId = request.headers.get("webhook-id")!;
if (!alreadyProcessed(webhookId)) {
await handleEvent(event);
markProcessed(webhookId);
}
return new Response(null, { status: 204 });
}request.text() devuelve el cuerpo exacto; la verificación usa WebCrypto (HMAC-SHA256), disponible en ambos runtimes. No leas antes request.json().
Qué recibe el navegador
{
"id": "an_01J…",
"object": "analysis",
"status": "completed",
"document": { "type": "es_dni", "label": "DNI (España)", "confidence": 0.97, "side": "both", "country": "ESP" },
"verdict": {
"expected": ["es_dni"],
"match": true,
"status": "valid",
"reasons": [
{ "code": "type_match", "severity": "info", "message": "El documento es DNI (España)." },
{ "code": "not_expired", "severity": "info", "message": "Vigente hasta el 12/03/2031." },
{ "code": "holder", "severity": "info", "message": "Los datos del titular coinciden (full_name)." }
]
},
"warnings": []
}Errores
| Error del SDK | Cuándo | Qué devuelve tu ruta |
|---|---|---|
InvalidRequestError | Fichero vacío, ilegible, formato no admitido, más de 20 MB, demasiadas páginas u opciones mal formadas (400/409/413/415/422) | 422 (o 413) con el mensaje, para que el usuario suba otro fichero |
RateLimitError | Superas las peticiones por segundo de tu clave (429). El SDK ya reintenta dos veces respetando Retry-After | 429 con Retry-After |
InsufficientCreditsError | No quedan créditos (402) | 503 al usuario y una alerta para ti: recarga en el panel |
AuthenticationError, PermissionError | Clave ausente, revocada o incorrecta (401/403) | 500: es un fallo de configuración tuyo, no del usuario |
APIError, APIConnectionError, APITimeoutError | Error 5xx de Constaia o de red, tras agotar los reintentos | 502 y un mensaje de reintentar |
Todas extienden ConstaiaError y exponen status, code y requestId. Registra siempre el requestId: es lo que te pedirá soporte. Detalle de cada código en Errores.
Probar en modo test
Arranca en local con vercel dev (puerto 3000) o netlify dev (puerto 8888) y sube un fichero:
curl -F "file=@dni_valid.jpg" -F "full_name=María García López" http://localhost:3000/api/verify-dni| Fichero | full_name | Resultado |
|---|---|---|
dni_valid.jpg | María García López | valid |
dni_valid.jpg | Juan Pérez | invalid: motivo holder con severidad error |
dni_expired.jpg | (vacío) | invalid: not_expired con el mensaje "Caducado el 15/06/2020." |
blurry.jpg | (vacío) | review: motivo low_quality y warnings: ["blurry", "low_quality"] |
factura.jpg | (vacío) | invalid: type_mismatch, no es un DNI |
En modo test la respuesta depende del nombre del fichero, que debe ser un JPEG, PNG, WEBP, HEIC o PDF real (vale cualquier imagen renombrada). No gasta créditos y livemode es false. Todos los nombres en Modo test.
Para probar el webhook, firma un cuerpo con signWebhook desde un script de Node.js:
import { signWebhook } from "@constaia/sdk";
const body = JSON.stringify({
type: "analysis.completed",
created_at: new Date().toISOString(),
data: { id: "an_test", object: "analysis", status: "completed", verdict: { status: "valid", reasons: [] } },
});
const headers = await signWebhook(body, process.env.CONSTAIA_WEBHOOK_SECRET!);
const res = await fetch("http://localhost:3000/webhooks/constaia", {
method: "POST",
headers: { ...headers, "content-type": "application/json" },
body,
});
console.log(res.status); // 204Checklist de producción
- Autenticación y rate limit por usuario en
/api/verify-dni: cada llamada gasta créditos. - Límites de cuerpo, duración y memoria de tu plan comprobados en la documentación de la plataforma (ficheros de hasta 20 MB, respuestas de hasta 30 s).
- Si no alcanzan:
async: true+ webhook, o subida a almacenamiento yfileUrlfirmado. - Deduplicación del webhook en un almacén compartido, no en memoria.
- Clave
ck_live_solo en las variables de entorno del servidor, sin prefijos públicos. - Alerta ante
InsufficientCreditsErrory el eventocredits.low.