IRS filing season is open. File your IRS for free (early access) →
descodify
Invoicing

Descodify API Quickstart: Authenticate, Create a Customer, Issue an Invoice

How to authenticate against the Descodify API, create a customer, and issue an AT-certified invoice: idempotency, rate limits, and the error envelope explained with real request/response examples.

About 5 minutes.

Last updated:

Say you're running your own storefront, or a back-office spreadsheet-replacement, and every sale needs a certified Portuguese invoice on the other end. You don't want to open the Descodify app and re-type each one. This is the path from an API key to your first issued invoice.

Step 1: Create a key

In Settings → Developers, the account owner creates an API key. Pick the scopes it needs: customers:read, customers:write, and the matching pair for products and invoices. A write scope always includes read on the same resource, so invoices:write is enough to both create and fetch invoices.

The secret is shown once, in the form dsc_live_<hex>. Copy it somewhere safe. There's no way to see it again, only to revoke the key and create a new one.

Every request sends it as a bearer token:

Authorization: Bearer dsc_live_1a2b3c4d...

Get this wrong (missing header, expired key, revoked key) and you get a 401 before anything else runs:

{
  "error": {
    "type": "invalid_api_key",
    "message": "The API key is invalid, revoked, or expired."
  }
}

Step 2: Create a customer

A minimal customer only needs three fields: customerType, name, and country. Everything else, email, address, VAT number, defaults to null if you leave it out.

curl https://descodify.pt/api/v1/customers \
  -H "Authorization: Bearer dsc_live_1a2b3c4d..." \
  -H "Content-Type: application/json" \
  -d '{
    "customerType": "business",
    "name": "Acme Lda",
    "country": "PT",
    "vatNumber": "PT513445377"
  }'

The response is the created customer, with an id you'll use as customerId on the invoice.

Step 3: Issue the invoice

The headline path for an e-commerce or back-office integration is create-and-issue in a single call: send "action": "issue" on the create request, and Descodify creates the draft, then runs it through the exact same certified path the app itself uses (series validation, ATCUD, digital signature) before handing back the issued invoice.

This call requires an Idempotency-Key header. Generate a fresh UUID per logical invoice, not per HTTP attempt, so a network retry replays the stored response instead of minting a second certified document.

curl https://descodify.pt/api/v1/invoices \
  -H "Authorization: Bearer dsc_live_1a2b3c4d..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f2c6b6a-2f0e-4b0a-9c0a-000000000001" \
  -d '{
    "invoiceType": "invoice",
    "customerId": "cust_9f8e7d6c",
    "action": "issue",
    "items": [
      {
        "description": "Consulting, June 2026",
        "quantity": 1,
        "unitPrice": 50000,
        "vatRate": 23,
        "itemType": "services"
      }
    ]
  }'

unitPrice is in cents (50000 = €500.00), vatRate is a whole percent (23 = 23%). invoiceType picks the document type: invoice (FT), invoice_receipt (FR), simplified (FS), credit_note (NC), debit_note (ND), receipt (RG), or receipt_vat_cash (RC).

A successful response is a 201 with the issued invoice: sequential invoiceNumber, atcud, qrCodeData, a certification hash, and atCommStatus: "pending". AT communication runs asynchronously, the same as when you issue from the app, so poll GET /api/v1/invoices/{id} (or check back later) to see it move to "accepted" once AT has confirmed it.

If the draft can't be issued

Validation problems (a missing required field, a rule the invoice fails) come back as a 422 with the draft preserved, so you don't have to start over:

{
  "error": {
    "type": "invoice_not_issuable",
    "message": "...",
    "details": { "draft_id": "inv_1a2b3c4d" }
  }
}

Fix whatever the message points at (often reconnecting AT, or a missing customer field the invoice type requires), then call POST /api/v1/invoices/{draft_id}/issue with the same Idempotency-Key to retry the exact same logical invoice.

Two-step alternative: draft, then issue separately

If your integration wants a review step between creating and issuing, drop "action": "issue" from the create call. That returns a plain draft (status: "draft", no invoiceNumber yet). When you're ready, issue it with:

curl -X POST https://descodify.pt/api/v1/invoices/inv_1a2b3c4d/issue \
  -H "Authorization: Bearer dsc_live_1a2b3c4d..." \
  -H "Idempotency-Key: 6f2c6b6a-2f0e-4b0a-9c0a-000000000002"

Same Idempotency-Key requirement, same certified path, same response shape.

Rate limits and fair usage

Every call is rate-limited per key under a fair-usage policy. Go over it and you get a 429:

{
  "error": {
    "type": "rate_limited",
    "message": "Rate limit exceeded. Retry after the indicated interval."
  }
}

with a Retry-After header (in seconds) telling you how long to back off. Honor it: pause for that many seconds, then retry. There's no remaining-quota header, so a well-behaved client backs off on the 429 and paces itself rather than hammering.

Certified invoice issuance additionally has a daily quota (creating drafts and reading data don't count against it). If you hit it, that specific call returns a 429 with a distinct type:

{
  "error": {
    "type": "daily_quota_exceeded",
    "message": "You've reached today's certified-invoice limit. Apply for a higher threshold: mailto:hello@descodify.pt."
  }
}

The Retry-After on that response tells you when the daily window resets. If your volume needs a higher issuance threshold, email hello@descodify.pt and we'll raise it.

The error envelope

Every non-2xx response has the same shape:

{
  "error": {
    "type": "insufficient_scope",
    "message": "This key lacks the required scope \"invoices:write\".",
    "details": "optional, present on validation errors"
  }
}

Common type values: invalid_api_key (401), insufficient_scope (403), not_found (404), validation_error (422, details carries the field-level issues), idempotency_key_required (400, missing header on an issue call), idempotency_key_reused (422, same key with a different body), invoice_not_issuable (422, draft preserved), invoice_immutable (409, tried to edit or re-issue an already-issued document), and rate_limited (429).

Common mistakes

  • Forgetting Idempotency-Key on an issue call. Both POST /invoices with "action": "issue" and POST /invoices/{id}/issue reject with idempotency_key_required (400) if it's missing. It's not required on a plain draft create or on customer/product writes, only on issuing.
  • Reusing an Idempotency-Key for a different logical request. The API treats the same key with a different body as idempotency_key_reused (422), not as a new attempt. Generate one key per invoice you intend to issue, and keep sending that same key if you retry it.
  • Assuming a scope covers more than it does. customers:write doesn't touch products or invoices, and invoices:read can't issue anything. Scopes are per-resource and fixed at key creation.
  • Editing or re-issuing an already-issued invoice. Issued documents are immutable (invoice_immutable, 409). To correct one, create a credit note (invoiceType: "credit_note", originalInvoiceId set to the invoice you're correcting) instead.

What's not here yet

There's no sandbox or test mode: every key issues real, certified documents against your organization, so test against a throwaway org rather than production. Webhooks aren't available either, poll GET /invoices/{id} for the atCommStatus transition. Both are worth knowing before you build around gaps that don't exist yet.

Full reference

The complete OpenAPI 3.1 document, every field, every response shape, lives at /api/v1/openapi.json, and the browsable version is on the API reference page. For the positioning, scopes, and rate limits at a glance, see the Developers page.

Related terms