Skip to main content

FawtaraX API

v1.4.0
Stable

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.

Base URL
https://api.fawtarax.com/v1
OpenAPI spec
GET https://api.fawtarax.com/v1/openapi.json

Importable into Postman, Stoplight, Insomnia, and SDK generators.

Downloads

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.

Security
  • 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.

FieldTypeDescription
PATCH (1.0.x)
non-breakingBug fixes, doc-only changes, performance improvements.
MINOR (1.x.0)
non-breakingNew endpoints, new optional fields, new enum values. Safe to upgrade.
MAJOR (2.0.0)
breakingNew 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 currency is not OMR you must pass exchange_rate_to_omr; OMR totals are computed server-side.
  • Pagination uses limit (max 100) and offset.
  • 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.

422idempotency_conflict— Idempotency-Key reused with a different body

This `Idempotency-Key` was already used for a request whose body hashes differently.

When it happens

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.

Recommended action

Use a fresh key per operation. Replaying the SAME body with the same key returns the first response, which is the behaviour you want.

Example payload
{
  "error": {
    "message": "This Idempotency-Key was already used with a different request body",
    "code": "idempotency_conflict"
  }
}
409idempotency_in_progress— A request with this key is still running

Another request carrying this `Idempotency-Key` has been claimed and has not finished.

When it happens

Two copies of the same request raced, or a previous attempt is still executing.

Recommended action

Retry after a short delay with the same key; you will get the first request's stored response.

Example payload
{
  "error": {
    "message": "A request with this Idempotency-Key is still in progress",
    "code": "idempotency_in_progress"
  }
}
403insufficient_scope— Insufficient scope

The API key authenticated, but it does not carry the scope this endpoint requires.

When it happens

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.

Recommended action

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.

Example payload
{
  "error": {
    "message": "This API key does not carry the invoices:write scope",
    "code": "insufficient_scope"
  }
}
403tenant_suspended— The workspace is suspended

FawtaraX has suspended the workspace this API key belongs to. Reads still work; every write is refused with this code until the workspace is reactivated.

When it happens

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.

Recommended action

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.

Example payload
{
  "error": {
    "message": "This workspace is suspended. Contact FawtaraX support to reactivate it.",
    "code": "tenant_suspended"
  }
}
402subscription_inactive— Subscription is not active

The tenant's subscription has lapsed. Reads stay open so you can export your data; writes resume when it is renewed.

When it happens

The plan was cancelled, the trial expired, or a renewal payment failed.

Recommended action

Renew the plan in the portal. Retrying will not help until then.

Example payload
{
  "error": {
    "message": "Subscription is not active. Renew the plan to continue.",
    "code": "subscription_inactive"
  }
}
422not_issuable— Document cannot be issued

The request was well-formed but the document does not satisfy the EN 16931 mandatory fields, so it cannot be issued.

When it happens

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.

Recommended action

Read `error.message`: it names the fields. Set them on the customer or the request and retry.

Example payload
{
  "error": {
    "message": "Invoice cannot be issued: Buyer country (ISO 3166-1 alpha-2) is required (BR-CL-14)",
    "code": "not_issuable"
  }
}
401unauthorized— Unauthorized

The Authorization header is missing or the bearer token does not match any active API key.

When it happens

No Authorization header, malformed header, or key was revoked.

Recommended action

Ensure the request sends `Authorization: Bearer fwx_7Kq2mZ9a_…`. If the key was revoked, create a new one from the API Keys tab.

Example payload
{
  "error": {
    "message": "Invalid or missing API key",
    "code": "unauthorized"
  }
}
400validation_error— Validation failed

The request body or query parameters did not match the expected schema.

When it happens

Missing required fields, wrong types, value out of range, or `exchange_rate_to_omr` missing for a non-OMR currency.

Recommended action

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.

Example payload
{
  "error": {
    "message": "Validation failed: lines: Array must contain at least 1 element(s)",
    "code": "validation_error"
  }
}
404not_found— Not found

The requested resource does not exist or does not belong to the authenticated tenant.

When it happens

Wrong ID, deleted resource, or attempting to access another tenant's data.

Recommended action

Verify the ID and that it was created under the same tenant as the API key.

Example payload
{
  "error": {
    "message": "Invoice not found",
    "code": "not_found"
  }
}
409invalid_state— Invalid state

The operation is not allowed in the resource's current state.

When it happens

