Constaia
Integrations

n8n

Validate documents from n8n with the HTTP Request node (multipart or JSON), route by verdict and receive Constaia webhooks with signature checks.

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

n8n runs on your server or on n8n Cloud, so the key is stored as a credential and never reaches whoever uploads the document. This guide uses standard nodes: HTTP Request to call the API, Switch or IF to route by verdict, and Webhook + Code to receive events.

Coming soon

The official Constaia node for n8n is in development. Until then, the HTTP Request node covers the whole API.

Prerequisites

  • n8n 1.x (self-hosted or Cloud).
  • A test key ck_test_… from the dashboard. See Authentication.
  • A previous node that provides the document as binary data (Form Trigger with a file field, Gmail or Outlook attachment, Google Drive, Read/Write Files from Disk…) or a downloadable https:// URL of the file.

Analyze a document

Create the credential

In Credentials → Add credential → Header Auth:

FieldValue
NameAuthorization
ValueBearer ck_test_…

Name it, for example, Constaia (test). For production create another one with the ck_live_… key and switch the node's credential once the workflow is tested.

Configure the HTTP Request node

FieldValue
MethodPOST
URLhttps://api.constaia.com/v1/analyze
AuthenticationGeneric Credential Type
Generic Auth TypeHeader Auth → Constaia (test)
Send Bodyon
Body Content TypeForm-Data

Under Body Parameters add two parameters:

Parameter TypeNameValue
n8n Binary FilefileInput Data Field Name: data (the binary property coming from the previous node)
Form Dataoptionsthe options JSON, as text

A typical value for options:

options
{
  "expect": ["es_dni", "es_nie", "passport"],
  "checks": { "min_age_years": 18 },
  "language": "en",
  "metadata": { "source": "n8n" }
}

You can use expressions inside the JSON, for example to check the holder against form data: "holder": { "full_name": "{{ $json.name }}" } inside checks. All options are in POST /v1/analyze and the checks in Checks.

Route by verdict

Add a Switch node (Rules mode) with the value {{ $json.verdict.status }} and three String → is equal to rules: valid, invalid and review. If you only need two outputs, an IF node with the same expression and the condition is equal to valid is enough.

OutputWhat to do
VálidoContinue: store the data from {{ $json.fields }}, approve the request…
No válidoReject and send the user {{ $json.verdict.reasons }} (the message is already in the language you asked for).
RevisarCreate a task for a person (Slack, email, your CRM). See Human review.

Each extracted field is an object: the value is in {{ $json.fields.document_number.value }} and the confidence in {{ $json.fields.document_number.confidence }}. What each status means is in Verdicts.

Handle errors

If the API responds with an error (401, 402, 413, 422, 429…), the node fails. In Settings → On Error choose Continue (using error output) to send those cases to their own branch. The error body follows the format { "error": { "type", "code", "message", "param", "request_id" } }; keep request_id if you need support. The codes are in Errors.

Also account for the 202 response: if the analysis does not finish within 30 s (long PDFs), the API returns the analysis with status: "queued" or "processing" and an empty verdict. Add a condition on {{ $json.status }} equal to completed before the Switch, or use async: true and receive the result by webhook (below).

Alternative: JSON body with file_url

If the document is already at a public or signed https:// URL (for example a temporary link from your storage), you don't need to download it in n8n. In the HTTP Request node use Body Content Type JSON, Specify Body Using JSON and:

Body (JSON)
{
  "file_url": "{{ $json.document_url }}",
  "filename": "{{ $json.document_name }}",
  "options": {
    "expect": "payment_receipt",
    "checks": { "expected_amount": 45, "expected_reference": "INSCRIPCION 123" }
  }
}

Constaia downloads the file with a 20 MB and 15 s limit; it only accepts https and rejects private IPs. You can also send the file as base64 with file_base64 + filename instead of file_url.

Test in test mode

With a ck_test_… key the response depends on the file name, not its content, and no credits are used. The file must be a real JPEG, PNG, WEBP, HEIC or PDF.

  • With multipart, rename the binary before the HTTP Request node (for example, upload a file named dni_valid.jpg, dni_expired.jpg or blurry.jpg).
  • With JSON, pass "filename": "dni_valid.jpg": it replaces the name Constaia derives from the URL.
