Skip to main content

Quickstart

In five minutes you'll send a real POST /v1/documents, get back an emitted invoice, and read its lifecycle events. We'll use Colombia (DIAN) as the example; the same shape works for MX, EC, and PE with a different country_specific block.

What you need

Bearer tokenAn OAuth2 / JWT access token with the invoicing.document.write scope. Ask your Quetzal admin or see Authentication.
Tenant IDThe tenant slug your account belongs to (e.g. mattilda). Sent in the X-Tenant-Id header.
An issuer profileA registered IssuerProfile for the country you're billing in. The profile holds your fiscal cert and numerator range — without it the request will fail at the gatekeeping stage.
curl and jqWe'll keep it shell-only for the quickstart. Any HTTP client works.

1. Set your environment

export QUETZAL_API="https://quetzal-api.mattilda.io" # or .net for staging
export QUETZAL_TOKEN="eyJhbGciOiJI..." # your bearer token
export TENANT="mattilda"

2. POST your first invoice

The minimum request needs tenant_id, country, document_type, intent, an issuer, a receiver, at least one line_items[] entry, and a country_specific block. SUBMIT tells Quetzal to send the document to the tax authority immediately; use DRAFT to stage it without emitting.

IDEMPOTENCY_KEY=$(uuidgen)

curl -sS -X POST "$QUETZAL_API/v1/documents" \
-H "Authorization: Bearer $QUETZAL_TOKEN" \
-H "X-Tenant-Id: $TENANT" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "mattilda",
"country": "CO",
"document_type": "INVOICE",
"intent": "SUBMIT",
"issuer": {
"tax_id": "901722162",
"tax_id_type": "NIT",
"name": "Mattilda Colombia S.A.S."
},
"receiver": {
"tax_id": "1566021566",
"tax_id_type": "GENERIC_ID",
"name": "Acme S.A.S.",
"email": "ar@acme.example"
},
"line_items": [
{
"description": "Monthly tuition",
"unit_price": { "amount": "1200000.00", "currency": "COP" },
"quantity": 1
}
],
"country_specific": {
"type": "co_dian.v2024_11",
"co_dian.v2024_11": {
"numerator_prefix": "SETT",
"municipality_code": "11001",
"department_code": "11"
}
}
}' | jq

You'll get back a Document resource. Save its id:

DOC_ID=$(curl ... | jq -r '.id')

3. What just happened

The response includes the document's current status. Right after SUBMIT you usually see PENDING_SUBMISSION — Quetzal accepted the request and is calling the tax authority asynchronously. Common terminal states:

StatusMeaning
EMITTEDThe tax authority accepted it. fiscal_id is populated. Done.
FAILEDThe tax authority rejected it (or a non-retryable error). Inspect failure.classification and failure.reason.
CANCELLEDSomeone called POST /v1/documents/{id}/cancel.

Poll the resource until it leaves the PENDING_* family:

curl -sS "$QUETZAL_API/v1/documents/$DOC_ID" \
-H "Authorization: Bearer $QUETZAL_TOKEN" \
-H "X-Tenant-Id: $TENANT" | jq '{ status, fiscal_id, failure }'

For production use cases, don't poll — subscribe to a webhook instead (next section).

4. Read the event timeline

Every state transition is recorded. Pull the full timeline:

curl -sS "$QUETZAL_API/v1/documents/$DOC_ID/events" \
-H "Authorization: Bearer $QUETZAL_TOKEN" \
-H "X-Tenant-Id: $TENANT" | jq '.items'

This is your audit trail. Events are append-only and chronologically ordered.

5. Idempotency — what the Idempotency-Key does

You sent a UUID as Idempotency-Key in step 2. If you resend the exact same request (same body) with the same key, Quetzal returns the original response instead of creating a duplicate document. Always send a fresh UUID for each new intent — never reuse one across distinct documents.

This matters because retries are everywhere: networks drop, your worker restarts, your queue redelivers. Without an idempotency key those retries each create a new document. With one, only the first succeeds and the rest are deduplicated.

Next steps

  • Other countries: swap the country field and the country_specific block. See the API Reference for the per-country shape.
  • Authentication deep-dive: Authentication covers token scopes, header semantics, and common 401 / 403 failure modes.
  • Webhooks: skip polling — register an endpoint and let Quetzal push the state changes to you.
  • Cancel a document: POST /v1/documents/{id}/cancel with a reason.

For the full request shape (all optional fields, country-specific schemas, error responses), browse the API Reference.