Español
Fiscaza

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.

Get a sandbox key →

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

All endpoints

POST/v1/signupCreates a sandbox account and returns the first key. Unauthenticated.
GET/v1/healthDiagnostics for your taxpayers, in your language. Always 200.
POST/v1/taxpayersRegisters a taxpayer.
GET/v1/taxpayersLists 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}/certificateUploads the taxpayer's certificate (base64 .p12 and passphrase).
POST/v1/taxpayers/{nif}/certificate-linkMints a one-time link so the owner can upload it themselves.
POST/v1/invoicesRegisters an invoice. Returns hash, QR and legend immediately.
GET/v1/invoicesLists invoices with cursor pagination and filters.
GET/v1/invoices/{id}Reads one invoice and the tax agency's verdict.
POST/v1/invoices/{id}/cancelCancels an invoice with a chained cancellation record.
POST/v1/invoices/{id}/subsanarCorrects an invoice AEAT already holds, keeping the same identity.
POST/v1/webhooksRegisters a webhook endpoint. Returns the secret once.
GET/v1/webhooksLists your webhooks and their delivery health.
POST/v1/webhooks/{id}/rotateRotates the signing secret. The old one stops working immediately.
POST/v1/webhooks/{id}/resumeResumes a webhook paused after repeated failures.
POST/v1/api-keysCreates another key in your current environment. Shown once.
GET/v1/api-keysLists 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-…"
  }
}
401unauthorizedKey missing, unknown, revoked or suspended. We do not distinguish.
400invalid_requestValidation failed. Carries fields[] with the field and reason, in your language.
404not_foundDoes not exist, or is not yours. We return 404 for both, deliberately.
409conflictThe operation conflicts with current state (cancelling twice, say).
422idempotency_conflictSame Idempotency-Key with a different body.
429rate_limitedYou have exceeded your key's per-minute limit.
500internalOur fault. Carries a request_id; send it to us.