Constaia
Concepts

Security

Security with Constaia, how to store and rotate API keys, verify webhooks, protect your upload endpoint and avoid trusting verdicts from the browser.

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

This page gathers what you need to do to keep a Constaia integration secure. What Constaia does with your data is in storage and privacy.

API keys

Server only

A ck_live_ key spends credits and gives access to every analysis in your account. Treat it like a password:

  • Never in browser code, mobile apps, distributed desktop apps or repositories. Anything that reaches the user's device can be extracted.
  • Your frontend or mobile app uploads the file to your backend, and your backend calls Constaia.
  • The JavaScript SDK throws secret_key_in_browser if it detects a ck_ key in a browser, and the widget rejects any attribute that looks like a key (secret_key_rejected).

Coming soon: publishable keys

There are no publishable keys for browser use yet. Today, every key is secret.

Where to store them

In environment variables or a secret manager, never in code:

EnvironmentWhere
LocalA .env file listed in .gitignore.
Vercel, Netlify, Render, Fly…The project's environment variables.
AWSAWS Secrets Manager or SSM Parameter Store.
Google CloudSecret Manager.
KubernetesA Secret mounted as an environment variable.
TeamsA shared secret manager (Doppler, 1Password, Vault…).

The SDKs read CONSTAIA_API_KEY by default:

.env
CONSTAIA_API_KEY=ck_test_...
src/constaia.ts
import { Constaia } from "@constaia/sdk";

export const constaia = new Constaia(); // process.env.CONSTAIA_API_KEY

Constaia stores keys as a hash and shows them only once, when created. If you lose one, create another.

One key per environment

  • Development, CI and staging: ck_test_ keys. They don't spend credits and give deterministic results. See test mode.
  • Production: a ck_live_ key that only exists in production.
  • A different key per application or service, so you can revoke one without affecting the others and know who was using it.

Keys have no per-resource permissions: any key of an account can read and delete that account's analyses. If you need to isolate data (for example, two customers or two products), use separate accounts.

Rotating a key

Keys can coexist, so you can rotate without downtime:

Create a new key in the dashboard → API keys.

Update the environment variable and deploy.

Check that traffic uses the new one (the dashboard shows when each key was last used).

Revoke the old one in the dashboard. From then on it returns 401 invalid_api_key.

Rotate periodically and whenever someone with access leaves the team.

If a key leaks

  1. Revoke it immediately in the dashboard. Don't wait until the new one is deployed: a few minutes of errors are better than a third party spending your credits or reading your analyses.
  2. Create a new one and deploy it.
  3. Review usage (GET /v1/usage or the dashboard) and the account's audit log.
  4. If the key reached a repository, remove it from the history too; revoking it is what invalidates it.
  5. If you see usage you don't recognise, write to hola@constaia.com.

Webhooks

  • Always verify the signature (webhook-signature) with the endpoint's whsec_… secret, over the raw body. The SDKs do it for you.
  • Reject timestamps older than 5 minutes (webhook-timestamp), so an old event cannot be replayed. The SDKs apply that tolerance.
  • Deduplicate on webhook-id: it is stable across retries.
  • https only: the API does not accept http endpoints.
  • Store the secret like the API key: in environment variables.

Verify the signature instead of filtering by IP: these docs don't publish fixed Constaia egress IPs.

src/webhooks.ts
import express from "express";
import { Constaia, WebhookVerificationError } from "@constaia/sdk";

const constaia = new Constaia();
const app = express();
const seen = new Set<string>(); // in production, your database

app.post("/webhooks/constaia", express.raw({ type: "application/json" }), async (req, res) => {
  try {
    const event = await constaia.webhooks.verify(req.body, req.headers, process.env.CONSTAIA_WEBHOOK_SECRET!);
    const id = req.header("webhook-id")!;
    if (!seen.has(id)) {
      seen.add(id);
      // process event.type / event.data in the background
    }
    res.sendStatus(200);
  } catch (err) {
    if (err instanceof WebhookVerificationError) return res.sendStatus(400);
    throw err;
  }
});

app.listen(3000);

More details and PHP and Python examples in webhooks.

file_url

When you send file_url, Constaia downloads the file from its servers:

  • https only.
  • Private and local addresses are not allowed (400 invalid_file_url).
  • 15 s and 20 MB maximum.

If you generate signed URLs from your own storage for Constaia to download, give them a short expiry (a few minutes is enough).

Protect your upload endpoint

The endpoint in your backend that receives the file and calls Constaia spends your credits. Protect it like any paid action:

  • Authentication: only signed-in users (or a single-use token tied to the registration) may use it.
  • Per-user and per-IP limits: for example, a few analyses per minute.
  • CSRF: if you use session cookies, require a CSRF token (the widget supports custom headers through the headers attribute).
  • You decide the options: the server sets expect and checks. Whatever the browser sends is, at most, a hint.
  • Validate size and type before forwarding (20 MB, JPEG/PNG/WEBP/HEIC/PDF) so you don't waste requests.
src/routes/constaia.ts
import express from "express";
import multer from "multer";
import rateLimit from "express-rate-limit";
import { Constaia } from "@constaia/sdk";
import { requireSession } from "./auth"; // your session middleware

const constaia = new Constaia();
const upload = multer({ limits: { fileSize: 20 * 1024 * 1024 } });
export const router = express.Router();

router.post(
  "/api/constaia",
  requireSession,
  rateLimit({ windowMs: 60_000, limit: 5 }),
  upload.single("file"),
  async (req, res) => {
    if (!req.file) return res.status(400).json({ error: { message: "The file is missing." } });
    const analysis = await constaia.analyze(
      { file: req.file.buffer, filename: req.file.originalname },
      { expect: ["es_dni", "es_nie", "passport"], metadata: { user_id: String(req.session.userId) } },
    );
    await saveAnalysisForUser(req.session.userId, analysis.id, analysis.verdict?.status ?? null);
    res.json(analysis);
  },
);

declare function saveAnalysisForUser(userId: string, analysisId: string, status: string | null): Promise<void>;

Don't trust what comes back from the browser

The browser may display the verdict, but it must not be the one deciding. If the final form submits verdict: "valid" or an analysis_id, a user can change it.

  • Store the id and the verdict on your server at the moment you call Constaia (as in the example above).
  • When processing the form, use what you stored, or read the analysis again with GET /v1/analyses/{id} and check it belongs to that user (for example, via metadata).
  • For async results, trust the verified webhook, not the client.

Other measures

  • TLS: the API is only served over HTTPS. Don't disable certificate verification in your HTTP client.
  • Audit log: the account logs sensitive actions (who, from which IP and what action).
  • Logs: log X-Request-Id, not the key or document contents.

Reporting a vulnerability

If you find a security issue in Constaia, write to hola@constaia.com with the details to reproduce it. Please don't disclose it publicly until we have fixed it.

Next steps

Sur cette page