FileWith expect: "es_dni"
dni_valid.jpgVálido
dni_expired.jpgNo válido reason not_expired with severity error
blurry.jpgRevisar reason low_quality with severity warning

That way you exercise the three Switch branches. All scenarios are in Test mode.

Receive events by webhook

For asynchronous analyses (async: true), batches or balance alerts, Constaia calls a URL of yours.

Create the Webhook node

FieldValue
HTTP MethodPOST
Pathfor example constaia
RespondImmediately
Options → Raw Bodyon

Raw Body is essential: the signature is computed over the exact body, byte for byte, and the node exposes it as binary data (property data; check it in the node output). Responding immediately returns a fast 2xx; Constaia considers the delivery failed if it does not get a 2xx within 15 s.

Register the endpoint

Copy the node's Production URL (activate the workflow) and register it in the dashboard or through the API. The endpoint only receives events of the mode of the key you create it with.

Terminal
curl https://api.constaia.com/v1/webhook-endpoints \
  -H "Authorization: Bearer $CONSTAIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://n8n.example.com/webhook/constaia","events":["analysis.completed","analysis.failed","analysis.review_required"]}'

The response includes secret: "whsec_…" only this once. Store it.

Verify the signature in a Code node

Add a Code node (JavaScript, Run Once for All Items) right after the Webhook:

Code: verify signature
const crypto = require("crypto");

const secret = $env.CONSTAIA_WEBHOOK_SECRET; // whsec_…
const headers = $input.first().json.headers;
const raw = await this.helpers.getBinaryDataBuffer(0, "data");

const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !timestamp || !signatures) throw new Error("Missing signature headers");

if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) throw new Error("Timestamp outside tolerance");

const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = crypto
  .createHmac("sha256", key)
  .update(Buffer.concat([Buffer.from(`${id}.${timestamp}.`), raw]))
  .digest();

const valid = signatures.split(" ").some((entry) => {
  const [version, signature] = entry.split(",");
  if (version !== "v1" || !signature) return false;
  const received = Buffer.from(signature, "base64");
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
if (!valid) throw new Error("Invalid signature");

const event = JSON.parse(raw.toString("utf8"));
return [{ json: { webhook_id: id, ...event } }];

If the signature is not valid, the node throws and the execution stops: nothing after it is processed. Next, a Switch on {{ $json.type }} separates analysis.completed, analysis.review_required, analysis.failed and batch.completed; the analysis is in {{ $json.data }}.

Drop duplicates

Constaia retries failed deliveries for about 3 days, always with the same webhook-id. Store the processed webhook_id values (in your database, or with the Remove Duplicates node in Remove Items Processed in Previous Executions mode if your version has it) and drop repeats.

Code node requirements

On self-hosted n8n, require("crypto") needs the environment variable NODE_FUNCTION_ALLOW_BUILTIN=crypto, and $env needs environment access not to be blocked (N8N_BLOCK_ENV_ACCESS_IN_NODE=false). If your instance allows neither, don't verify the signature in n8n: use only data.id from the event and read the analysis again with an HTTP Request to GET https://api.constaia.com/v1/analyses/{{ $json.data.id }} using your credential. That response comes from the API with your key and is the source of truth.

The full event format, retry policy and signature algorithm are in Webhooks.

Security

  • Never paste a ck_live_… key into a text field, the body JSON or a workflow note: always use the Header Auth credential. When you export or share a workflow, credentials don't travel with it.
  • Use test keys while building the workflow and a separate credential for live.
  • If documents come from a public Form Trigger, decide expect and checks in the workflow, not from what the user sends.
  • By default (storage: "none") Constaia doesn't keep the file. Also check what n8n keeps: the execution history stores binaries and extracted data. Adjust execution retention if you process identity documents. More in Storage and privacy.

Limits

  • 20 MB per file; PDFs up to 30 pages synchronously and up to 200 with async: true.
  • 2 requests per second per key on the free plan (10 on paid). If you process many items, enable Options → Batching on the HTTP Request node (for example, 5 items every 1000 ms). On a 429, honour the Retry-After header. Details in Rate limits.
  • For dozens of documents at once, POST /v1/batches avoids a trickle of requests.

Next steps

Sur cette page