Quickstart
Your first invoice in 5 minutes — verify your key, grab a series and a tax, create a contact, issue an invoice and send it. One copy-paste sequence against a fact_test_ key.
This guide takes you from a fresh API key to a real (sandbox) invoice in
five steps. Every call below is copy-paste runnable against a
fact_test_ key — no real emails, no AEAT submission, no production
numbering consumed. See Test mode & sandbox for
what "test" switches off.
Prefer an SDK? If you're on TypeScript/Node or PHP, the
official SDKs wrap this whole flow with built-in retries,
idempotency, cursor pagination and typed errors —
npm install @factuarea/sdk or composer require factuarea/factuarea-php.
The raw HTTP steps below work in any language and show exactly what the
SDK sends under the hood.
Run everything with a fact_test_ key first. The API surface is
identical in live and test — when your flow works end-to-end, swap the
prefix to fact_live_ to go to production. Get a test key from
Settings → Developers → API Keys.
Export your key once so every snippet picks it up:
export FACTUAREA_API_KEY="fact_test_3pXnR2VbY7TcA9eFmN5z8KqW"The base URL is https://api.factuarea.com/v1. Authenticate with
Authorization: Bearer (or the equivalent X-API-Key header). Identifiers
are opaque id values (UUID v7); you copy them from one response into the
next.
Verify your key
GET /v1/me introspects the credential. It needs no scope: any valid key can
read itself, and it is how you learn the opaque id of the tax IDs the key
reaches. That id is the {company} segment every company route carries.
curl https://api.factuarea.com/v1/me \
-H "Authorization: Bearer $FACTUAREA_API_KEY"This is the answer of a live key of an account provider, so every block is
filled. With your fact_test_ key, api_key.prefix and api_key.environment
read test.
{
"data": {
"object": "me",
"account": {
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a00",
"name": "Asesoría Martínez",
"environment": "live",
"is_provider": true,
"privileged_scopes": [
"accounts:read",
"accounts:write"
]
},
"api_key": {
"id": "019e81ef-e1d4-7208-a51d-94c2c2a54a84",
"name": "Producción",
"prefix": "fact_live_1OSf9KdP",
"scopes": [
"account:read",
"series:read",
"series:write"
],
"tier": "scale",
"environment": "live",
"created_at": "2026-01-15T09:30:00Z",
"last_used_at": "2026-05-25T18:00:00Z",
"expires_at": null
},
"scope": [
{
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01",
"tax_id": "B12345674",
"name": "Acme Corporation"
},
{
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02",
"tax_id": "B87654323",
"name": "Acme Logística S.L."
}
],
"client_accounts": [
{
"id": "0199a1f0-1c22-7a10-b111-5a5b5c5d5e5f",
"name": "Construcciones Ruiz",
"origin": "granted",
"access_level": "view",
"status": "active",
"paid_by_provider": false,
"environment": "live"
},
{
"id": "0199a1f0-1c22-7a10-b111-6a6b6c6d6e6f",
"name": "Talleres García",
"origin": "provisioned",
"access_level": "operate",
"status": "provisioned",
"paid_by_provider": true,
"environment": "live"
}
],
"client_accounts_visible": true,
"client_accounts_has_more": false
}
}A 200 here means the key is valid. Read the answer like this:
| Field | What it tells you |
|---|---|
account | The account of the key. id is the value of the {account} segment of the account routes, is_provider says whether the account provisions managed accounts for its clients and privileged_scopes lists the privileged scopes Factuarea granted it. |
api_key | The key itself. scopes are the effective scopes: a scope that the plan of the tax IDs it reaches does not allow is not listed. |
scope | The tax IDs the key reaches, each with its id, tax_id and name. It is always enumerated, also when there is exactly one. |
client_accounts | The accounts of your clients that your account reaches through a relation. origin is granted when they gave you access and provisioned when you provisioned them and still manage them. It is filled only when the key carries the privileged accounts:read scope; otherwise it is empty and client_accounts_visible is false. |
If you get 401 invalid_api_key, re-check the value. If a later step fails
with 403 insufficient_scope, the scopes array above tells you what's
missing. The key in this example only carries account:read, series:read and
series:write: every step below names the scope it needs, so compare it with
the scopes of your own key. Accounts and tax IDs
describes every field.
Choose the {company}
The tax IDs a key reaches can be of three kinds, and all of them go in the same
{company} segment:
| The tax ID is… | Where you read its id |
|---|---|
| Your own | In scope. |
| Granted by another account | In scope, once the scope of the key names it. The account that granted it appears in client_accounts with origin: "granted". See granted access. |
| Of a managed account you provisioned | In scope, once the scope of the key names it and your level on the account is not none. The account appears in client_accounts with origin: "provisioned", and companies[] of GET /v1/accounts/{account} lists its tax IDs, with that id as {account}. See managed accounts. |
Whichever kind you choose, the routes below are the same. A tax ID your key does
not reach answers 404 profile_not_found, the same as one that does not exist.
On a tax ID where your level is view you can read, but any write — the contact
and the invoice below, for example — answers 403 company_read_only. With a
fact_test_ key, client_accounts lists only the accounts of the test
environment; Test managed accounts walks
through them.
Reach a tax ID through the company segment explains
the three kinds, their levels and every answer.
Export the tax ID you're going to invoice for — every company route below
carries its opaque id in the path. Here, the first entry of scope:
export COMPANY_ID="01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01"Grab the ids you'll need
An invoice references a series (its numbering) and each line references a tax rate. Both are existing resources you list once and reuse.
A series id
GET /v1/companies/{company}/series returns your numbering series. Pick one whose
document_type is invoice and invoice_kind is complete (the is_default one
is a safe choice). Needs series:read.
curl "https://api.factuarea.com/v1/companies/$COMPANY_ID/series?document_type=invoice" \
-H "Authorization: Bearer $FACTUAREA_API_KEY"{
"data": [
{
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e",
"object": "series",
"code": "F-2026",
"name": "Facturas 2026",
"document_type": "invoice",
"invoice_kind": "complete",
"prefix": "F-2026-",
"next_number": 46,
"year_reset": true,
"is_default": true,
"is_active": true,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-20T11:30:00Z"
}
],
"has_more": false,
"next_cursor": null
}Copy the id (01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e) — that's your
series_id. The field is optional when you create an invoice: without it the
invoice goes to your default series of the right purpose, which is created on
the spot if you have none.
A tax rate id
GET /v1/companies/{company}/taxes returns the tax catalog (global system
taxes + your custom ones). For a standard Spanish invoice line you want the
VAT 21% rate — look for type: "vat" and rate: 21. Needs taxes:read.
curl "https://api.factuarea.com/v1/companies/$COMPANY_ID/taxes?type=vat" \
-H "Authorization: Bearer $FACTUAREA_API_KEY"{
"data": [
{
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f",
"object": "tax",
"name": "IVA general 21%",
"code": "IVA21",
"rate": 21,
"type": "vat",
"applies_to": "both",
"country": "ES",
"is_default": true,
"is_active": true,
"is_system": true
}
],
"has_more": false,
"next_cursor": null
}Copy this id — on an invoice line it goes into tax_rate_id.
tax_rate_id vs tax_rate. On a line you can reference a catalog
rate by tax_rate_id, or skip the lookup and pass the numeric percentage
directly as tax_rate (e.g. "tax_rate": 21). Use one or the other per
line — tax_rate_id keeps the line tied to your catalog, tax_rate is a
quick inline override.
Create a customer contact
Create one contact with name, kind, roles and a valid fiscal identity.
For this Spanish company, use kind: "company", roles: ["customer"] and
tax_id (NIF/CIF/NIE). Needs
contacts:write.
This is a write — send an Idempotency-Key so a retried request never
creates a duplicate contact. See Idempotency.
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/contacts \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Cliente Demo SL",
"kind": "company",
"roles": ["customer"],
"tax_id": "B76543214"
}'{
"data": {
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03",
"object": "contact",
"kind": "company",
"name": "Cliente Demo SL",
"business_name": null,
"commercial_name": null,
"tax_id": "B76543214",
"vat_id": null,
"alternative_id": null,
"address": {
"line1": null,
"line2": null,
"number": null,
"floor": null,
"door": null,
"staircase": null,
"city": null,
"province": null,
"postal_code": null,
"country": "ES"
},
"contact_person": null,
"email": null,
"phone": null,
"mobile": null,
"fax": null,
"website": null,
"billing_emails": [],
"tags": [],
"coordinates": null,
"notes": null,
"metadata": {},
"accumulate_347": true,
"external_id": null,
"roles": [
{
"role": "customer",
"status": "active",
"assigned_at": "2026-08-29T10:00:00+00:00",
"deactivated_at": null
}
],
"bank_accounts": [],
"customer_profile": null,
"supplier_profile": null,
"financial_summary": {
"sales": {
"invoiced_amount": "0.00",
"collected_amount": "0.00",
"receivable_amount": "0.00",
"currency": "EUR"
},
"purchases": {
"purchased_amount": "0.00",
"paid_amount": "0.00",
"payable_amount": "0.00",
"currency": "EUR"
}
},
"latest_activity": null,
"is_archived": false,
"archived_at": null,
"created_at": "2026-08-29T10:00:00+00:00",
"updated_at": "2026-08-29T10:00:00+00:00"
}
}(Optional fields you didn't send come back as null; address is always
an object whose sub-keys are filled in as you provide them.) Copy the
returned id (01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03) — that's the contact
you bill, and the value the invoice takes as client_id.
The document field remains client_id: pass the contact’s canonical id with its active customer role.
Create the invoice
Now combine the three ids. POST /v1/companies/{company}/invoices requires
client_id, series_id, issued_on, due_on and at least one line. Each
line needs
description, quantity and unit_price; add tax_rate_id (or
tax_rate) to apply VAT. Optional per line: discount_percent and
product_id. Needs invoices:write.
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03",
"series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e",
"issued_on": "2026-06-02",
"due_on": "2026-07-02",
"lines": [
{
"description": "Consultoría — junio 2026",
"quantity": 10,
"unit_price": 100,
"tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f",
"discount_percent": 0
}
]
}'The API computes the line and document totals for you and returns the
invoice envelope. A freshly created invoice starts as a draft: no
definitive number yet (is_number_assigned: false).
{
"data": {
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42",
"object": "invoice",
"number": null,
"is_number_assigned": false,
"type": "F1",
"series": {
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e",
"code": "F-2026"
},
"client": {
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03",
"name": "Cliente Demo SL"
},
"status": "draft",
"issued_on": "2026-06-02",
"due_on": "2026-07-02",
"subtotal": "1000.00",
"taxes_total": "210.00",
"total": "1210.00",
"currency": "EUR",
"notes": null,
"lines": [
{
"object": "invoice_line",
"description": "Consultoría — junio 2026",
"product": null,
"quantity": 10,
"unit_price": "100.0000",
"tax_rate": 21,
"discount_percent": 0,
"subtotal": "1000.00",
"taxes": "210.00",
"total": "1210.00"
}
],
"metadata": {},
"operation_regime": "general",
"verifactu_status": "not_applicable",
"is_corrective": false,
"corrective": null,
"payment": null,
"public_link": null,
"substituted_by": null,
"recurring": null,
"paid_recorded_at": null,
"paid_date": null,
"sent_at": null,
"voided_at": null,
"void_reason": null,
"created_at": "2026-06-02T10:31:00Z",
"updated_at": "2026-06-02T10:31:00Z"
}
}Notice the computed money fields: the line subtotal (10 × 100 = 1000),
its taxes (21% of 1000 = 210) and total (1210), aggregated into
the invoice's subtotal / taxes_total / total. Amounts come back as decimal
strings — two decimals in a total ("1000.00") and four in the unit_price of a
line ("100.0000") — and the unit_price you send can be a number or that same
string. Copy the invoice id (01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42) for the
next step.
Get the PDF and send it
With the invoice id you can download its PDF and email it to the client.
GET /v1/companies/{company}/invoices/{id}/pdf streams the binary PDF
(application/pdf). Needs pdfs:read. Save it straight to a file with
curl's -o:
curl https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42/pdf \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-o invoice.pdfPOST /v1/companies/{company}/invoices/{id}/send emails the invoice to the
client. With no body it uses the client's email on file; you can override the
recipient and copy with to, cc, bcc, subject and body. Needs
invoices:send.
Because you're on a fact_test_ key, the email is not delivered to
any real recipient (sandbox effects are off). The call still succeeds and
the invoice transitions as it would in production — perfect for wiring up
your flow without spamming anyone.
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42/send \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"to": "demo@example.com",
"subject": "Tu factura de Acme Corporation"
}'The response is the updated invoice envelope (same shape as above), now
with sent_at populated.
That's it
You verified a key, discovered the ids it needs, created a contact, issued
an invoice with server-computed totals, and sent it — all against an
isolated sandbox. From here, point the same code at a fact_live_ key to
operate on your real company.