Updating or deleting an issued invoice, cancelling a paid invoice, or modifying a non-draft credit note.

Recommended action

Use the appropriate workflow — issue a credit note to amend an invoice, or only mutate resources while still in `draft`.

Example payload
{
  "error": {
    "message": "Only draft invoices can be deleted",
    "code": "invalid_state"
  }
}
429rate_limited— Too many requests

The per-key rate limit was exceeded.

When it happens

Bursting above 10 req/s on GETs or 5 req/s on writes for a single key.

Recommended action

Honour the `Retry-After` response header (seconds) and add exponential backoff. Distribute load across multiple keys for high-volume integrations.

Example payload
{
  "error": {
    "message": "Rate limit exceeded",
    "code": "rate_limited"
  }
}
500internal_error— Server error

Unexpected internal error.

When it happens

Transient infrastructure or downstream provider failure.

Recommended action

Safe to retry idempotent (GET/DELETE) requests with backoff. For writes, check whether the resource was created before retrying.

Example payload
{
  "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).

FieldTypeDescription
GET endpoints
10 req/secBurst capacity: 60 requests.
POST / PATCH / DELETE
5 req/secBurst capacity: 30 requests.

List invoices

GET/invoices

Return a paginated list of tax invoices (UBL document type 380) for the authenticated tenant, newest first.

Query parameters

FieldTypeDescription
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

200Paginated invoice list.
{
  "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"
    }
  ]
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/invoices
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/invoices" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Create invoice

POST/invoices

Issue a new tax invoice. Totals, OMR amounts and the invoice number are derived server-side; pass a custom number to override.

Request body

FieldTypeDescription
number
string (≤ 50)Optional custom invoice number
external_ref
string (1–120) | nullYour 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
stringISO 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 | nullIBT-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) | nullIBT-080 ISO 3166-1 alpha-2. Required on an export (IBR-014-OM).
payment_means_code
string (≤ 4) | nullUNTDID 4461 payment means, e.g. 30 credit transfer, 10 cash.
place_of_supply
string (≤ 100) | null
buyer_country_code
string (2–2) | nullBR-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) | nullThe 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

201Invoice created.
{
  "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"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
422The document is well-formed but cannot be issued — it does not satisfy the EN 16931 mandatory fields.
{
  "error": {
    "code": "not_issuable",
    "message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/invoices
Copy this exact call
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"
    }
  ]
}'

Retrieve invoice

GET/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

200Invoice with embedded lines.
{
  "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"
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/invoices/{id}
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/invoices/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Update draft invoice

PATCH/invoices/{id}

Update fields on a draft invoice. Once issued, invoices are immutable — cancel or issue a credit note instead.

Request body

FieldTypeDescription
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 | nullIBT-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) | nullIBT-080 ISO 3166-1 alpha-2. Required on an export (IBR-014-OM).
payment_means_code
string (≤ 4) | nullUNTDID 4461 payment means, e.g. 30 credit transfer, 10 cash.
place_of_supply
string (≤ 100) | null
buyer_country_code
string (2–2) | nullBR-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

