FawtaraX API
The FawtaraX REST API lets you create and manage Oman-compliant tax invoices and credit notes programmatically. All endpoints are JSON-over-HTTPS, scoped to the tenant that owns the API key used. The documentation below is generated from the same Zod schemas that validate live requests, so it never drifts from the backend.
https://api.fawtarax.com/v1Importable into Postman, Stoplight, Insomnia, and SDK generators.
Auto-generated from the OpenAPI document. Bump alongside every API release.
Authentication & API keys
The API uses bearer tokens. Every request must include an Authorization header with your key.
1. Get a key
Open the API Keys tab above, click New key, give it a name (e.g. "Production server"), and copy the value. The full key is shaped fwx_<prefix>_<secret> and is shown only once — only the SHA-256 hash and the 8-character prefix are stored server-side. If you lose it, revoke and replace.
2. Send it with every request
curl "https://api.fawtarax.com/v1/invoices" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"3. Rotate or revoke
Create the new key first, deploy it, then revoke the old one from the API Keys tab. Revoked keys immediately return 401 unauthorized.
- Never commit keys to source control or expose them in browser bundles.
- Keys are tenant-scoped — one key cannot read another tenant's data.
- Use separate keys per environment and per integration so you can revoke selectively.
Versioning
The API is versioned in the URL: https://api.fawtarax.com/v1. The current spec version is 1.2.2. We follow semantic versioning at the spec level.
| Field | Type | Description |
|---|---|---|
PATCH (1.0.x) | non-breaking | Bug fixes, doc-only changes, performance improvements. |
MINOR (1.x.0) | non-breaking | New endpoints, new optional fields, new enum values. Safe to upgrade. |
MAJOR (2.0.0) | breaking | New URL prefix (e.g. /v2). The previous major is deprecated for at least 6 months before removal. |
To detect drift programmatically, fetch https://api.fawtarax.com/v1/openapi.json in CI and diff info.version. See the changelog for the full history.
Conventions
- Successful responses return
{ "data": ... }. Errors return{ "error": { "message", "code" } }. - All monetary values use 3-decimal precision (OMR baisa). When
currencyis not OMR you must passexchange_rate_to_omr; OMR totals are computed server-side. - Pagination uses
limit(max 100) andoffset. - Timestamps are ISO 8601 (UTC). Dates are
YYYY-MM-DD. - CORS is enabled (
Access-Control-Allow-Origin: *), but keys must never be exposed in client-side bundles.
Error catalog
Errors use standard HTTP status codes plus a stable machine-readable code that you can branch on safely — messages may change for clarity, codes will not.
idempotency_conflict— Idempotency-Key reused with a different bodyThis `Idempotency-Key` was already used for a request whose body hashes differently.
A key was reused across two genuinely different requests — usually a client that generates one key per retry loop rather than one per logical operation.
Use a fresh key per operation. Replaying the SAME body with the same key returns the first response, which is the behaviour you want.
{
"error": {
"message": "This Idempotency-Key was already used with a different request body",
"code": "idempotency_conflict"
}
}idempotency_in_progress— A request with this key is still runningAnother request carrying this `Idempotency-Key` has been claimed and has not finished.
Two copies of the same request raced, or a previous attempt is still executing.
Retry after a short delay with the same key; you will get the first request's stored response.
{
"error": {
"message": "A request with this Idempotency-Key is still in progress",
"code": "idempotency_in_progress"
}
}insufficient_scope— Insufficient scopeThe API key authenticated, but it does not carry the scope this endpoint requires.
A key minted with a scope list is used against an endpoint outside it — for example an `invoices:read` key calling POST /invoices, or any key without `inbox:read` calling /received-invoices.
Mint a key with the scopes the integration needs. A key created before scopes existed carries an empty list, which is read as full access.
{
"error": {
"message": "This API key does not carry the invoices:write scope",
"code": "insufficient_scope"
}
}subscription_inactive— Subscription is not activeThe tenant's subscription has lapsed. Reads stay open so you can export your data; writes resume when it is renewed.
The plan was cancelled, the trial expired, or a renewal payment failed.
Renew the plan in the portal. Retrying will not help until then.
{
"error": {
"message": "Subscription is not active. Renew the plan to continue.",
"code": "subscription_inactive"
}
}not_issuable— Document cannot be issuedThe request was well-formed but the document does not satisfy the EN 16931 mandatory fields, so it cannot be issued.
A missing buyer country (BR-CL-14), no customer (BR-07), no lines (BR-16), or missing totals. The country comes from the customer record — a customer created without one produces this.
Read `error.message`: it names the fields. Set them on the customer or the request and retry.
{
"error": {
"message": "Invoice cannot be issued: Buyer country (ISO 3166-1 alpha-2) is required (BR-CL-14)",
"code": "not_issuable"
}
}validation_error— Validation failedThe request body or query parameters did not match the expected schema.
Missing required fields, wrong types, value out of range, or `exchange_rate_to_omr` missing for a non-OMR currency.
Inspect `error.message` for the offending path and fix the payload. The OpenAPI document at `/openapi.json` on the API base URL lists every field.
{
"error": {
"message": "Validation failed: lines: Array must contain at least 1 element(s)",
"code": "validation_error"
}
}not_found— Not foundThe requested resource does not exist or does not belong to the authenticated tenant.
Wrong ID, deleted resource, or attempting to access another tenant's data.
Verify the ID and that it was created under the same tenant as the API key.
{
"error": {
"message": "Invoice not found",
"code": "not_found"
}
}invalid_state— Invalid stateThe operation is not allowed in the resource's current state.
Updating or deleting an issued invoice, cancelling a paid invoice, or modifying a non-draft credit note.
Use the appropriate workflow — issue a credit note to amend an invoice, or only mutate resources while still in `draft`.
{
"error": {
"message": "Only draft invoices can be deleted",
"code": "invalid_state"
}
}rate_limited— Too many requestsThe per-key rate limit was exceeded.
Bursting above 10 req/s on GETs or 5 req/s on writes for a single key.
Honour the `Retry-After` response header (seconds) and add exponential backoff. Distribute load across multiple keys for high-volume integrations.
{
"error": {
"message": "Rate limit exceeded",
"code": "rate_limited"
}
}internal_error— Server errorUnexpected internal error.
Transient infrastructure or downstream provider failure.
Safe to retry idempotent (GET/DELETE) requests with backoff. For writes, check whether the resource was created before retrying.
{
"error": {
"message": "Unexpected error",
"code": "server_error"
}
}Rate limits
Limits are applied per API key using a token-bucket algorithm. Exceeding a limit returns HTTP 429 with a Retry-After header (seconds).
| Field | Type | Description |
|---|---|---|
GET endpoints | 10 req/sec | Burst capacity: 60 requests. |
POST / PATCH / DELETE | 5 req/sec | Burst capacity: 30 requests. |
List invoices
/invoicesReturn a paginated list of tax invoices (UBL document type 380) for the authenticated tenant, newest first.
Query parameters
| Field | Type | Description |
|---|---|---|
limit | integer (1–100) | Page size (max 100). |
offset | integer (≥ 0) | Records to skip. |
status | string (≤ 20) | Filter by status. |
Example request
curl -X GET "https://api.fawtarax.com/v1/invoices?limit=20&offset=0" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": [
{
"id": "3f1a7c2e-9b04-4d51-8a6f-2c7e1d0b4a55",
"number": "INV-2026-000412",
"issue_date": "2026-06-08",
"due_date": "2026-07-08",
"status": "issued",
"currency": "OMR",
"subtotal": 250,
"vat_total": 12.5,
"total": 262.5,
"invoice_category": "standard",
"uuid": "9e2f4b60-3c17-5d8a-b04e-7f1a6c9d2e83"
}
]
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Safe to retry — this operation is idempotent.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/invoicescurl -X GET "https://api.fawtarax.com/v1/invoices" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Create invoice
/invoicesIssue a new tax invoice. Totals, OMR amounts and the invoice number are derived server-side; pass a custom number to override.
Request body
| Field | Type | Description |
|---|---|---|
number | string (≤ 50) | Optional custom invoice number |
customer_id | string | null | |
issue_daterequired | string | ISO date YYYY-MM-DD |
due_date | string | null | |
currency | string (3–3) | ISO 4217 currency code |
invoice_category | "standard" | "simplified" | "proforma" | "export" | "reverse_charge" | "exempt" | "zero_rated" | |
exchange_rate_to_omr | number (≥ 0) | Required when currency is not OMR |
notes | string (≤ 2000) | null | |
issue_time | string | null | IBT-168 time of issue, 24-hour HH:MM or HH:MM:SS. Required by ALIGNED-IBRP-016-OM; when omitted the document carries 00:00:00. |
delivery_country | string (2–2) | null | IBT-080 ISO 3166-1 alpha-2. Required on an export (IBR-014-OM). |
payment_means_code | string (≤ 4) | null | UNTDID 4461 payment means, e.g. 30 credit transfer, 10 cash. |
place_of_supply | string (≤ 100) | null | |
buyer_country_code | string (2–2) | null | BR-CL-14; taken from the matched customer when omitted. |
invoice_type_code | "380" | "389" | 380 tax invoice (the default, so an existing caller is unaffected) or 389 self-billed invoice. A credit note (381) or debit note (383) is created through /credit-notes, which requires the original document and a reason those two cannot be valid without. |
self_billing_agreement_ref | string (≤ 200) | null | The agreement the self-billing arrangement rests on. Only meaningful for 389, where the builder emits it as an AdditionalDocumentReference. |
linesrequired | array (1–200) |
Example request
curl -X POST "https://api.fawtarax.com/v1/invoices" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"
\
-H "Content-Type: application/json" \
-d '{ "customer_id": "b2e91d47-6a58-4c03-9f71-0d4a8e5c3b16", "issue_date": "2026-06-08", "issue_time": "14:30:00", "due_date": "2026-07-08", "currency": "OMR", "invoice_type_code": "380", "payment_means_code": "30", "lines": [ { "description": "Consulting services — May 2026", "quantity": 10, "unit_price": 25, "vat_rate": 5, "vat_category_code": "S", "isic_code": "620100", "goods_services_code": "S" } ] }'Responses
{
"data": {
"id": "3f1a7c2e-9b04-4d51-8a6f-2c7e1d0b4a55",
"number": "INV-2026-000412",
"issue_date": "2026-06-08",
"due_date": "2026-07-08",
"status": "issued",
"currency": "OMR",
"subtotal": 250,
"vat_total": 12.5,
"total": 262.5,
"invoice_category": "standard",
"uuid": "9e2f4b60-3c17-5d8a-b04e-7f1a6c9d2e83"
}
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "invalid_state",
"message": "Invoice is already issued and cannot be edited."
}
}{
"error": {
"code": "not_issuable",
"message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Send a stable Idempotency-Key header so retries don't create duplicates.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/invoicescurl -X POST "https://api.fawtarax.com/v1/invoices" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "b2e91d47-6a58-4c03-9f71-0d4a8e5c3b16",
"issue_date": "2026-06-08",
"issue_time": "14:30:00",
"due_date": "2026-07-08",
"currency": "OMR",
"invoice_type_code": "380",
"payment_means_code": "30",
"lines": [
{
"description": "Consulting services — May 2026",
"quantity": 10,
"unit_price": 25,
"vat_rate": 5,
"vat_category_code": "S",
"isic_code": "620100",
"goods_services_code": "S"
}
]
}'Retrieve invoice
/invoices/{id}Fetch a single invoice including all line items.
Example request
curl -X GET "https://api.fawtarax.com/v1/invoices/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": {
"id": "3f1a7c2e-9b04-4d51-8a6f-2c7e1d0b4a55",
"number": "INV-2026-000412",
"issue_date": "2026-06-08",
"due_date": "2026-07-08",
"status": "issued",
"currency": "OMR",
"subtotal": 250,
"vat_total": 12.5,
"total": 262.5,
"invoice_category": "standard",
"uuid": "9e2f4b60-3c17-5d8a-b04e-7f1a6c9d2e83"
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Safe to retry — this operation is idempotent.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/invoices/{id}curl -X GET "https://api.fawtarax.com/v1/invoices/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Update draft invoice
/invoices/{id}Update fields on a draft invoice. Once issued, invoices are immutable — cancel or issue a credit note instead.
Request body
| Field | Type | Description |
|---|---|---|
number | string (≤ 50) | |
customer_id | string | null | |
issue_date | string | |
due_date | string | null | |
currency | string (3–3) | |
invoice_category | "standard" | "simplified" | "proforma" | "export" | "reverse_charge" | "exempt" | "zero_rated" | |
exchange_rate_to_omr | number (≥ 0) | |
notes | string (≤ 2000) | null | |
issue_time | string | null | IBT-168 time of issue, 24-hour HH:MM or HH:MM:SS. Required by ALIGNED-IBRP-016-OM; when omitted the document carries 00:00:00. |
delivery_country | string (2–2) | null | IBT-080 ISO 3166-1 alpha-2. Required on an export (IBR-014-OM). |
payment_means_code | string (≤ 4) | null | UNTDID 4461 payment means, e.g. 30 credit transfer, 10 cash. |
place_of_supply | string (≤ 100) | null | |
buyer_country_code | string (2–2) | null | BR-CL-14; taken from the matched customer when omitted. |
lines | array (1–200) |
Example request
curl -X PATCH "https://api.fawtarax.com/v1/invoices/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"
\
-H "Content-Type: application/json" \
-d '{ "notes": "Updated PO reference: PO-1043", "due_date": "2026-07-22" }'Responses
{
"data": {
"id": "3f1a7c2e-9b04-4d51-8a6f-2c7e1d0b4a55",
"number": "INV-2026-000412",
"issue_date": "2026-06-08",
"due_date": "2026-07-08",
"status": "issued",
"currency": "OMR",
"subtotal": 250,
"vat_total": 12.5,
"total": 262.5,
"invoice_category": "standard",
"uuid": "9e2f4b60-3c17-5d8a-b04e-7f1a6c9d2e83"
}
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "invalid_state",
"message": "Invoice is already issued and cannot be edited."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Before retrying a write, GET the resource (or list with a unique field) to confirm it was created.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/invoices/{id}curl -X PATCH "https://api.fawtarax.com/v1/invoices/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…" \
-H "Content-Type: application/json" \
-d '{
"notes": "Updated PO reference: PO-1043",
"due_date": "2026-07-22"
}'Delete draft invoice
/invoices/{id}Permanently delete a draft invoice and its line items.
Example request
curl -X DELETE "https://api.fawtarax.com/v1/invoices/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "invalid_state",
"message": "Invoice is already issued and cannot be edited."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Safe to retry — this operation is idempotent.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/invoices/{id}curl -X DELETE "https://api.fawtarax.com/v1/invoices/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Cancel invoice
/invoices/{id}/cancelMark an issued invoice as cancelled. Not allowed if paid, partially paid, or delivered via Peppol — issue a credit note instead.
Example request
curl -X POST "https://api.fawtarax.com/v1/invoices/{id}/cancel" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": {
"id": "3f1a7c2e-9b04-4d51-8a6f-2c7e1d0b4a55",
"number": "INV-2026-000412",
"issue_date": "2026-06-08",
"due_date": "2026-07-08",
"status": "issued",
"currency": "OMR",
"subtotal": 250,
"vat_total": 12.5,
"total": 262.5,
"invoice_category": "standard",
"uuid": "9e2f4b60-3c17-5d8a-b04e-7f1a6c9d2e83"
}
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "invalid_state",
"message": "Invoice is already issued and cannot be edited."
}
}{
"error": {
"code": "not_issuable",
"message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Before retrying a write, GET the resource (or list with a unique field) to confirm it was created.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/invoices/{id}/cancelcurl -X POST "https://api.fawtarax.com/v1/invoices/{id}/cancel" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Send invoice to the Peppol network
/invoices/{id}/sendFile an issued invoice with the Tax Authority through the access point. This is the endpoint that makes the API able to FILE rather than only record: before it, every dispatch path required a signed-in browser session and an integrator's documents had to be sent by hand from the portal. The call is synchronous and its result is a transport outcome, not a conformance verdict: `sent` means the access point accepted the document, and the Tax Authority's own decision arrives later on the document's status. Re-sending a document that has already gone returns `already_sent` rather than filing it twice. A `422` means the document itself was refused. When it was refused by the PINT-OM conformance gate the body carries `rules` and `findings` — the schematron rule ids that would have rejected it downstream, so you can fix the document rather than guess.
Example request
curl -X POST "https://api.fawtarax.com/v1/invoices/{id}/send" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": {
"outcome": "sent",
"mode": "live",
"transmission_uuid": "c4a71e38-2b95-4d07-8f16-9e3a5c0b7d24",
"as4_message_id": "urn:uuid:7d2f5a91-c063-4e18-9b45-1a8e6c3f0d72@fawtarax.com"
}
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"message": "The document was refused by the PINT-OM conformance gate.",
"code": "conformance_failed",
"rules": [
"IBR-081-OM",
"ALIGNED-IBRP-016-OM"
],
"findings": [
{
"rule": "IBR-081-OM",
"field": "lines[0].isic_code",
"message": "A full tax invoice must carry the ISIC classification code on every line."
}
]
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Before retrying a write, GET the resource (or list with a unique field) to confirm it was created.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/invoices/{id}/sendcurl -X POST "https://api.fawtarax.com/v1/invoices/{id}/send" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Send credit note to the Peppol network
/credit-notes/{id}/sendFile an issued credit note (381) or debit note (383) with the Tax Authority through the access point — the dispatch half of the /credit-notes resource. Added 2026-08-28 after the first live run showed a credit note created through the API could not be FILED through it: /invoices/{id}/send scopes to the documents /invoices owns (380, 389), so a 381 answered 404 there. Semantics are identical to sending an invoice: synchronous, a transport outcome rather than a conformance verdict, `already_sent` on a repeat, and a `422` that names the refusing rules.
Example request
curl -X POST "https://api.fawtarax.com/v1/credit-notes/{id}/send" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": {
"outcome": "sent",
"mode": "live",
"transmission_uuid": "e18b6d40-3f72-4c95-a027-5d9e1b7a3c86",
"as4_message_id": "urn:uuid:2c9e4b17-8d50-4a63-b1f8-6e0a7d3c5921@fawtarax.com"
}
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"message": "The document was refused by the PINT-OM conformance gate.",
"code": "conformance_failed",
"rules": [
"IBR-032-OM"
],
"findings": [
{
"rule": "IBR-032-OM",
"field": "original_invoice_id",
"message": "A credit note must reference the document it adjusts."
}
]
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Before retrying a write, GET the resource (or list with a unique field) to confirm it was created.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/credit-notes/{id}/sendcurl -X POST "https://api.fawtarax.com/v1/credit-notes/{id}/send" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Get invoice QR payload
/invoices/{id}/qrThe QR payload of an issued invoice. The SERVER composes the payload and the client renders the image — a client must never build its own TLV, because the payload carries the BTOM-002 Invoice UUID and BTOM-004 Seller UUID and has to be byte-identical to what is filed. `format=tlv` returns the Oman QR: Base64 of the Tag-Length-Value byte string defined in Appendix D of the Solution Reference Architecture. It exists only for the shapes that carry a QR — simplified (B2C) and profit-margin invoices — and is a 404 on any other invoice. `format=url` returns the consumer verification URL this platform has always stored. That form is not defined by any OTA publication; prefer `tlv`.
Query parameters
| Field | Type | Description |
|---|---|---|
format | "url" | "tlv" | "both" | Which representation to return. |
Example request
curl -X GET "https://api.fawtarax.com/v1/invoices/{id}/qr?format=url" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": {
"format": "tlv",
"payload": "AQhGYXd0YXJhWAIMT00xMTAwMDAzNTU0",
"url": null,
"tlv": "AQhGYXd0YXJhWAIMT00xMTAwMDAzNTU0",
"invoice_uuid": "9e2f4b60-3c17-5d8a-b04e-7f1a6c9d2e83",
"seller_uuid": "5c8a1f74-6d29-5b03-8e17-4a9f2c6d0b35"
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "invalid_state",
"message": "Invoice is already issued and cannot be edited."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Safe to retry — this operation is idempotent.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/invoices/{id}/qrcurl -X GET "https://api.fawtarax.com/v1/invoices/{id}/qr" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"List credit notes
/credit-notesQuery parameters
| Field | Type | Description |
|---|---|---|
limit | integer (1–100) | Page size (max 100). |
offset | integer (≥ 0) | Records to skip. |
status | string (≤ 20) | Filter by status. |
Example request
curl -X GET "https://api.fawtarax.com/v1/credit-notes?limit=20&offset=0" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": [
{
"id": "8c4d2b19-7e35-4f60-9a12-5b8e3f7c1d02",
"number": "CN-2026-000038",
"issue_date": "2026-06-20",
"status": "issued",
"currency": "OMR",
"subtotal": 50,
"vat_total": 2.5,
"total": 52.5,
"original_invoice_id": "3f1a7c2e-9b04-4d51-8a6f-2c7e1d0b4a55",
"reason_code": "QTY",
"uuid": "1a7d5e93-8b26-5c40-9f31-2e6b0a4d7c18"
}
]
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Safe to retry — this operation is idempotent.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/credit-notescurl -X GET "https://api.fawtarax.com/v1/credit-notes" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Create credit note
/credit-notesIssue a credit note against an existing invoice. If lines are omitted, original invoice lines are mirrored with negative quantities.
Request body
| Field | Type | Description |
|---|---|---|
original_invoice_idrequired | string | The document being adjusted. Becomes the BillingReference (IBR-032-OM). |
reason_coderequired | "CAN" | "VAT" | "VAL" | "QTY" | "OTH" | Adjustment reason, CL-02-OM (closed list): CAN cancellation, VAT VAT correction, VAL value adjustment, QTY quantity adjustment, OTH other (explain in reason_note). |
reason_note | string (≤ 500) | |
document_type_code | "381" | "383" | 381 credit note (the default, so an existing caller is unaffected) or 383 debit note. Both live here because both are adjustments to an earlier document and cannot be valid without the two fields above; a 383 is nonetheless emitted with an Invoice root, because PINT-OM has no DebitNote transaction. |
issue_date | string | |
issue_time | string | Local issue time HH:MM[:SS] (IBT-168, ALIGNED-IBRP-016-OM). |
number | string (≤ 50) | |
notes | string (≤ 2000) | null | |
lines | array (1–200) | Omit to mirror the original invoice lines as negative quantities |
Example request
curl -X POST "https://api.fawtarax.com/v1/credit-notes" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"
\
-H "Content-Type: application/json" \
-d '{ "original_invoice_id": "3f1a7c2e-9b04-4d51-8a6f-2c7e1d0b4a55", "reason_code": "QTY", "reason_note": "Customer returned 2 units", "document_type_code": "381", "issue_date": "2026-06-20", "issue_time": "09:15:00" }'Responses
{
"data": {
"id": "8c4d2b19-7e35-4f60-9a12-5b8e3f7c1d02",
"number": "CN-2026-000038",
"issue_date": "2026-06-20",
"status": "issued",
"currency": "OMR",
"subtotal": 50,
"vat_total": 2.5,
"total": 52.5,
"original_invoice_id": "3f1a7c2e-9b04-4d51-8a6f-2c7e1d0b4a55",
"reason_code": "QTY",
"uuid": "1a7d5e93-8b26-5c40-9f31-2e6b0a4d7c18"
}
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "invalid_state",
"message": "Invoice is already issued and cannot be edited."
}
}{
"error": {
"code": "not_issuable",
"message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Send a stable Idempotency-Key header so retries don't create duplicates.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/credit-notescurl -X POST "https://api.fawtarax.com/v1/credit-notes" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…" \
-H "Content-Type: application/json" \
-d '{
"original_invoice_id": "3f1a7c2e-9b04-4d51-8a6f-2c7e1d0b4a55",
"reason_code": "QTY",
"reason_note": "Customer returned 2 units",
"document_type_code": "381",
"issue_date": "2026-06-20",
"issue_time": "09:15:00"
}'Retrieve credit note
/credit-notes/{id}Example request
curl -X GET "https://api.fawtarax.com/v1/credit-notes/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": {
"id": "8c4d2b19-7e35-4f60-9a12-5b8e3f7c1d02",
"number": "CN-2026-000038",
"issue_date": "2026-06-20",
"status": "issued",
"currency": "OMR",
"subtotal": 50,
"vat_total": 2.5,
"total": 52.5,
"original_invoice_id": "3f1a7c2e-9b04-4d51-8a6f-2c7e1d0b4a55",
"reason_code": "QTY",
"uuid": "1a7d5e93-8b26-5c40-9f31-2e6b0a4d7c18"
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Safe to retry — this operation is idempotent.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/credit-notes/{id}curl -X GET "https://api.fawtarax.com/v1/credit-notes/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Update draft credit note
/credit-notes/{id}Request body
| Field | Type | Description |
|---|---|---|
reason_note | string (≤ 500) | null | |
notes | string (≤ 2000) | null | |
issue_date | string | |
lines | array (1–200) |
Example request
curl -X PATCH "https://api.fawtarax.com/v1/credit-notes/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"
\
-H "Content-Type: application/json" \
-d '{ "reason_note": "Corrected: customer returned 3 units" }'Responses
{
"data": {
"id": "8c4d2b19-7e35-4f60-9a12-5b8e3f7c1d02",
"number": "CN-2026-000038",
"issue_date": "2026-06-20",
"status": "issued",
"currency": "OMR",
"subtotal": 50,
"vat_total": 2.5,
"total": 52.5,
"original_invoice_id": "3f1a7c2e-9b04-4d51-8a6f-2c7e1d0b4a55",
"reason_code": "QTY",
"uuid": "1a7d5e93-8b26-5c40-9f31-2e6b0a4d7c18"
}
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "invalid_state",
"message": "Invoice is already issued and cannot be edited."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Before retrying a write, GET the resource (or list with a unique field) to confirm it was created.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/credit-notes/{id}curl -X PATCH "https://api.fawtarax.com/v1/credit-notes/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…" \
-H "Content-Type: application/json" \
-d '{
"reason_note": "Corrected: customer returned 3 units"
}'Delete draft credit note
/credit-notes/{id}Example request
curl -X DELETE "https://api.fawtarax.com/v1/credit-notes/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "invalid_state",
"message": "Invoice is already issued and cannot be edited."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Safe to retry — this operation is idempotent.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/credit-notes/{id}curl -X DELETE "https://api.fawtarax.com/v1/credit-notes/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Create invoices in bulk
/invoices/batchUp to 50 rows are created synchronously and the response carries a per-row result. Above 50 the work is queued and the response is a 202 with a job id to poll at /import-jobs/{id}. Partial failure is the contract: one bad row fails alone and leaves nothing behind. Charged its item count against a batch-specific rate bucket, and honours Idempotency-Key.
Request body
| Field | Type | Description |
|---|---|---|
invoicesrequired | array (1–20000) | |
status | "draft" | "issued" | |
file_name | string (1–255) | Recorded on the job when the batch is asynchronous |
Example request
curl -X POST "https://api.fawtarax.com/v1/invoices/batch" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"
\
-H "Content-Type: application/json" \
-d '{ "status": "draft", "file_name": "june-2026.csv", "invoices": [ { "external_ref": "ROW-1", "customer_match": { "vat_number": "OM1100003554" }, "issue_date": "2026-08-26", "currency": "OMR", "buyer_country_code": "OM", "invoice_type_code": "380", "lines": [ { "description": "Consulting", "quantity": 10, "unit_price": 25, "vat_rate": 5 } ] } ] }'Responses
{
"data": {
"results": [
{
"external_ref": "ROW-1",
"ok": true,
"invoice_id": "3f1a7c2e-9b04-4d51-8a6f-2c7e1d0b4a55",
"number": "INV-2026-000412"
},
{
"external_ref": "ROW-2",
"ok": false,
"error": "Customer not found and no name supplied"
}
],
"summary": {
"total": 2,
"created": 1,
"failed": 1
}
}
}{
"data": {
"job_id": "d7c30e85-1f42-4a96-b508-6e9c2a17d403",
"total_rows": 120,
"status": "queued",
"poll": "/v1/import-jobs/d7c30e85-1f42-4a96-b508-6e9c2a17d403"
}
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "invalid_state",
"message": "Invoice is already issued and cannot be edited."
}
}{
"error": {
"code": "not_issuable",
"message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Send a stable Idempotency-Key header so retries don't create duplicates.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/invoices/batchcurl -X POST "https://api.fawtarax.com/v1/invoices/batch" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…" \
-H "Content-Type: application/json" \
-d '{
"status": "draft",
"file_name": "june-2026.csv",
"invoices": [
{
"external_ref": "ROW-1",
"customer_match": {
"vat_number": "OM1100003554"
},
"issue_date": "2026-08-26",
"currency": "OMR",
"buyer_country_code": "OM",
"invoice_type_code": "380",
"lines": [
{
"description": "Consulting",
"quantity": 10,
"unit_price": 25,
"vat_rate": 5
}
]
}
]
}'Dry-run a batch
/invoices/batch/validateRuns the same validation the portal's import preview runs and writes nothing. Returns a per-row report and a summary of valid / warnings / invalid.
Request body
| Field | Type | Description |
|---|---|---|
invoicesrequired | array (1–500) |
Example request
curl -X POST "https://api.fawtarax.com/v1/invoices/batch/validate" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"
\
-H "Content-Type: application/json" \
-d '{ "invoices": [ { "external_ref": "ROW-1", "customer_match": { "vat_number": "OM1100003554" }, "issue_date": "2026-08-26", "currency": "OMR", "buyer_country_code": "OM", "lines": [ { "description": "Consulting", "quantity": 10, "unit_price": 25, "vat_rate": 5 } ] } ] }'Responses
{
"data": {
"reports": [
{
"external_ref": "ROW-1",
"number": "INV-PREVIEW-1",
"totals": {
"subtotal": 250,
"vat_total": 12.5,
"total": 262.5
},
"status": "valid",
"errors": 0,
"warnings": 0,
"findings": []
},
{
"external_ref": "ROW-2",
"number": "INV-PREVIEW-2",
"totals": {
"subtotal": 80,
"vat_total": 4,
"total": 84
},
"status": "invalid",
"errors": 1,
"warnings": 0,
"findings": [
{
"rule": "BR-CL-14",
"severity": "error",
"message": "The buyer country code is required."
}
]
}
],
"summary": {
"total": 2,
"valid": 1,
"warnings": 0,
"invalid": 1
}
}
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "invalid_state",
"message": "Invoice is already issued and cannot be edited."
}
}{
"error": {
"code": "not_issuable",
"message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Before retrying a write, GET the resource (or list with a unique field) to confirm it was created.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/invoices/batch/validatecurl -X POST "https://api.fawtarax.com/v1/invoices/batch/validate" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…" \
-H "Content-Type: application/json" \
-d '{
"invoices": [
{
"external_ref": "ROW-1",
"customer_match": {
"vat_number": "OM1100003554"
},
"issue_date": "2026-08-26",
"currency": "OMR",
"buyer_country_code": "OM",
"lines": [
{
"description": "Consulting",
"quantity": 10,
"unit_price": 25,
"vat_rate": 5
}
]
}
]
}'Retrieve an import job
/import-jobs/{id}Progress of an asynchronous batch, with the state of each row so you can reconcile your own external_refs.
Example request
curl -X GET "https://api.fawtarax.com/v1/import-jobs/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": {
"job": {
"id": "d7c30e85-1f42-4a96-b508-6e9c2a17d403",
"status": "completed",
"total_rows": 120,
"processed_rows": 120,
"created_rows": 118,
"failed_rows": 2,
"error_summary": "2 rows failed validation",
"created_at": "2026-08-26T07:41:10.000Z"
},
"rows": [
{
"id": "0f8c3a72-5d19-4b06-8e4f-3a1c7d9b2e50",
"row_index": 1,
"external_ref": "ROW-1",
"state": "created",
"invoice_number": "INV-2026-000412",
"error": null
}
]
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Safe to retry — this operation is idempotent.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/import-jobs/{id}curl -X GET "https://api.fawtarax.com/v1/import-jobs/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"List customers
/customersBuyers belonging to the authenticated tenant, newest first. `vat_number`, `email`, `phone` and `address` are encrypted at rest and returned decrypted; `search` therefore matches on name only.
Query parameters
| Field | Type | Description |
|---|---|---|
limit | integer (1–100) | Page size (max 100). |
offset | integer (≥ 0) | Records to skip. |
search | string (≤ 255) | Case-insensitive match on name. |
Example request
curl -X GET "https://api.fawtarax.com/v1/customers?limit=20&offset=0" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": [
{
"id": "b2e91d47-6a58-4c03-9f71-0d4a8e5c3b16",
"name": "Muscat Trading LLC",
"country_code": "OM",
"vat_number": "OM1100003554",
"email": "ap@muscat-trading.om",
"phone": "+96824000000",
"address": "Way 3011, Al Khuwair, Muscat",
"peppol_scheme": "0248",
"peppol_value": "OM1100003554",
"peppol_reachability": "peppol",
"created_at": "2026-05-02T08:14:22.000Z"
}
]
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Safe to retry — this operation is idempotent.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/customerscurl -X GET "https://api.fawtarax.com/v1/customers" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Create customer
/customersCreate a buyer. `country_code` is what BR-CL-14 requires on every invoice issued to this customer, which is why it defaults to OM rather than being optional. Honours `Idempotency-Key`.
Request body
| Field | Type | Description |
|---|---|---|
namerequired | string (1–255) | |
country_code | any | ISO 3166-1 alpha-2. Required downstream by BR-CL-14, so it defaults to OM here. |
vat_number | string (≤ 64) | null | OM + 10 digits for Oman buyers |
email | any | |
phone | string (≤ 64) | null | |
address | string (≤ 1000) | null | |
address_line2 | string (≤ 255) | null | IBT-036, self-billing only |
address_line3 | string (≤ 255) | null | IBT-162, self-billing only |
city | string (≤ 120) | null | IBT-037, self-billing only |
postal_code | string (≤ 40) | null | IBT-038, self-billing only |
contact_name | string (≤ 255) | null | |
peppol_scheme | string (≤ 4) | null | 4-digit code, 0248 for Oman |
peppol_value | string (≤ 64) | null | The buyer's e-invoicing address |
peppol_reachability | "peppol" | "not_on_network" |
Example request
curl -X POST "https://api.fawtarax.com/v1/customers" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"
\
-H "Content-Type: application/json" \
-d '{ "name": "Muscat Trading LLC", "country_code": "OM", "vat_number": "OM1100003554", "email": "ap@muscat-trading.om", "phone": "+96824000000", "address": "Way 3011, Al Khuwair, Muscat", "peppol_scheme": "0248", "peppol_value": "OM1100003554" }'Responses
{
"data": {
"id": "b2e91d47-6a58-4c03-9f71-0d4a8e5c3b16",
"name": "Muscat Trading LLC",
"country_code": "OM",
"vat_number": "OM1100003554",
"email": "ap@muscat-trading.om",
"phone": "+96824000000",
"address": "Way 3011, Al Khuwair, Muscat",
"peppol_scheme": "0248",
"peppol_value": "OM1100003554",
"peppol_reachability": "peppol",
"created_at": "2026-05-02T08:14:22.000Z"
}
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "invalid_state",
"message": "Invoice is already issued and cannot be edited."
}
}{
"error": {
"code": "not_issuable",
"message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Send a stable Idempotency-Key header so retries don't create duplicates.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/customerscurl -X POST "https://api.fawtarax.com/v1/customers" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Muscat Trading LLC",
"country_code": "OM",
"vat_number": "OM1100003554",
"email": "ap@muscat-trading.om",
"phone": "+96824000000",
"address": "Way 3011, Al Khuwair, Muscat",
"peppol_scheme": "0248",
"peppol_value": "OM1100003554"
}'Retrieve customer
/customers/{id}Example request
curl -X GET "https://api.fawtarax.com/v1/customers/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": {
"id": "b2e91d47-6a58-4c03-9f71-0d4a8e5c3b16",
"name": "Muscat Trading LLC",
"country_code": "OM",
"vat_number": "OM1100003554",
"email": "ap@muscat-trading.om",
"phone": "+96824000000",
"address": "Way 3011, Al Khuwair, Muscat",
"peppol_scheme": "0248",
"peppol_value": "OM1100003554",
"peppol_reachability": "peppol",
"created_at": "2026-05-02T08:14:22.000Z"
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Safe to retry — this operation is idempotent.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/customers/{id}curl -X GET "https://api.fawtarax.com/v1/customers/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Update customer
/customers/{id}Send the whole customer, not a sparse patch: every PII column is re-encrypted on write, so omitted fields are cleared rather than kept.
Request body
| Field | Type | Description |
|---|---|---|
namerequired | string (1–255) | |
country_code | any | ISO 3166-1 alpha-2. Required downstream by BR-CL-14, so it defaults to OM here. |
vat_number | string (≤ 64) | null | OM + 10 digits for Oman buyers |
email | any | |
phone | string (≤ 64) | null | |
address | string (≤ 1000) | null | |
address_line2 | string (≤ 255) | null | IBT-036, self-billing only |
address_line3 | string (≤ 255) | null | IBT-162, self-billing only |
city | string (≤ 120) | null | IBT-037, self-billing only |
postal_code | string (≤ 40) | null | IBT-038, self-billing only |
contact_name | string (≤ 255) | null | |
peppol_scheme | string (≤ 4) | null | 4-digit code, 0248 for Oman |
peppol_value | string (≤ 64) | null | The buyer's e-invoicing address |
peppol_reachability | "peppol" | "not_on_network" |
Example request
curl -X PATCH "https://api.fawtarax.com/v1/customers/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"
\
-H "Content-Type: application/json" \
-d '{ "name": "Muscat Trading LLC", "country_code": "OM", "vat_number": "OM1100003554", "email": "ap@muscat-trading.om", "phone": "+96824000000", "address": "Way 3011, Al Khuwair, Muscat", "peppol_scheme": "0248", "peppol_value": "OM1100003554" }'Responses
{
"data": {
"id": "b2e91d47-6a58-4c03-9f71-0d4a8e5c3b16",
"name": "Muscat Trading LLC",
"country_code": "OM",
"vat_number": "OM1100003554",
"email": "ap@muscat-trading.om",
"phone": "+96824000000",
"address": "Way 3011, Al Khuwair, Muscat",
"peppol_scheme": "0248",
"peppol_value": "OM1100003554",
"peppol_reachability": "peppol",
"created_at": "2026-05-02T08:14:22.000Z"
}
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Before retrying a write, GET the resource (or list with a unique field) to confirm it was created.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/customers/{id}curl -X PATCH "https://api.fawtarax.com/v1/customers/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Muscat Trading LLC",
"country_code": "OM",
"vat_number": "OM1100003554",
"email": "ap@muscat-trading.om",
"phone": "+96824000000",
"address": "Way 3011, Al Khuwair, Muscat",
"peppol_scheme": "0248",
"peppol_value": "OM1100003554"
}'List received documents
/received-invoicesDocuments this tenant received over Peppol, newest first, each with its AP approval state. `status` filters on the approval, which lives in another table and is applied after the page is fetched — a filtered page can return fewer rows than `limit`.
Query parameters
| Field | Type | Description |
|---|---|---|
limit | integer (1–100) | Page size (max 100). |
offset | integer (≥ 0) | Records to skip. |
status | "pending" | "approved" | "rejected" | "on_hold" | Filter by approval state. |
Example request
curl -X GET "https://api.fawtarax.com/v1/received-invoices?limit=20&offset=0&status=pending" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": [
{
"id": "6d9b0f38-4c71-4e25-a83b-1f5c7e2a9d04",
"invoice_number": "SUP-2026-1187",
"invoice_type_code": "380",
"seller_name": "Gulf Office Supplies LLC",
"seller_vat": "OM1100007781",
"buyer_vat": "OM1100003554",
"issue_date": "2026-06-01",
"currency": "OMR",
"subtotal": 480,
"total": 504,
"status": "received",
"received_at": "2026-06-01T11:02:47.000Z",
"source": "peppol",
"document_type": "invoice",
"peppol_message_id": "urn:uuid:5b3f1c07-2a94-4d18-b6e0-8c7a2f5d9134",
"sender_participant_id": "OM1100007781",
"sender_scheme": "0248",
"dispute_status": "none",
"dispute_reason_code": null,
"mlr_status": "sent",
"mlr_sent_at": "2026-06-01T11:03:05.000Z",
"reverse_charge": false,
"is_import": false,
"approval": {
"status": "pending",
"comment": null,
"decided_at": null,
"decided_by_user": null
}
}
]
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Safe to retry — this operation is idempotent.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/received-invoicescurl -X GET "https://api.fawtarax.com/v1/received-invoices" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Retrieve received document
/received-invoices/{id}Example request
curl -X GET "https://api.fawtarax.com/v1/received-invoices/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": {
"id": "6d9b0f38-4c71-4e25-a83b-1f5c7e2a9d04",
"invoice_number": "SUP-2026-1187",
"invoice_type_code": "380",
"seller_name": "Gulf Office Supplies LLC",
"seller_vat": "OM1100007781",
"buyer_vat": "OM1100003554",
"issue_date": "2026-06-01",
"currency": "OMR",
"subtotal": 480,
"total": 504,
"status": "received",
"received_at": "2026-06-01T11:02:47.000Z",
"source": "peppol",
"document_type": "invoice",
"peppol_message_id": "urn:uuid:5b3f1c07-2a94-4d18-b6e0-8c7a2f5d9134",
"sender_participant_id": "OM1100007781",
"sender_scheme": "0248",
"dispute_status": "none",
"dispute_reason_code": null,
"mlr_status": "sent",
"mlr_sent_at": "2026-06-01T11:03:05.000Z",
"reverse_charge": false,
"is_import": false,
"approval": {
"status": "pending",
"comment": null,
"decided_at": null,
"decided_by_user": null
}
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Safe to retry — this operation is idempotent.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/received-invoices/{id}curl -X GET "https://api.fawtarax.com/v1/received-invoices/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Approve, reject or hold a received document
/received-invoices/{id}/decisionRecord an AP approval decision. One decision exists per document and a second call replaces it. This is not the MLR — the message layer response is a send on the tenant's behalf and is not exposed under `inbox:write`.
Request body
| Field | Type | Description |
|---|---|---|
decisionrequired | "pending" | "approved" | "rejected" | "on_hold" | `pending` clears a previous decision |
comment | string (≤ 1000) | null |
Example request
curl -X POST "https://api.fawtarax.com/v1/received-invoices/{id}/decision" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"
\
-H "Content-Type: application/json" \
-d '{ "decision": "approved", "comment": "Matched PO-1043" }'Responses
{
"data": {
"id": "6d9b0f38-4c71-4e25-a83b-1f5c7e2a9d04",
"invoice_number": "SUP-2026-1187",
"invoice_type_code": "380",
"seller_name": "Gulf Office Supplies LLC",
"seller_vat": "OM1100007781",
"buyer_vat": "OM1100003554",
"issue_date": "2026-06-01",
"currency": "OMR",
"subtotal": 480,
"total": 504,
"status": "received",
"received_at": "2026-06-01T11:02:47.000Z",
"source": "peppol",
"document_type": "invoice",
"peppol_message_id": "urn:uuid:5b3f1c07-2a94-4d18-b6e0-8c7a2f5d9134",
"sender_participant_id": "OM1100007781",
"sender_scheme": "0248",
"dispute_status": "none",
"dispute_reason_code": null,
"mlr_status": "sent",
"mlr_sent_at": "2026-06-01T11:03:05.000Z",
"reverse_charge": false,
"is_import": false,
"approval": {
"status": "pending",
"comment": null,
"decided_at": null,
"decided_by_user": null
}
}
}{
"error": {
"code": "validation_error",
"message": "Request body failed validation: lines must contain at least 1 element."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}{
"error": {
"code": "subscription_inactive",
"message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
}
}{
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the invoices:write scope."
}
}{
"error": {
"code": "not_found",
"message": "Invoice not found."
}
}{
"error": {
"code": "invalid_state",
"message": "Invoice is already issued and cannot be edited."
}
}{
"error": {
"code": "not_issuable",
"message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 1 second."
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong. Reference: req_8f2c14a9."
}
}5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
On 429 honour Retry-After; on 5xx use exponential backoff starting at 250 ms, capped at 8 s, with full jitter.
Send a stable Idempotency-Key header so retries don't create duplicates.
Try it
Paste a key from the API Keys tab to enable signed requests.
https://api.fawtarax.com/v1/received-invoices/{id}/decisioncurl -X POST "https://api.fawtarax.com/v1/received-invoices/{id}/decision" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…" \
-H "Content-Type: application/json" \
-d '{
"decision": "approved",
"comment": "Matched PO-1043"
}'Changelog
Every published change to the API, newest first.
- v1.2.2fixed2026-08-29The reference documents what the API actually does
Nothing about a request or a response changed here. What changed is that the documentation stopped disagreeing with the API in four places. Three endpoints published on 2026-08-27 were in /v1/openapi.json, in the Postman collection and in both SDKs, and missing from the reference page: POST /v1/invoices/{id}/send, POST /v1/credit-notes/{id}/send and GET /v1/invoices/{id}/qr. If you evaluated this API from the reference page since then, you could not see that it can file a document at all. The page is now generated from the OpenAPI document, so it cannot fall behind it again. Idempotency-Key is now declared on the five operations that honour it — POST /v1/invoices, /v1/credit-notes, /v1/customers, /v1/invoices/batch and /v1/received-invoices/{id}/decision — so the generated SDKs and the Postman collection can send it. It is deliberately NOT declared on the two /send calls: those are made safe to retry by returning `already_sent` from the document's own state, not by a key, and the page previously implied otherwise. Two published claims were wrong and are withdrawn. X-RateLimit-Reset was listed as a response header on every endpoint; it has never been sent — X-RateLimit-Limit and X-RateLimit-Remaining are, on every authenticated success as well as the 429, and they are now in the document where a client can find them. And the page stated that every POST and PATCH accepts an idempotency key; no PATCH does. One response body was under-documented: PATCH /v1/credit-notes/{id} returns the updated credit note in the usual `{ data: … }` envelope and the document described a 200 with no body, so a generated client typed that call's return as nothing. Regenerate your SDK if you use that call. Every schema now carries a worked example, and every example is validated against the schema it illustrates on each build. One had rotted: the credit-note sample showed `"reason_code": "RETURN"`, which stopped being accepted when CL-02-OM closed that field to CAN, VAT, VAL, QTY and OTH on 2026-08-28.
- v1.2.1fixed2026-08-28Customer writes refuse fields they will not apply
POST /v1/customers and PATCH /v1/customers/{id} now return 400 for any field the customer schema does not know, instead of silently ignoring it and returning 200 — the same strictness every other write body on this surface already had. A PATCH body may still echo the two read-only fields the GET representation carries: `created_at` is discarded, and `id` is accepted only when it equals the id in the URL (a different id is a 400, because it asks to update a row the URL does not address). If a previously-accepted call starts failing with `Unrecognized key(s)`, the named fields were never being applied.
- v1.2.0added2026-08-27The API can file a document, and can request all four document types
POST /v1/invoices/{id}/send files an issued invoice with the Tax Authority through the access point. Until now nothing under /v1 could dispatch anything: the API created and edited rows, and filing was a manual step in the portal. Scope invoices:write, write rate limit. The 200 carries the transport outcome — `sent` means the access point accepted the document, not that the authority approved it, which arrives later on the document's status; re-sending something already sent returns `already_sent` rather than filing it twice. A 422 means the document was refused, and when the PINT-OM conformance gate is what refused it the body carries `rules` and `findings` — the schematron rule ids that would have rejected it downstream, so you can fix the document instead of guessing. Document types: POST /v1/invoices takes `invoice_type_code` (380 tax invoice, 389 self-billed) and POST /v1/credit-notes takes `document_type_code` (381 credit note, 383 debit note). Both default to what the resource produced before, so no existing call changes. The lists now return them too: a 383 or a 389 created through the bulk importer used to be readable through neither resource. Header facts the document needs are requestable at last — `issue_time` above all, whose absence meant every document carried 00:00:00. One behaviour change to know about: when POST /v1/credit-notes mirrors the original invoice's lines (because you sent none), the mirrored quantities are now POSITIVE. Supplying lines yourself always required positive quantities — the type code expresses the direction, not the sign — so the mirror was contradicting the schema this API publishes. It would also have inverted a debit note. Additive otherwise, so MINOR.
- v1.1.0changed2026-08-27The API has moved to its own hostname, and the base URL is shorter
The API is now served at https://api-test.fawtarax.com/v1 (test environment) and nowhere else: /api/public/v1 on the portal hostnames is being retired, and every path on the API hostname that is not the API returns 404. Change your base URL — everything after it is unchanged, so /v1/invoices is the old /api/public/v1/invoices, byte for byte, with the same auth, the same scopes and the same envelope. The generated OpenAPI document, the Postman collection and both SDKs already carry the new base; regenerate them rather than editing the old copies, because the base URL is compiled into both SDK constructors as a default. Why the URL and not just the host: this is the address that will be published to tenants, and under the policy above a path change after publication is MAJOR — it would need a /v2 prefix and a six-month window. Better to spend the change now, in test, than to be unable to spend it later. Not MAJOR itself: no request, response or resource changed, only where they are served. POST /webhooks/test moved too — to /api/public/portal/webhook-test on the portal host, because it is a signed-in-portal helper rather than a key-authenticated resource, and it has never been in the OpenAPI document.
- v1.1.0added2026-08-25Customers and inbox
New: GET/POST /customers and GET/PATCH /customers/{id} (scopes customers:read / customers:write) — a buyer resource, so an invoice's customer_id can be created through the API; and GET /received-invoices, GET /received-invoices/{id}, POST /received-invoices/{id}/decision (scopes inbox:read / inbox:write) — the documents your tenant received over Peppol, with their AP approval state. Additive, so MINOR.
- v1.1.0removed2026-08-25Outbound webhooks were never shipped, and this changelog said they were
Two earlier entries described outbound webhooks (invoice.issued/paid/cancelled, credit_note.created, payment.failed) and their HMAC-SHA256 signature scheme. No such delivery exists: there is no subscription table, no delivery machinery and no registration endpoint, and there never was one — the only webhook code on this surface is POST /webhooks/test, which calls a URL you supply. The claims are withdrawn rather than left standing while the feature is built; the entry stays here so a client that read them knows they were wrong. If you built against them, nothing you wrote was ever called.
- v1.0.0added2026-06-08Public v1 launch
Stable release of Invoices and Credit Notes CRUD, invoice cancellation, per-key rate limits, and the OpenAPI 3.1 document at /api/public/v1/openapi.json.
- v0.9.0added2026-05-22Credit note line override
POST /credit-notes now accepts an explicit `lines` array; omitting it still mirrors the original invoice lines as negative quantities.
- v0.8.0changed2026-05-10Tightened invoice validation
`exchange_rate_to_omr` is now required when `currency` is not OMR. Requests missing it previously silently defaulted to 1.0.
- v0.7.0added2026-04-28Rate limiting
Introduced per-key token-bucket limits (10 req/s GET, 5 req/s write) with `Retry-After` on 429 responses.