Getting started
From nothing to your first registered invoice, in four calls.
Fiscaza generates the invoicing record, chains it to the previous one, computes the hash and submits it to the Spanish tax agency for you. Your software keeps issuing invoices as before; the only change is that after issuing one, you tell Fiscaza about it.
Everything below works in the sandbox, which sends nothing to the tax agency and has no fiscal validity. Get a key at /registro.
1. Check your key
The key goes in the Authorization header. If this returns 200 you are in. Sandbox keys start with fz_test_ and production keys with fz_live_; the key determines the environment, so you never pass it per call.
curl -X GET https://fiscaza.com/api/v1/health \ -H "Authorization: Bearer fz_test_…"
Response
{
"status": "ok",
"environment": "sandbox",
"checks": [ … ]
}Detail. This is the same diagnostic we run internally, scoped to your own taxpayers. It returns 200 even when it finds problems: your customer's expired certificate is not an outage of ours.
2. Register the taxpayer
A taxpayer is the business issuing the invoices — your customer. You register them once. The tax ID must be valid; we check before accepting it.
curl -X POST https://fiscaza.com/api/v1/taxpayers \
-H "Authorization: Bearer fz_test_…"
\
-H "Content-Type: application/json" \
-d '{
"nif": "B12345674",
"razonSocial": "Bar Ejemplo SL"
}'Response
{
"nif": "B12345674",
"razon_social": "Bar Ejemplo SL",
"environment": "sandbox",
"status": "active",
"credential_mode": "own_cert",
"numero_instalacion": "20260823T120000-000001",
"can_submit": false,
"blocked": "no certificate"
}Detail. can_submit stays false until the taxpayer has a certificate. You can still issue records: they queue and go out as soon as credentials exist. numero_instalacion is generated by Fiscaza and is immutable.
3. Register an invoice
This is the call you will make thousands of times. It returns the hash, the QR and the legend immediately — it does not wait for the tax agency, because your customer is waiting to print a receipt.
curl -X POST https://fiscaza.com/api/v1/invoices \
-H "Authorization: Bearer fz_test_…"
\
-H "Content-Type: application/json" \
-d '{
"nif": "B12345674",
"serie": "A-",
"tipoFactura": "F2",
"fechaExpedicion": "23-08-2026",
"descripcionOperacion": "Consumición",
"cuotaTotal": "2.10",
"importeTotal": "12.10",
"desglose": [
{
"impuesto": "01",
"claveRegimen": "01",
"calificacionOperacion": "S1",
"tipoImpositivo": "21.00",
"baseImponibleOimporteNoSujeto": "10.00",
"cuotaRepercutida": "2.10"
}
]
}'Response
{
"id": "…",
"num_serie": "A-1",
"huella": "02BB1BAC4EFAE737…",
"qr": "https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR?…",
"leyenda": "VERI*FACTU",
"submission": { "status": "queued" }
}Detail. Send an Idempotency-Key header carrying your own identifier for the operation. If your process dies between our response and your commit, retrying with the same key returns the same invoice rather than creating a second one. The same key with a different body returns 422.
4. Read the tax agency's verdict
Submission is asynchronous. Poll the invoice whenever you like, or better, register a webhook and let us tell you.
curl -X GET https://fiscaza.com/api/v1/invoices \ -H "Authorization: Bearer fz_test_…"
Response
{
"data": [
{
"id": "…",
"num_serie": "A-1",
"submission": {
"status": "accepted",
"csv": "A-CSV-FROM-AEAT",
"aeat": null
}
}
],
"next_cursor": null
}Detail. Statuses: queued, sent, accepted, accepted_with_errors, rejected. accepted_with_errors means the tax agency stored it anyway: do NOT resend, correct it with a subsanación record.
What next
- Upload the taxpayer's certificate, or send them a link so they upload it themselves: POST /v1/taxpayers/{nif}/certificate-link returns a one-time URL you can email them.
- Register a webhook with POST /v1/webhooks and we will tell you when something goes wrong, signed with HMAC-SHA256, in your language.
- Cancel an invoice with POST /v1/invoices/{id}/cancel. A cancellation is another chained record, not a deletion: the ledger is immutable by law.
All endpoints
| POST | /v1/signup | Creates a sandbox account and returns the first key. Unauthenticated. |
| GET | /v1/health | Diagnostics for your taxpayers, in your language. Always 200. |
| POST | /v1/taxpayers | Registers a taxpayer. |
| GET | /v1/taxpayers | Lists your taxpayers. |
| GET | /v1/taxpayers/{nif} | Reads a taxpayer, including whether it can submit and why not. |
| DELETE | /v1/taxpayers/{nif} | Closes a taxpayer. Does not delete: the ledger is immutable. |
| POST | /v1/taxpayers/{nif}/certificate | Uploads the taxpayer's certificate (base64 .p12 and passphrase). |
| POST | /v1/taxpayers/{nif}/certificate-link | Mints a one-time link so the owner can upload it themselves. |
| POST | /v1/invoices | Registers an invoice. Returns hash, QR and legend immediately. |
| GET | /v1/invoices | Lists invoices with cursor pagination and filters. |
| GET | /v1/invoices/{id} | Reads one invoice and the tax agency's verdict. |
| POST | /v1/invoices/{id}/cancel | Cancels an invoice with a chained cancellation record. |
| POST | /v1/invoices/{id}/subsanar | Corrects an invoice AEAT already holds, keeping the same identity. |
| POST | /v1/webhooks | Registers a webhook endpoint. Returns the secret once. |
| GET | /v1/webhooks | Lists your webhooks and their delivery health. |
| POST | /v1/webhooks/{id}/rotate | Rotates the signing secret. The old one stops working immediately. |
| POST | /v1/webhooks/{id}/resume | Resumes a webhook paused after repeated failures. |
| POST | /v1/api-keys | Creates another key in your current environment. Shown once. |
| GET | /v1/api-keys | Lists your keys: prefix, label, last use, revocation. Never the key. |
| DELETE | /v1/api-keys/{prefix} | Revokes a key immediately. The only active key cannot revoke itself: create another first. |
Errors
Every error has the same shape: a stable code you can branch on, a message in your language you can show, and a request_id.
{
"error": {
"code": "invalid_request",
"message": "Required fields are missing.",
"fields": [ { "field": "nif", "problem": "required", "detail": "…" } ],
"locale": "en",
"request_id": "76020a7a-…"
}
}| 401 | unauthorized | Key missing, unknown, revoked or suspended. We do not distinguish. |
| 400 | invalid_request | Validation failed. Carries fields[] with the field and reason, in your language. |
| 404 | not_found | Does not exist, or is not yours. We return 404 for both, deliberately. |
| 409 | conflict | The operation conflicts with current state (cancelling twice, say). |
| 422 | idempotency_conflict | Same Idempotency-Key with a different body. |
| 429 | rate_limited | You have exceeded your key's per-minute limit. |
| 500 | internal | Our fault. Carries a request_id; send it to us. |