200Updated invoice.
{
  "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"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/invoices/{id}
Copy this exact call
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

DELETE/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

204Deleted.
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/invoices/{id}
Copy this exact call
curl -X DELETE "https://api.fawtarax.com/v1/invoices/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Cancel invoice

POST/invoices/{id}/cancel

Mark 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

200Invoice cancelled.
{
  "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"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
422The document is well-formed but cannot be issued — it does not satisfy the EN 16931 mandatory fields.
{
  "error": {
    "code": "not_issuable",
    "message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/invoices/{id}/cancel
Copy this exact call
curl -X POST "https://api.fawtarax.com/v1/invoices/{id}/cancel" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Send invoice to the Peppol network

POST/invoices/{id}/send

File 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

200The document reached the access point.
{
  "data": {
    "outcome": "sent",
    "mode": "live",
    "transmission_uuid": "c4a71e38-2b95-4d07-8f16-9e3a5c0b7d24",
    "as4_message_id": "urn:uuid:7d2f5a91-c063-4e18-9b45-1a8e6c3f0d72@fawtarax.com"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
409The document is a draft, or a send for it is already in flight. Retry once it settles.
422The document was refused. A conformance refusal carries `rules` and `findings`.
{
  "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."
      }
    ]
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
502The access point could not be reached, or refused the document.
503Transmission is not armed on this deployment.
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/invoices/{id}/send
Copy this exact call
curl -X POST "https://api.fawtarax.com/v1/invoices/{id}/send" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Send credit note to the Peppol network

POST/credit-notes/{id}/send

File 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

200The document reached the access point.
{
  "data": {
    "outcome": "sent",
    "mode": "live",
    "transmission_uuid": "e18b6d40-3f72-4c95-a027-5d9e1b7a3c86",
    "as4_message_id": "urn:uuid:2c9e4b17-8d50-4a63-b1f8-6e0a7d3c5921@fawtarax.com"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
409The document is a draft, or a send for it is already in flight. Retry once it settles.
422The document was refused. A conformance refusal carries `rules` and `findings`.
{
  "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."
      }
    ]
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
502The access point could not be reached, or refused the document.
503Transmission is not armed on this deployment.
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/credit-notes/{id}/send
Copy this exact call
curl -X POST "https://api.fawtarax.com/v1/credit-notes/{id}/send" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Get invoice QR payload

GET/invoices/{id}/qr

The 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

FieldTypeDescription
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

200The QR payload.
{
  "data": {
    "format": "tlv",
    "payload": "AQhGYXd0YXJhWAIMT00xMTAwMDAzNTU0",
    "url": null,
    "tlv": "AQhGYXd0YXJhWAIMT00xMTAwMDAzNTU0",
    "invoice_uuid": "9e2f4b60-3c17-5d8a-b04e-7f1a6c9d2e83",
    "seller_uuid": "5c8a1f74-6d29-5b03-8e17-4a9f2c6d0b35"
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/invoices/{id}/qr
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/invoices/{id}/qr" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

List credit notes

GET/credit-notes

Query parameters

FieldTypeDescription
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

200Paginated credit note list.
{
  "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"
    }
  ]
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/credit-notes
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/credit-notes" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Create credit note

POST/credit-notes

Issue 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

FieldTypeDescription
original_invoice_idrequired
stringThe 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
stringLocal 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

201Credit note created.
{
  "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"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
422The document is well-formed but cannot be issued — it does not satisfy the EN 16931 mandatory fields.
{
  "error": {
    "code": "not_issuable",
    "message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/credit-notes
Copy this exact call
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"
}'

Retrieve credit note

GET/credit-notes/{id}

Example request

curl -X GET "https://api.fawtarax.com/v1/credit-notes/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Responses

200Credit note with lines.
{
  "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"
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/credit-notes/{id}
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/credit-notes/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Update draft credit note

PATCH/credit-notes/{id}

Request body

FieldTypeDescription
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

200Updated credit note.
{
  "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"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/credit-notes/{id}
Copy this exact call
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

DELETE/credit-notes/{id}

Example request

curl -X DELETE "https://api.fawtarax.com/v1/credit-notes/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Responses

204Deleted.
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/credit-notes/{id}
Copy this exact call
curl -X DELETE "https://api.fawtarax.com/v1/credit-notes/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Create invoices in bulk

POST/invoices/batch

Up 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

FieldTypeDescription
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

200Synchronous batch: one result per row.
{
  "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
    }
  }
}
202Queued as a job; poll `/import-jobs/{id}`.
{
  "data": {
    "job_id": "d7c30e85-1f42-4a96-b508-6e9c2a17d403",
    "total_rows": 120,
    "status": "queued",
    "poll": "/v1/import-jobs/d7c30e85-1f42-4a96-b508-6e9c2a17d403"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
422The document is well-formed but cannot be issued — it does not satisfy the EN 16931 mandatory fields.
{
  "error": {
    "code": "not_issuable",
    "message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/invoices/batch
Copy this exact call
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
        }
      ]
    }
  ]
}'

Dry-run a batch

POST/invoices/batch/validate

Runs 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

FieldTypeDescription
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

200Per-row validation report.
{
  "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
    }
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
422The document is well-formed but cannot be issued — it does not satisfy the EN 16931 mandatory fields.
{
  "error": {
    "code": "not_issuable",
    "message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/invoices/batch/validate
Copy this exact call
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
        }
      ]
    }
  ]
}'

Retrieve an import job

GET/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

200Job and rows.
{
  "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
      }
    ]
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/import-jobs/{id}
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/import-jobs/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

List customers

GET/customers

Buyers 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

FieldTypeDescription
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

200Customer list.
{
  "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"
    }
  ]
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/customers
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/customers" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Create customer

POST/customers

Create 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

FieldTypeDescription
namerequired
string (1–255)
country_code
anyISO 3166-1 alpha-2. Required downstream by BR-CL-14, so it defaults to OM here.
vat_number
string (≤ 64) | nullOM + 10 digits for Oman buyers
email
any
phone
string (≤ 64) | null
address
string (≤ 1000) | null
address_line2
string (≤ 255) | nullIBT-051 buyer address line 2 — needed for a tax invoice
address_line3
string (≤ 255) | nullIBT-163 buyer address line 3 — needed for a tax invoice
city
string (≤ 120) | nullIBT-052 — needed for a tax invoice
postal_code
string (≤ 40) | nullIBT-053 — needed for a tax invoice; 3 digits in Oman
contact_name
string (≤ 255) | null
peppol_scheme
string (≤ 4) | null4-digit code, 0248 for Oman
peppol_value
string (≤ 64) | nullThe 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

201Customer created.
{
  "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"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
422The document is well-formed but cannot be issued — it does not satisfy the EN 16931 mandatory fields.
{
  "error": {
    "code": "not_issuable",
    "message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/customers
Copy this exact call
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"
}'

Retrieve customer

GET/customers/{id}

Example request

curl -X GET "https://api.fawtarax.com/v1/customers/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Responses

200Customer.
{
  "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"
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/customers/{id}
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/customers/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Update customer

PATCH/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

FieldTypeDescription
namerequired
string (1–255)
country_code
anyISO 3166-1 alpha-2. Required downstream by BR-CL-14, so it defaults to OM here.
vat_number
string (≤ 64) | nullOM + 10 digits for Oman buyers
email
any
phone
string (≤ 64) | null
address
string (≤ 1000) | null
address_line2
string (≤ 255) | nullIBT-051 buyer address line 2 — needed for a tax invoice
address_line3
string (≤ 255) | nullIBT-163 buyer address line 3 — needed for a tax invoice
city
string (≤ 120) | nullIBT-052 — needed for a tax invoice
postal_code
string (≤ 40) | nullIBT-053 — needed for a tax invoice; 3 digits in Oman
contact_name
string (≤ 255) | null
peppol_scheme
string (≤ 4) | null4-digit code, 0248 for Oman
peppol_value
string (≤ 64) | nullThe 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

200Customer updated.
{
  "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"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/customers/{id}
Copy this exact call
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

GET/received-invoices

Documents 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

FieldTypeDescription
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

200Received document list.
{
  "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
      }
    }
  ]
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/received-invoices
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/received-invoices" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Retrieve received document

GET/received-invoices/{id}

Example request

curl -X GET "https://api.fawtarax.com/v1/received-invoices/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Responses

200Received document with its approval state.
{
  "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
    }
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/received-invoices/{id}
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/received-invoices/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Approve or hold a received document

POST/received-invoices/{id}/decision

Record 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

FieldTypeDescription
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) | nullThe 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

200Decision recorded; the document with its new approval state.
{
  "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
    }
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
422The document is well-formed but cannot be issued — it does not satisfy the EN 16931 mandatory fields.
{
  "error": {
    "code": "not_issuable",
    "message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/received-invoices/{id}/decision
Copy this exact call
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"
}'

Get the prepaid wallet

GET/wallet

The 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

200The wallet.
{
  "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
    }
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/wallet
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/wallet" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

List webhook endpoints

GET/webhook-endpoints

Every 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

200Endpoint list.
{
  "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"
    }
  ]
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/webhook-endpoints
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/webhook-endpoints" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Register a webhook endpoint

POST/webhook-endpoints

Register 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

FieldTypeDescription
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

201Endpoint created; the secret is in this response only.
{
  "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"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
422The document is well-formed but cannot be issued — it does not satisfy the EN 16931 mandatory fields.
{
  "error": {
    "code": "not_issuable",
    "message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/webhook-endpoints
Copy this exact call
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"
  ]
}'

Retrieve webhook endpoint

GET/webhook-endpoints/{id}

Example request

curl -X GET "https://api.fawtarax.com/v1/webhook-endpoints/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Responses

200Endpoint.
{
  "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"
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/webhook-endpoints/{id}
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/webhook-endpoints/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Update webhook endpoint

PATCH/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

FieldTypeDescription
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

200Endpoint updated.
{
  "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"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/webhook-endpoints/{id}
Copy this exact call
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

DELETE/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

204Endpoint deleted.
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/webhook-endpoints/{id}
Copy this exact call
curl -X DELETE "https://api.fawtarax.com/v1/webhook-endpoints/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Rotate the signing secret

POST/webhook-endpoints/{id}/rotate-secret

Issues 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

200Rotated; the new secret is in this response only.
{
  "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"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
422The document is well-formed but cannot be issued — it does not satisfy the EN 16931 mandatory fields.
{
  "error": {
    "code": "not_issuable",
    "message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/webhook-endpoints/{id}/rotate-secret
Copy this exact call
curl -X POST "https://api.fawtarax.com/v1/webhook-endpoints/{id}/rotate-secret" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

List webhook deliveries

GET/webhook-deliveries

Every 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

FieldTypeDescription
limit
integer (1–100)Page size (max 100).
offset
integer (≥ 0)Records to skip.
endpoint_id
stringOnly 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

200Delivery list.
{
  "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"
    }
  ]
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

10 req/sec per API key (burst 60). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/webhook-deliveries
Copy this exact call
curl -X GET "https://api.fawtarax.com/v1/webhook-deliveries" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Re-send a delivery

POST/webhook-deliveries/{id}/redeliver

Queues 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

201Redelivery queued.
{
  "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"
  }
}
400Request body or query failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation: lines must contain at least 1 element."
  }
}
401Invalid or missing API key.
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}
402The tenant's subscription has lapsed. Reads stay open; writes resume when it is renewed.
{
  "error": {
    "code": "subscription_inactive",
    "message": "The subscription has lapsed. Reads stay open; writes resume when it is renewed."
  }
}
403Forbidden. Either the API key does not carry the scope this endpoint requires (`insufficient_scope`), or — on any write — the workspace the key belongs to is suspended (`tenant_suspended`, PA-3.2): reads stay open, writes are refused until FawtaraX reactivates it, and renewing the subscription does not lift it (that state is `402 subscription_inactive`).
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not carry the invoices:write scope."
  }
}
404Resource not found for this tenant.
{
  "error": {
    "code": "not_found",
    "message": "Invoice not found."
  }
}
409Operation not allowed in the current resource state.
{
  "error": {
    "code": "invalid_state",
    "message": "Invoice is already issued and cannot be edited."
  }
}
422The document is well-formed but cannot be issued — it does not satisfy the EN 16931 mandatory fields.
{
  "error": {
    "code": "not_issuable",
    "message": "The invoice cannot be issued: the buyer has no country code (BR-CL-14)."
  }
}
429Per-key rate limit exceeded. Honour Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second."
  }
}
500Something went wrong on our side. The message carries a correlation reference and never the underlying error (PE-M8).
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong. Reference: req_8f2c14a9."
  }
}
Rate limit

