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-Keyon an issue call. BothPOST /invoiceswith"action": "issue"andPOST /invoices/{id}/issuereject withidempotency_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-Keyfor a different logical request. The API treats the same key with a different body asidempotency_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:writedoesn't touch products or invoices, andinvoices:readcan'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",originalInvoiceIdset 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
AT Code Unique Document: the unique identifier required on every invoice and receipt in Portugal, combining a validation code from the tax authority with a sequential number.
VIESThe EU's free online tool for verifying VAT numbers. Confirms whether a business is VAT-registered and, in Portugal's case, reveals the name and address behind the number.
SAF-TA standardized XML file containing all of a business's transaction data for a period. Certified invoicing software in Portugal generates SAF-T files for audit purposes. The Portuguese version is SAF-T (PT), based on the OECD standard.
NIFPortugal's 9-digit tax identification number, required for every tax-related interaction from opening a bank account to issuing an invoice.