✦ Lançamento · Primeiras 50 empresas: 30% off no Pro para sempre Garantir →
CContaCerta
Começar grátis →

Developer reference · Base URL https://app.contacerta.cc

API Documentation

Submit documents programmatically, have them parsed by ContaCerta's extraction pipeline, and receive the structured results in your own system. Available on the **Pro** and **Business** plans. All endpoints return JSON and live under /api/v1; the ingestion endpoint takes raw file bytes as the request body.

1. Authentication

Every request authenticates with an organization-scoped API key. Create and revoke keys under **Settings → API** (organization admins and owners only). The plaintext key is shown once at creation — store it securely; ContaCerta keeps only a hash and can never show it again.

Send the key as a bearer token:

Authorization: Bearer cc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The header X-Api-Key: cc_live_… is also accepted.

ConditionResponse
Missing, malformed, unknown, or revoked key401 with a generic body (no distinction between cases)
Valid key on a **free** plan403 { "code": "plan_required" }
Valid key on a paid planrequest proceeds

Keys are never logged. Treat a key as a secret equivalent to a password.

2. Rate limits

Every API response carries the caller's current limit state:

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute for your plan
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetEpoch seconds when the window resets

When you exceed the limit the response is 429 with a Retry-After header (seconds). Rejected requests do not consume window slots, so a client that retries on 429 settles at the limit rather than starving.

PlanRequests / minuteConcurrent in-flight documents
Pro6025
Business300100

"In-flight" counts documents still pending or processing. Submitting past that cap returns 429 { "code": "in_flight_limit" } with Retry-After; capacity frees as documents reach a terminal state.

3. Submit a document

POST /api/v1/documents

Sends the raw file bytes as the request body.

Headers

HeaderRequiredNotes
AuthorizationyesBearer cc_live_…
Content-TypeyesOne of the accepted types below
Content-LengthyesRejected if absent or above 25 MB, before the body is read
X-FilenamenoOriginal filename, stored as metadata (sanitized)

**Accepted content types:** application/pdf, image/jpeg, image/png, image/heic, image/heif, message/rfc822 (.eml). The declared type must match the file's actual bytes (except .eml, which has no signature). Maximum size: **25 MB**.

Success — 202 Accepted

{ "documentId": "d41f…", "status": "pending" }

The document now flows through the same pipeline as email and dashboard uploads: it appears in your ContaCerta organization and counts toward your plan allowance.

Errors

StatuscodeCause
401Invalid/missing/revoked key
403plan_requiredFree plan
403READ_ONLY_ENTITYOrganization is read-only on its plan
429in_flight_limitToo many documents pending/processing
429Per-minute rate limit exceeded
411length_requiredContent-Length missing
413payload_too_largeOver 25 MB
415unsupported_typeContent type not accepted
415content_mismatchDeclared type ≠ actual bytes
400empty_body / bad_bodyNo or unreadable body
402Hard ceiling: usage passed 5× the included allowance
500storage_errorTransient storage failure — safe to retry

4. Read a document back

GET /api/v1/documents/{documentId}

Returns the current status and, once parsing finishes, the extracted result.

{
  "documentId": "d41f…",
  "status": "ready",
  "failureReason": null,
  "duplicateStatus": null,
  "result": { "documentType": "invoice", "supplier": { "nif": "…", "name": "…" }, "…": "…" }
}

A document that belongs to another organization returns 404 not_found — identical to a genuinely missing id (no existence leak).

5. Check usage

GET /api/v1/usage
{
  "periodStart": "2026-07-01T00:00:00.000Z",
  "periodEnd": "2026-07-31T23:59:59.999Z",
  "used": 1840,
  "included": 2000,
  "overageCount": 0,
  "plan": "business"
}

6. Webhooks

Configure a single endpoint URL under **Settings → API**. ContaCerta calls it when a submitted document reaches a terminal state.

**Requirements:** the URL must be public **HTTPS** on the default port. Private, loopback, link-local, and cloud-metadata addresses are rejected at save time and re-checked at delivery. Redirects are never followed (a 3xx is treated as a failed delivery).

Events

typeWhenresult present
document.readyParsing succeededyes
document.failedParsing failedno

Payload

{
  "id": "evt_…",
  "type": "document.ready",
  "createdAt": "2026-07-07T12:00:00.000Z",
  "data": {
    "documentId": "d41f…",
    "status": "ready",
    "duplicateStatus": null,
    "result": { "documentType": "invoice", "…": "…" }
  }
}

On document.failed, data carries failureReason and no result.

Verifying the signature

Each delivery includes an X-Webhook-Signature header:

X-Webhook-Signature: t=1751889600,v1=3ba2…hex

Reject deliveries whose t is more than **300 seconds** from your clock to prevent replay. Example (Node.js):

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const candidates = header
    .split(",")
    .filter((p) => p.startsWith("v1="))
    .map((p) => p.slice(3));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) return false;
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return candidates.some((sig) => {
    const a = Buffer.from(sig);
    const b = Buffer.from(expected);
    return a.length === b.length && timingSafeEqual(a, b);
  });
}

Always verify against the **raw request body** before parsing the JSON.

Delivery and retries

Failed deliveries (non-2xx, timeout, or redirect) are retried with bounded exponential backoff. Each attempt is recorded and visible under **Settings → API → Recent deliveries**. Include the payload id in your idempotency handling so a redelivery isn't processed twice.

7. Billing and overage

API documents draw from the same monthly allowance as email and upload (Pro: 200, Business: 2,000). Past the allowance, documents are not blocked — each additional document on any channel bills as metered overage at **€0.10**, added to your Stripe invoice. A hard ceiling stops intake at 5× the allowance as a runaway safeguard.

8. Quickstart

# Submit a PDF
curl -sS -X POST https://app.contacerta.cc/api/v1/documents \
  -H "Authorization: Bearer $CONTACERTA_API_KEY" \
  -H "Content-Type: application/pdf" \
  -H "X-Filename: invoice-2026-07.pdf" \
  --data-binary @invoice-2026-07.pdf

# → {"documentId":"d41f…","status":"pending"}

# Poll for the result (or receive it via webhook)
curl -sS https://app.contacerta.cc/api/v1/documents/d41f… \
  -H "Authorization: Bearer $CONTACERTA_API_KEY"