5 req/sec per API key (burst 30). Exceeding returns 429 rate_limited.

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Retry guidance

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

Try it
Calls the live API with your test key.

Paste a key from the API Keys tab to enable signed requests.

https://api.fawtarax.com/v1/webhook-deliveries/{id}/redeliver
Copy this exact call
curl -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.

  1. v1.4.0
    changed2026-10-18
    Received 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.

  2. v1.3.1
    changed2026-09-06
    A 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.

  3. v1.3.0
    added2026-09-05
    Outbound 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).

  4. v1.2.3
    fixed2026-09-05
    The 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.

  5. v1.2.2
    fixed2026-08-29
    The 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.

  6. v1.2.1
    fixed2026-08-28
    Customer 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.

  7. v1.2.0
    added2026-08-27
    The 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.

  8. v1.1.0
    changed2026-08-27
    The 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.

  9. v1.1.0
    added2026-08-25
    Customers 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.

  10. v1.1.0
    removed2026-08-25
    Outbound 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.

  11. v1.0.0
    added2026-06-08
    Public 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.

  12. v0.9.0
    added2026-05-22
    Credit note line override

    POST /credit-notes now accepts an explicit `lines` array; omitting it still mirrors the original invoice lines as negative quantities.

  13. v0.8.0
    changed2026-05-10
    Tightened invoice validation

    `exchange_rate_to_omr` is now required when `currency` is not OMR. Requests missing it previously silently defaulted to 1.0.

  14. v0.7.0
    added2026-04-28
    Rate limiting

    Introduced per-key token-bucket limits (10 req/s GET, 5 req/s write) with `Retry-After` on 429 responses.