API manual
Last updated: 11 August 2026
The HTTP API gives your own system exactly what you see in the panel: the entities behind your SPV certificate, the documents ANAF has sent you, and their original XML.
It is part of the Platinum and Accountant plans. On other plans a token can still be created, but every request answers 403 rather than 401 — so nobody goes hunting for a credentials bug they do not have.
What you need
- An account on the Platinum or Accountant plan.
- An API token, created under Settings → Integrations. It is shown once, at creation.
- At least one entity with monitoring switched on — the API only answers about the tax ids you monitor.
You need nothing from ANAF. The certificate, the OAuth dance and the SPV polling stay on our side; you only talk to this API.
Base address and version
https://alertespv.ro/api/v1The version has been in the path since the first line. New fields may appear in a response at any time, so read them by name and ignore the ones you do not know. Existing fields do not change meaning and do not disappear without a new version in the path.
Every response is JSON, with one exception: the XML download, which returns the file exactly as ANAF issued it.
Authentication
The token travels in the "Authorization: Bearer …" header. There are no sessions, no cookies and no keys in the query string.
curl -s https://alertespv.ro/api/v1/entities \
-H "Authorization: Bearer spva_your_key" \
-H "Accept: application/json"- A token always starts with "spva_", so a leaked one is recognisable in a log or a bad paste.
- Five active tokens per account at most. Revoking takes effect immediately.
- We do not keep the plaintext — if you lost it, create another and revoke the old one.
- The "last used" column under Settings → Integrations tells you whether a token is still in use before you revoke it.
The token opens every fiscal document on the account. Keep it on a server, in an environment variable — never in versioned code, in a mobile app, or in JavaScript running in a browser.
| Situation | Response | What it means |
|---|---|---|
| Header missing, or without the "spva_" prefix | 401 | Missing or malformed API token. |
| Unknown or revoked token | 401 | Unknown API token. |
| Valid token, plan without the API | 403 | The API is not part of your current plan. |
Limits and pagination
120 requests a minute per account. Over the limit the answer is 429, with a "Retry-After" header saying how many seconds to wait. The limit is far more than a daily sync needs and far less than a loop with a bug costs.
- Paginated lists accept "per_page" between 1 and 100; 100 by default.
- Ask for a page with "page". The number of pages is in "meta.last_page".
- Documents come back newest first, by their creation date at ANAF, then by id.
GET /entities — entities and slots
Returns everything your certificate can see — companies and individuals — together with the state of your plan's slots. An entity is listed even when it is not monitored; monitoring is what spends a slot.
{
"data": [
{
"id": 12,
"cif": "44674942",
"kind": "company",
"name": "EXEMPLU SRL",
"monitored": true,
"enabled": true,
"last_polled_at": "2026-08-11T07:00:12+00:00",
"slot_releasable_from": "2026-09-09"
}
],
"slots": { "used": 1, "limit": 25, "free": 24 }
}| Field | Type | What it is |
|---|---|---|
| id | number | The entity's identifier here. This is what goes into "/entities/{id}/start". |
| cif | string | The tax id or personal id, without the "RO" prefix. |
| kind | string | "company" or "person". |
| name | string or null | The name as ANAF gives it. |
| monitored | boolean | Whether the entity currently holds a slot. |
| enabled | boolean | Whether polling is on. A stopped entity can still hold its slot. |
| last_polled_at | ISO 8601 or null | The last successful poll at ANAF. |
| slot_releasable_from | date or null | The first day the slot can be released, if monitoring stays off. |
POST /entities/{id}/start and /stop
Starting spends a slot that does not come back immediately. The response says outright whether one was taken ("slot_taken"), because a script is exactly the caller most likely to start thirty entities without having read anything first.
curl -s -X POST https://alertespv.ro/api/v1/entities/12/start \
-H "Authorization: Bearer spva_your_key" \
-H "Accept: application/json"- "slot_taken": false means the entity already held a slot — the call was idempotent, you were not charged twice.
- Stopping always answers "slot_released": false. A slot is released only at a subscription-month boundary, at least 30 days after it was taken, and only if monitoring was already off by then.
- An entity belonging to another account answers 403, not 404.
GET /documents — the documents
The documents of the tax ids you monitor, with the summary extracted from the XML. A "cif" that is not among them is silently ignored as a filter — the list stays within what you are allowed to see.
| Parameter | Value | Effect |
|---|---|---|
| cif | string | Narrows to a single monitored tax id. |
| from | date | From the start of that day, by creation date at ANAF. |
| to | date | Up to the end of that day, inclusive. |
| per_page | 1–100 | Rows per page. 100 by default. |
| page | number | The page requested. 1 by default. |
curl -s -G https://alertespv.ro/api/v1/documents \
-H "Authorization: Bearer spva_your_key" \
--data-urlencode "cif=44674942" \
--data-urlencode "from=2026-08-01" \
--data-urlencode "to=2026-08-11" \
--data-urlencode "per_page=50"- "type" and "details" come from ANAF exactly as it words them: "FACTURA PRIMITA", "FACTURA TRIMISA", "ERORI FACTURA". Treat them as text, not as a closed enumeration.
- "summary" is null when the document has not been archived yet, or when the XML is not an invoice we can read (an error report with no content, an unrecognised format).
- "summary.kind" is "invoice", "credit_note", "errors" or "unsupported".
- "counterpartyRole" says which party is the other one: "supplier" on a received invoice, "customer" on one you issued. The same invoice has different roles for the two companies, when both are monitored.
- Amounts are strings, not numbers: "990.08" stays exactly that. Do not put them through a float on the way into an accounting system.
- "anaf_id" plus "cif" identify the document at ANAF; "id" is its identifier here.
GET /documents/{id}/xml — the original file
Returns the XML exactly as ANAF issued it, as a download named "{cif}-{anaf_id}.xml". It is not JSON and it is not modified in any way — it is the copy we archived.
curl -s -OJ https://alertespv.ro/api/v1/documents/8814/xml \
-H "Authorization: Bearer spva_your_key"| Response | When | What to do |
|---|---|---|
| 403 | The document belongs to a tax id you do not monitor. | Check the list from /entities. |
| 404 | The document was never archived. | Nothing to download; "archived": false told you so beforehand. |
| 410 | It was archived, then fell outside the plan's retention. | The row stays, the file does not. Download sooner, or move to a plan with longer retention. |
Response codes
| Code | Meaning |
|---|---|
| 200 | The request succeeded. |
| 401 | Token missing, malformed, unknown or revoked. |
| 403 | The plan does not include the API, or the resource belongs to another account. |
| 404 | The resource does not exist. |
| 409 | Every slot on the plan is taken for the current subscription month. |
| 410 | The file existed and was deleted when retention expired. |
| 422 | Invalid parameters. The body names the field and the reason. |
| 429 | Over 120 requests a minute. Wait as long as "Retry-After" says. |
Recipe: yesterday's documents, with their XML
The usual shape of a daily sync: ask for the documents in a date range, remember what you already took by "id", and download the XML only for the archived ones.
# Yesterday, every monitored tax id, first page.
YESTERDAY=$(date -d yesterday +%F)
curl -s -G https://alertespv.ro/api/v1/documents \
-H "Authorization: Bearer $ALERTESPV_TOKEN" \
--data-urlencode "from=$YESTERDAY" \
--data-urlencode "to=$YESTERDAY" \
--data-urlencode "per_page=100" | jq '.data[] | {id, cif, type, archived}'If you want to hear about documents the moment they appear rather than on the hour, use webhooks. The API is for syncing and for asking; the webhook is for being told.