Retool
Build an internal Retool panel to validate documents with Constaia: a REST resource with the key in configuration variables, file upload and results.
Retool is a good fit for an internal back-office panel: a person uploads a document, picks the expected type and immediately sees the verdict, the reasons and the extracted fields. Queries to REST resources run on Retool's server, so the key never reaches the browser of whoever uses the app.
Prerequisites
- A Retool organization where you can create resources and apps.
- A test key
ck_test_…from the dashboard. See Authentication.
Set up the resource
Store the key as a secret configuration variable
In Settings → Configuration variables create CONSTAIA_API_KEY, mark it as secret and set it to
ck_test_… (and the live value in the production environment, if you use environments). Secret variables can only
be used in resource configuration, not in app code.
Create the REST API resource
In Resources → Create new → REST API:
| Field | Value |
|---|---|
| Name | Constaia |
| Base URL | https://api.constaia.com/v1 |
| Headers | Authorization → Bearer {{ retoolContext.configVars.CONSTAIA_API_KEY }} |
Every query using this resource carries the header without the key showing up in the app.
Build the app
Components
Create an app and add:
fileInput1: a File Input that accepts images and PDF.typeSelect: a Select for the expected type.analyzeButton: a Button labelled "Analyze".fieldsTable: a Table for the extracted fields.reasonsTable: a Table for the verdict reasons.
Load the type catalogue
Create the documentTypes query on the Constaia resource: method GET, path document-types?language=en, run
automatically. In typeSelect use {{ documentTypes.data.data }} as data, {{ item.type }} as value and
{{ item.label }} as label. The endpoint returns the full catalogue, with
each type's fields.
Create the analysis query
Create the analyzeDocument query on the Constaia resource:
| Field | Value |
|---|---|
| Action type | POST |
| URL | analyze |
| Headers | Content-Type → application/json |
| Body | Raw |
| Run behavior | manual (only when the button is clicked) |
With this body:
{{ JSON.stringify({
file_base64: fileInput1.value[0].base64Data,
filename: fileInput1.value[0].name,
options: {
expect: typeSelect.value,
language: "en",
metadata: { reviewer: current_user.email }
}
}) }}fileInput1.value is the list of selected files; each one carries its base64 content and its name. Check the exact
property names in the component's state inspector, as they can vary between Retool versions. On analyzeButton,
add a Click event handler that runs analyzeDocument.
If you work with live keys, turn on the confirmation before running the query: each analysis spends credits.
Show the result
- A Text with the verdict:
{{ analyzeDocument.data?.verdict?.status ?? analyzeDocument.data?.status }}. reasonsTablewith{{ analyzeDocument.data?.verdict?.reasons ?? [] }}(columnscode,severity,message).fieldsTablewith:
{{ Object.entries(analyzeDocument.data?.fields ?? {}).map(([name, f]) => ({
field: name,
value: typeof f.value === "object" ? JSON.stringify(f.value) : f.value,
confidence: f.confidence,
validated: f.validated,
})) }}| Verdict | What it means in the panel |
|---|---|
| Válido | The type matches and there are no warnings or errors |
| No válido | At least one reason has severity error (wrong type, expired, wrong NIF letter…) |
| Revisar | There are warnings (low quality, low confidence…): a person should look at it |
More in Verdicts. If the query fails, Retool marks it as failed and Constaia's error
body (error.code, error.message, error.request_id) shows up in the query result; add a Failure handler that
shows a notification. The codes are in Errors.
Analysis history
Create a recentAnalyses query with GET analyses?limit=20 and show it in another table to see the key's latest
analyses (newest first). To paginate, pass the last item's id as starting_after while has_more is true. If an
analysis returned 202 (queued or processing), call GET analyses/{id} until it is completed. Details in
Analyses.
Test in test mode
With ck_test_… no credits are used and the response depends on the file name sent in filename (the content
must be a real JPEG, PNG, WEBP, HEIC or PDF):
| File | Type | Result |
|---|---|---|
dni_valid.jpg | es_dni | Válido MARÍA GARCÍA LÓPEZ, 12345678Z |
dni_expired.jpg | es_dni | No válido not_expired with severity error |
blurry.jpg | es_dni | Revisar low_quality |
invoice.pdf | invoice | Válido total 121 EUR with the invoice_totals check passed |
More scenarios in Test mode.
Security
- The key lives only in the secret configuration variable and in the resource. Don't write it in queries, transformers or components.
- Restrict who can use the
Constaiaresource and who can edit the app. - Use the test key in the staging environment and the live key in production.
- Constaia deletes the file when it finishes with
storage: "none"(the default). Check what Retool keeps in its query audit logs 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); on a 429 the API sends
Retry-After. See Rate limits.
Next steps
Airtable
Validate Airtable attachments with a "Run a script" automation that sends the attachment URL to Constaia and writes the verdict to the record.
Function calling (OpenAI and Anthropic)
Expose Constaia as a validate_document tool for OpenAI and Anthropic models, run it on your server and return a trimmed result to the model.