Skip to main content

FawtaraX API

v1.2.2
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.2.2. 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_conflictIdempotency-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_progressA 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_scopeInsufficient 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"
  }
}
402subscription_inactiveSubscription 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_issuableDocument 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"
  }
}
401unauthorizedUnauthorized

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_errorValidation 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_foundNot 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_stateInvalid 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_limitedToo 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_errorServer 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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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}
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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}
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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}
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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, original invoice lines are mirrored with negative quantities.

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 invoice lines as negative quantities

Example request

curl -X POST "https://api.fawtarax.com/v1/credit-notes" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"
 \
  -H "Content-Type: application/json" \
  -d '{ "original_invoice_id": "3f1a7c2e-9b04-4d51-8a6f-2c7e1d0b4a55", "reason_code": "QTY", "reason_note": "Customer returned 2 units", "document_type_code": "381", "issue_date": "2026-06-20", "issue_time": "09:15:00" }'

Responses

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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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}
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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}
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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}
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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}
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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-036, self-billing only
address_line3
string (≤ 255) | nullIBT-162, self-billing only
city
string (≤ 120) | nullIBT-037, self-billing only
postal_code
string (≤ 40) | nullIBT-038, self-billing only
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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}
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-036, self-billing only
address_line3
string (≤ 255) | nullIBT-162, self-billing only
city
string (≤ 120) | nullIBT-037, self-billing only
postal_code
string (≤ 40) | nullIBT-038, self-billing only
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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}
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" | "rejected" | "on_hold"Filter by approval state.

Example request

curl -X GET "https://api.fawtarax.com/v1/received-invoices?limit=20&offset=0&status=pending" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Responses

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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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}
curl -X GET "https://api.fawtarax.com/v1/received-invoices/{id}" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"

Approve, reject or hold a received document

POST/received-invoices/{id}/decision

Record an AP approval decision. One decision exists per document and a second call replaces it. This is not the MLR — the message layer response is a send on the tenant's behalf and is not exposed under `inbox:write`.

Request body

FieldTypeDescription
decisionrequired
"pending" | "approved" | "rejected" | "on_hold"`pending` clears a previous decision
comment
string (≤ 1000) | null

Example request

curl -X POST "https://api.fawtarax.com/v1/received-invoices/{id}/decision" \
  -H "Authorization: Bearer fwx_7Kq2mZ9a_…"
 \
  -H "Content-Type: application/json" \
  -d '{ "decision": "approved", "comment": "Matched PO-1043" }'

Responses

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."
  }
}
403The API key does not carry the scope this endpoint requires.
{
  "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
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"
}'

Changelog

Every published change to the API, newest first.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

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

  8. 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.

  9. 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.

  10. 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.