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.4.0. 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"
}
}tenant_suspended— The workspace is suspendedFawtaraX has suspended the workspace this API key belongs to. Reads still work; every write is refused with this code until the workspace is reactivated.
Staff suspended the workspace (for example an unresolved compliance or billing matter, or at the account owner's request), or the workspace was closed. It is not a lapsed subscription — that is `402 subscription_inactive`, and renewing does not lift a suspension.
Stop retrying writes: they will fail until the suspension is lifted. The account owner should contact FawtaraX support. Reads (GET) remain available so an integration can keep reconciling.
{
"error": {
"message": "This workspace is suspended. Contact FawtaraX support to reactivate it.",
"code": "tenant_suspended"
}
}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 |
external_ref | string (1–120) | null | Your own identifier for this document (ERP document number, order id). Stored, returned, searchable in the portal, and carried on every webhook event about the document. Set at creation only. |
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, the original invoice's lines are mirrored with POSITIVE quantities — the document type code expresses the direction, not the sign.
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 document's lines. Mirrored quantities are POSITIVE, exactly as supplied ones must be: direction is expressed by document_type_code, not by the sign. |
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-051 buyer address line 2 — needed for a tax invoice |
address_line3 | string (≤ 255) | null | IBT-163 buyer address line 3 — needed for a tax invoice |
city | string (≤ 120) | null | IBT-052 — needed for a tax invoice |
postal_code | string (≤ 40) | null | IBT-053 — needed for a tax invoice; 3 digits in Oman |
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-051 buyer address line 2 — needed for a tax invoice |
address_line3 | string (≤ 255) | null | IBT-163 buyer address line 3 — needed for a tax invoice |
city | string (≤ 120) | null | IBT-052 — needed for a tax invoice |
postal_code | string (≤ 40) | null | IBT-053 — needed for a tax invoice; 3 digits in Oman |
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" | "on_hold" | Filter by payables 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 or hold a received document
/received-invoices/{id}/decisionRecord the payables decision — the same three actions as AP approvals in the portal: approve for payment (`approved`), put on hold with a reason (`on_hold`), or release (`pending`). One decision exists per document and a second call replaces it. There is no reject: in the Oman model nothing goes back to the supplier, who is not notified of approvals or holds; a document you will not pay is held, and the supplier corrects it with a credit note. Approving or releasing a document that was disputed in the portal also closes that dispute. `rejected`, a decision before 1.4.0, is still accepted and recorded as `on_hold`.
Request body
| Field | Type | Description |
|---|---|---|
decisionrequired | "pending" | "approved" | "on_hold" | `approved` approves the document for payment, `on_hold` holds it, `pending` releases a hold or clears an approval. There is no reject: nothing reaches the supplier, who corrects a document with a credit note. |
comment | string (≤ 1000) | null | The reason for a hold. Omitted on `on_hold`, it is stored as "No specific reason"; ignored on `approved` and `pending`. |
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"
}'Get the prepaid wallet
/walletThe workspace's prepaid balance in baisa, the unit price one document deducts, the low-balance threshold and grace allowance, and this month's metered usage — the same snapshot the Billing page shows. Requires the `billing:read` scope. Read-only: top-ups are arranged with FawtaraX and credited by the platform, not made through the API.
Example request
curl -X GET "https://api.fawtarax.com/v1/wallet" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": {
"balance_baisa": 12400,
"unit_price_baisa": 100,
"low_threshold_baisa": 5000,
"grace_baisa": 1000,
"currency": "OMR",
"usage": {
"period": "2026-09",
"documents": 37,
"charged_baisa": 3700
}
}
}{
"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/walletcurl -X GET "https://api.fawtarax.com/v1/wallet" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"List webhook endpoints
/webhook-endpointsEvery endpoint registered for the authenticated tenant, newest first. The signing secret is never returned here.
Example request
curl -X GET "https://api.fawtarax.com/v1/webhook-endpoints" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": [
{
"id": "7c9e1a3b-5d7f-4e2a-b8c0-2a4c6e8f0b15",
"url": "https://erp.example.com/hooks/fawtarax",
"description": "ERP — production",
"events": [
"submission.acknowledged",
"submission.rejected",
"submission.cleared",
"inbound.received"
],
"api_version": "v1",
"status": "active",
"failing_since": null,
"disabled_reason": null,
"disabled_at": null,
"last_success_at": "2026-09-05T08:04:12.000Z",
"secret_rotated_at": null,
"created_at": "2026-09-01T09:00:00.000Z",
"updated_at": "2026-09-05T08:04:12.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/webhook-endpointscurl -X GET "https://api.fawtarax.com/v1/webhook-endpoints" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Register a webhook endpoint
/webhook-endpointsRegister an HTTPS URL and the events to deliver to it. The 201 carries the signing secret ONCE — store it; it cannot be read back, only rotated. The URL is vetted at save time (HTTPS, publicly routable, resolvable) and again before every delivery. Honours `Idempotency-Key`, so a retried registration returns the same endpoint and the same secret rather than a second one.
Request body
| Field | Type | Description |
|---|---|---|
urlrequired | string (≤ 2048) | HTTPS only, publicly routable. Private, link-local and loopback destinations are refused at save time and again before every delivery. |
description | string (≤ 200) | null | |
eventsrequired | array (1–64) | The events to deliver to this URL. `*` subscribes to all of them. |
Example request
curl -X POST "https://api.fawtarax.com/v1/webhook-endpoints" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"
\
-H "Content-Type: application/json" \
-d '{ "url": "https://erp.example.com/hooks/fawtarax", "description": "ERP — production", "events": [ "submission.acknowledged", "submission.rejected", "submission.cleared", "inbound.received" ] }'Responses
{
"data": {
"id": "7c9e1a3b-5d7f-4e2a-b8c0-2a4c6e8f0b15",
"url": "https://erp.example.com/hooks/fawtarax",
"description": "ERP — production",
"events": [
"submission.acknowledged",
"submission.rejected",
"submission.cleared",
"inbound.received"
],
"api_version": "v1",
"status": "active",
"failing_since": null,
"disabled_reason": null,
"disabled_at": null,
"last_success_at": null,
"secret_rotated_at": null,
"created_at": "2026-09-01T09:00:00.000Z",
"updated_at": "2026-09-01T09:00:00.000Z",
"secret": "whsec_example_only_shown_once"
}
}{
"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/webhook-endpointscurl -X POST "https://api.fawtarax.com/v1/webhook-endpoints" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.example.com/hooks/fawtarax",
"description": "ERP — production",
"events": [
"submission.acknowledged",
"submission.rejected",
"submission.cleared",
"inbound.received"
]
}'Retrieve webhook endpoint
/webhook-endpoints/{id}Example request
curl -X GET "https://api.fawtarax.com/v1/webhook-endpoints/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": {
"id": "7c9e1a3b-5d7f-4e2a-b8c0-2a4c6e8f0b15",
"url": "https://erp.example.com/hooks/fawtarax",
"description": "ERP — production",
"events": [
"submission.acknowledged",
"submission.rejected",
"submission.cleared",
"inbound.received"
],
"api_version": "v1",
"status": "active",
"failing_since": null,
"disabled_reason": null,
"disabled_at": null,
"last_success_at": "2026-09-05T08:04:12.000Z",
"secret_rotated_at": null,
"created_at": "2026-09-01T09:00:00.000Z",
"updated_at": "2026-09-05T08:04:12.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/webhook-endpoints/{id}curl -X GET "https://api.fawtarax.com/v1/webhook-endpoints/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Update webhook endpoint
/webhook-endpoints/{id}A sparse patch: omitted fields are kept. Setting `status: "active"` re-enables an endpoint that was disabled — by you, or automatically after 72 hours in which every delivery failed — and clears `failing_since`. Deliveries that went `dead` or `skipped` while it was off are not replayed automatically; use the redeliver call.
Request body
| Field | Type | Description |
|---|---|---|
url | string (≤ 2048) | HTTPS only, publicly routable. Private, link-local and loopback destinations are refused at save time and again before every delivery. |
description | string (≤ 200) | null | |
events | array (1–64) | |
status | "active" | "disabled" | Set `active` to re-enable an endpoint that was disabled — by you, or automatically after 72 h of failed deliveries. Re-enabling clears `failing_since`. |
Example request
curl -X PATCH "https://api.fawtarax.com/v1/webhook-endpoints/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"
\
-H "Content-Type: application/json" \
-d '{ "events": [ "*" ], "status": "active" }'Responses
{
"data": {
"id": "7c9e1a3b-5d7f-4e2a-b8c0-2a4c6e8f0b15",
"url": "https://erp.example.com/hooks/fawtarax",
"description": "ERP — production",
"events": [
"submission.acknowledged",
"submission.rejected",
"submission.cleared",
"inbound.received"
],
"api_version": "v1",
"status": "active",
"failing_since": null,
"disabled_reason": null,
"disabled_at": null,
"last_success_at": "2026-09-05T08:04:12.000Z",
"secret_rotated_at": null,
"created_at": "2026-09-01T09:00:00.000Z",
"updated_at": "2026-09-05T08:04:12.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/webhook-endpoints/{id}curl -X PATCH "https://api.fawtarax.com/v1/webhook-endpoints/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…" \
-H "Content-Type: application/json" \
-d '{
"events": [
"*"
],
"status": "active"
}'Delete webhook endpoint
/webhook-endpoints/{id}Removes the endpoint and its delivery history. Queued deliveries are dropped.
Example request
curl -X DELETE "https://api.fawtarax.com/v1/webhook-endpoints/{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": "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/webhook-endpoints/{id}curl -X DELETE "https://api.fawtarax.com/v1/webhook-endpoints/{id}" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Rotate the signing secret
/webhook-endpoints/{id}/rotate-secretIssues a new signing secret and returns it ONCE. For 24 hours after rotation every delivery is signed with both secrets — `x-fwx-signature` carries two `v1=` entries — so you can roll the stored secret without a window of rejected deliveries. No request body. Honours `Idempotency-Key`.
Example request
curl -X POST "https://api.fawtarax.com/v1/webhook-endpoints/{id}/rotate-secret" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": {
"id": "7c9e1a3b-5d7f-4e2a-b8c0-2a4c6e8f0b15",
"url": "https://erp.example.com/hooks/fawtarax",
"description": "ERP — production",
"events": [
"submission.acknowledged",
"submission.rejected",
"submission.cleared",
"inbound.received"
],
"api_version": "v1",
"status": "active",
"failing_since": null,
"disabled_reason": null,
"disabled_at": null,
"last_success_at": null,
"secret_rotated_at": null,
"created_at": "2026-09-01T09:00:00.000Z",
"updated_at": "2026-09-01T09:00:00.000Z",
"secret": "whsec_example_only_shown_once"
}
}{
"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/webhook-endpoints/{id}/rotate-secretcurl -X POST "https://api.fawtarax.com/v1/webhook-endpoints/{id}/rotate-secret" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"List webhook deliveries
/webhook-deliveriesEvery delivery the dispatcher attempted for this tenant, newest first, with its retry state, the last HTTP status and error. Retries follow 1 m, 5 m, 30 m, 2 h, 6 h, 12 h, then every 12 h, and a delivery is `dead` 72 hours after its first attempt or at once on a 400, 401, 403, 404 or 410. `event_type` is applied AFTER the page is fetched — the type lives on the event — so 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. |
endpoint_id | string | Only deliveries to this endpoint. |
status | "queued" | "processing" | "delivered" | "retrying" | "dead" | "skipped" | Only deliveries in this state. |
event_type | "submission.validated" | "submission.validation_failed" | "submission.received" | "submission.dispatched" | "submission.acknowledged" | "submission.rejected" | "submission.cleared" | "submission.resolved" | "inbound.received" | "inbound.decided" | "invoice.issued" | "invoice.paid" | "invoice.cancelled" | "credit_note.created" | "payment.failed" | "integration.endpoint_disabled" | Only deliveries of this event type (applied after the page). |
Example request
curl -X GET "https://api.fawtarax.com/v1/webhook-deliveries?limit=20&offset=0&status=queued&event_type=submission.validated" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": [
{
"id": "0b6d2f84-9a1c-4e3b-8d5f-7c2a4b6e8d10",
"endpoint_id": "7c9e1a3b-5d7f-4e2a-b8c0-2a4c6e8f0b15",
"event_id": "e4a8c2d6-1b3f-4f5a-9c7e-2d4b6a8c0e12",
"event_type": "submission.acknowledged",
"status": "delivered",
"attempts": 1,
"max_attempts": 20,
"first_attempted_at": "2026-09-05T08:04:11.000Z",
"last_attempted_at": "2026-09-05T08:04:11.000Z",
"next_retry_at": "2026-09-05T08:04:11.000Z",
"delivered_at": "2026-09-05T08:04:12.000Z",
"response_status": 200,
"latency_ms": 412,
"last_error": null,
"redelivery_of": null,
"created_at": "2026-09-05T08:04:10.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/webhook-deliveriescurl -X GET "https://api.fawtarax.com/v1/webhook-deliveries" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Re-send a delivery
/webhook-deliveries/{id}/redeliverQueues a fresh delivery of the same event to the same endpoint; the new row's `redelivery_of` points at this one and the original is left untouched. A delivery that is still queued, retrying or processing is a 409 (it will be attempted anyway), and so is a redelivery to a disabled endpoint — re-enable it first. No request body. Honours `Idempotency-Key`.
Example request
curl -X POST "https://api.fawtarax.com/v1/webhook-deliveries/{id}/redeliver" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Responses
{
"data": {
"id": "0b6d2f84-9a1c-4e3b-8d5f-7c2a4b6e8d10",
"endpoint_id": "7c9e1a3b-5d7f-4e2a-b8c0-2a4c6e8f0b15",
"event_id": "e4a8c2d6-1b3f-4f5a-9c7e-2d4b6a8c0e12",
"event_type": "submission.acknowledged",
"status": "delivered",
"attempts": 1,
"max_attempts": 20,
"first_attempted_at": "2026-09-05T08:04:11.000Z",
"last_attempted_at": "2026-09-05T08:04:11.000Z",
"next_retry_at": "2026-09-05T08:04:11.000Z",
"delivered_at": "2026-09-05T08:04:12.000Z",
"response_status": 200,
"latency_ms": 412,
"last_error": null,
"redelivery_of": null,
"created_at": "2026-09-05T08:04:10.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": "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/webhook-deliveries/{id}/redelivercurl -X POST "https://api.fawtarax.com/v1/webhook-deliveries/{id}/redeliver" \
-H "Authorization: Bearer fwx_7Kq2mZ9a_…"Changelog
Every published change to the API, newest first.
- v1.4.0changed2026-10-18Received documents are approved or held — there is no reject
POST /v1/received-invoices/{id}/decision now takes the three actions AP approvals has in the portal since 2026-10-07: `approved` (for payment), `on_hold`, and `pending` (release a hold or clear an approval). `rejected` is no longer published. In the Oman model nothing goes back to the supplier from your side — the supplier is not notified of approvals or holds — so a rejection was only ever a local note that read as if the supplier had been told; a document you will not pay is held, and the supplier corrects it with a credit note. Nothing breaks: `rejected` is still accepted on the decision and on the `status` filter of GET /v1/received-invoices, and means `on_hold`; it goes in v2. A hold now always carries a reason — sent without a `comment`, it is stored as "No specific reason", what the portal stores for that choice — and a `comment` on `approved` or `pending` is no longer stored. `approval.status` on every received document is now the state the portal shows: a document decided `rejected` earlier, or disputed in the portal, reads `on_hold`, so the API and the screen no longer disagree about whether a document is held; approving or releasing such a document over the API closes the dispute, as the portal always did. The `inbound.decided` webhook carries `approved`, `on_hold` or `pending`. Behaviour changed and the published enum narrowed without any request starting to fail, so MINOR — 1.3.1 becomes 1.4.0.
- v1.3.1changed2026-09-06A suspended workspace answers 403 tenant_suspended on every write
The error code tenant_suspended (403) returns to the catalogue, this time with a handler behind it. FawtaraX can now suspend a workspace; from then on every write in this API — POST /v1/invoices, /v1/credit-notes, /v1/customers, the batch, the two /send calls, cancel, PATCH and DELETE on invoices, credit notes, customers and webhook endpoints, the inbound decision, rotate-secret and redeliver — is refused with 403 and code tenant_suspended before any other check, and every GET keeps working — GET /v1/wallet (scope billing:read) included — so an integration can go on reconciling and see the balance it will resume against. It is a different state from 402 subscription_inactive: renewing does not lift a suspension, so a client that treats the two the same will retry the wrong thing. The 403 response object on each write now names both of its codes (insufficient_scope and tenant_suspended); the status set of every operation is unchanged, which is why this is a PATCH. Nothing about a request or a successful response changed.
- v1.3.0added2026-09-05Outbound webhooks
FawtaraX now calls your server. Register an HTTPS endpoint with POST /v1/webhook-endpoints (scope webhooks:write), choose the events, and store the signing secret the 201 returns — it is shown once. Sixteen events are delivered: the ten that mirror a document's filing lifecycle (submission.validated, submission.validation_failed, submission.received, submission.dispatched, submission.acknowledged, submission.rejected, submission.cleared, submission.resolved, inbound.received, inbound.decided), the five commercial ones this changelog withdrew on 2026-08-25 because they had never shipped (invoice.issued, invoice.paid, invoice.cancelled, credit_note.created, payment.failed — they ship now, with the same names), and integration.endpoint_disabled. Every delivery carries the envelope documented as the WebhookEvent schema and is signed as HMAC-SHA256 over `${x-fwx-timestamp}.${raw body}` in x-fwx-signature — the contract the tester on the Developers page has used since it shipped, so a verifier written against it needs no change. Order deliveries by `sequence`, not by arrival: retries reorder them. Retries follow 1 m, 5 m, 30 m, 2 h, 6 h, 12 h and then every 12 h; a delivery is dead 72 hours after its first attempt, or at once on a 400, 401, 403, 404 or 410; an endpoint that fails every delivery for 72 hours is disabled and your other endpoints are told. GET /v1/webhook-deliveries lists every attempt with its status and last error; POST /v1/webhook-deliveries/{id}/redeliver queues a fresh one; POST /v1/webhook-endpoints/{id}/rotate-secret rotates the secret with a 24-hour overlap during which both secrets sign. The events and their payloads are published in the document's top-level `webhooks` object, from which the reference page derives its table. Also new: POST /v1/invoices and the batch accept `external_ref`, your own identifier for a document; it is stored, returned, and carried on every event about the document. Also new in this release: GET /v1/wallet (scope billing:read, a new mintable scope) returns the prepaid wallet — balance, the unit price your plan pays per document, the low-balance and grace thresholds, whether self-service top-up is enabled for the workspace — and this month's metered usage; issuing is now metered per document against that balance rather than a monthly quota, so poll it before a batch. Additive, so MINOR — 1.2.3 becomes 1.3.0 (the 1.2.x line was already taken by the document-type release of 2026-08-27, which is why this is not the 1.2.0 an earlier plan named).
- v1.2.3fixed2026-09-05The credit-note `lines` field said the opposite of what the mirror does
Documentation only — no request, response or behaviour changed. Since 1.2.0 (2026-08-27) POST /v1/credit-notes has mirrored the original document's lines with POSITIVE quantities, and the schema description for `lines` still read "Omit to mirror the original invoice lines as negative quantities", with the operation description saying the same. That sentence is published: it was in /v1/openapi.json, in the Postman collection, in both generated SDKs, on the /docs reference page and in the PDF specification, so every artefact an integrator reads contradicted both the handler and the 1.2.0 entry that announced the change. It also contradicted the schema beside it, which has always refused a negative quantity on lines you supply. An integrator who trusted it would have negated the quantities before sending them and been refused with a validation error, or expected negative quantities back and reconciled the wrong way. Both descriptions now say positive, and say why: the document type code expresses the direction, not the sign — which is also the only reading that works for a 383 debit note, where negating would invert the document. Regenerate your SDK or Postman collection only if you want the corrected prose; the wire is unchanged.
- 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.