Quick start
Register a sale with a single POST request.
curl https://vcr.am/api/v1/sales \
-H "X-API-Key: $VCR_AM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"cashier": { "id": 1 },
"items": [{
"offer": { "externalId": "SKU-001" },
"department": { "id": 1 },
"quantity": "1",
"price": "20000",
"unit": "pc"
}],
"amount": { "nonCash": "20000" },
"buyer": {
"type": "individual",
"receipt": { "email": "buyer@example.com", "language": "en" }
}
}'Start without tax credentials
A sandbox register is issued its registration number the moment you create it — no certificate exchange with the tax authority, nothing to wait for. Same base URL, same endpoints, same key mechanics; only the receipts differ, marked TEST and never filed.
- 1In the dashboard open Virtual Cash Registers, click Create VCR and pick "Sandbox cash register". Create as many as you need.
- 2Issue an API key on that register. A key belongs to one register, so a key taken from a production one keeps answering 403.
- 3Call the same endpoints at the same base URL. When you are ready for real fiscalization, create a production register and swap the key.
Built for production integrations
What your system must provide
Before integrating, make sure each sale carries the data SRC requires.
Authentication
Every request must include your API key in the X-API-Key header. The one exception is GET /exchange-rate, which is public and needs no key.
X-API-Key: your_api_keyGenerate and rotate API keys in your cash register settings.
Keep your API key private
Idempotency
A fiscal receipt cannot be corrected — only refunded and reissued. That makes a duplicate the most expensive mistake this API can make, and a payment webhook firing twice is normal rather than exceptional. Send an Idempotency-Key header on every POST: a repeat of the same request returns the original receipt instead of filing a second one.
Idempotency-Key: 3f6b2c1e-9a84-4d77-9f2a-1c5e0b7d8a63Without the header there is no protection
A UUID generated once per intended fiscal operation and stored next to the order before the first call, then reused for every retry. Not the order number by itself — one order can legitimately produce several fiscal documents. Not a fresh UUID per attempt, which defeats the purpose.
A replayed response is byte-identical to the original and carries the header Idempotent-Replay: true. Check it before treating a response as a newly filed document.
A stateless handler has nothing stored yet, so derive the key instead of generating it. Both deliveries then compute the same key and the second one replays. Do not derive it from the payment provider's event id: a different event about the same order would produce a different key and fiscalize again.
import { v5 as uuidv5 } from "uuid";
const NAMESPACE = "6f1d1a58-0d0c-4a1e-9a1a-2f9a7b3c4d5e";
const key = uuidv5(`${order.id}:sale`, NAMESPACE);
await fetch("https://vcr.am/api/v1/sales", {
method: "POST",
headers: {
"X-API-Key": process.env.VCR_API_KEY,
"Idempotency-Key": key,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});A key stays replayable for 30 days — long enough that a monthly reconciliation re-run replays instead of filing again. Reusing a key with a different body answers 422; reusing one while the first request is still running answers 409, which means wait and retry the same key, not mint a new one. A body the schema rejected spends no key at all: the request never reached the operation, so correct the field and retry with the same key.
Errors
Errors use standard HTTP status codes and a JSON body with the details.
| Status | Name | When it happens |
|---|---|---|
400 | Bad request | Malformed payload or schema validation failed. |
401 | Unauthorized | Missing or invalid API key. |
403 | Forbidden | The register cannot file documents yet: no registration number, or not activated at the tax authority. |
404 | Not found | The requested resource doesn't exist. |
409 | Conflict | The tax authority refused the document on its merits, an inline offer clashes with an existing one, or a request with the same Idempotency-Key is still running. A refusal carries `pending`: the document IS saved, so fix the cause rather than resending. |
422 | Key reused | The same Idempotency-Key arrived with a different body. A new document needs a new key. |
500 | Internal error | Unexpected failure on our side. Retry with exponential backoff. |
502 | Tax authority unreachable | The document IS saved and the response carries `pending`. Read `pending.mayResubmit` first: false means a resend would create a second receipt, so poll `pending.statusUrl` instead. |
Further reading
Continue building
Coming to the WordPress.org Plugin Directory