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 token | An OAuth2 / JWT access token with the invoicing.document.write scope. Ask your Quetzal admin or see Authentication. |
| Tenant ID | The tenant slug your account belongs to (e.g. mattilda). Sent in the X-Tenant-Id header. |
| An issuer profile | A 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 jq | We'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:
| Status | Meaning |
|---|---|
EMITTED | The tax authority accepted it. fiscal_id is populated. Done. |
FAILED | The tax authority rejected it (or a non-retryable error). Inspect failure.classification and failure.reason. |
CANCELLED | Someone 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
countryfield and thecountry_specificblock. 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}/cancelwith a reason.
For the full request shape (all optional fields, country-specific schemas, error responses), browse the API Reference.