Sending the key
Every request needs an API key, sent in the Authorization header as a bearer token: Authorization: Bearer <key>. The X-Api-Key header is accepted as an alternative. A key in the query string is refused, because URLs end up in logs.
# Same as the Bearer header; use whichever your HTTP client makes easier.
curl https://fatoora.tw.sa/api/v1/ping \
-H "X-Api-Key: wbk_test_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Accept: application/json"
All calls use HTTPS and JSON. Send Accept: application/json on every request, and Content-Type: application/json with a body.
Scopes
A key can do only what it was granted. Grant the least each system needs.
| Scope | Allows |
|---|---|
companies:read |
Read the company profile and whether it is ready to issue (GET /company, GET /company/readiness). |
customers:read |
List and read customers (GET /customers, GET /customers/{id}). Customer webhook events also need it. |
customers:write |
Create and update customers (POST /customers, PATCH /customers/{id}). |
products:read |
Find and read products and services through Wonder Fatoora MCP (search_products, get_product). No REST endpoint uses it yet. |
products:write |
Create and update products and services through Wonder Fatoora MCP (create_product, update_product). The person behind the credential must also be allowed to manage products. |
invoices:read |
Read invoices, their status and notes, and the event list (GET /invoices, GET /invoices/{id}, GET /notes/{id}, GET /events). Invoice and note webhook events need it, and a replayed create call returns the full invoice only to a credential holding it. |
invoices:draft |
Create invoices and credit or debit notes as drafts, and validate drafts (POST /invoices, POST /invoices/{id}/notes, POST /invoices/{id}/validate). invoices:write, its original name, keeps working. |
invoices:issue |
Issue invoices and notes (POST /invoices/{id}/issue, POST /notes/{id}/issue). Needed in addition to invoices:draft whenever a create call issues, which is the default. Issuing is irreversible, so it is a scope of its own. |
invoices:download |
Download the PDF and the legal XML of invoices and notes (GET /invoices/{id}/pdf, GET /invoices/{id}/xml, and the same for /notes/{id}). invoices:read does not include it. |
payments:write |
Record customer payments against issued invoices through Wonder Fatoora MCP (record_payment, which also needs invoices:read). The person behind the credential must also be allowed to record payments. |
webhooks:manage |
Register, change and delete webhook endpoints, rotate their secrets, and read and redeliver their deliveries (/webhook-endpoints). The person behind the credential must also be allowed to manage settings. |
A missing scope returns 403 insufficient_scope, with details.required_scope naming the scope the key needs (details.required_ability carries the original name, invoices:write for invoices:draft, for older clients). A key created before scopes existed holds invoices:read, invoices:draft and invoices:issue only.
A key acts as its creator
Each key acts as the user who created it (or who last rotated it), with that user's permissions at the time of each call. Creating an invoice needs the user's permission to create invoices; issuing also needs the permission to issue them. If the user's role is later restricted, the key is restricted too (403 forbidden). If the user leaves the company, the key stops working (401).
Plan
API access is part of the Business and Enterprise plans. If a company moves to a plan without API access, its keys answer 403 plan_upgrade_required until it returns; the keys can still be listed and revoked.
To bring a new customer straight to a key, give them https://fatoora.tw.sa/start?plan=enterprise&intent=api: they sign up or sign in, pay for the plan, and land on the page that creates their key. Without plan, they start on the free trial and that page shows what to subscribe to.
Expiry, rotation and revocation
- A key expires after the lifetime chosen when it was created: 30, 90, 180 or 365 days. No key lives forever.
- Rotate a key in Settings → API keys: a replacement with the same scopes and lifetime is created, and the old key stops working at the same moment. Put the new key in place right away.
- Revoke a key there at any time; it stops working immediately.
- Each key shows when and from which address it was last used, so a forgotten integration is easy to spot.
- Fourteen days, and again three days, before a live key expires, the person who created it gets an email with a button to renew it; if they have left the company, its admins get it instead. A key nobody uses any more can simply be left to expire.
An expired key answers 401 token_expired, with the day it expired in the message, the exact time in details.expired_at and the page where the company renews its keys in details.renew_url. Every other refusal (a missing, unknown or revoked key, or one whose creator has left) stays 401 unauthorized, with no reason given.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="The API key has expired"
{
"ok": false,
"error": {
"code": "token_expired",
"message": "This API key expired on 2026-11-03. Create or renew a key in the developer portal.",
"details": {
"renew_url": "https://fatoora.tw.sa/settings/developers/keys",
"expired_at": "2026-11-03T06:12:44+00:00"
}
}
}
Who can manage keys
Creating, rotating and revoking keys needs the permission to manage settings; company admins have it. A new key is shown once and never displayed again.
Apps and OAuth 2.1
An app that acts for companies other than its publisher's uses OAuth 2.1 instead of keys. Register it in Settings → Developers → My apps: you get a client_id (wfapp_…) and, for an app that runs on your servers, a client secret (wfsec_…) shown once. Each company then approves your app on a WonderFatoora consent screen, choosing the company and seeing exactly what the app asks to do.
| Endpoint | Use |
|---|---|
GET /oauth/authorize |
Send the person here to approve your app: response_type=code, client_id, redirect_uri, scope, state, code_challenge and code_challenge_method=S256. |
POST /oauth/token |
grant_type authorization_code (with code_verifier), refresh_token or client_credentials. Authenticate with HTTP Basic or client_id and client_secret in the body. |
POST /oauth/revoke |
Revoke an access or refresh token (RFC 7009). Always answers 200. |
GET /.well-known/oauth-authorization-server |
Server metadata (RFC 8414); /.well-known/oauth-protected-resource describes the API (RFC 9728). |
- PKCE is required for every app, with
S256.plainis refused. - The
redirect_urimust match one you registered exactly: no wildcards, no extra path or slash. Apps on a device may registerhttp://127.0.0.1and use any port. stateis required and comes back unchanged, withissnaming the issuer.- A code works once, for two minutes. Presenting it again revokes the tokens it gave.
- The token and revocation endpoints are rate limited per address and per app. Over the limit they answer
429with{"error": "temporarily_unavailable"}and aRetry-Afterheader: wait that many seconds. - Access tokens (
wfo_at_…,wfo_test_at_…in a sandbox) last one hour and are sent asAuthorization: Bearer. Refresh tokens rotate on every use; a reused one revokes the whole grant. - Ask for
resource=https://fatoora.tw.sa/api. A token is accepted only by the resource it was issued for. - Client credentials give a confidential app a token for its own company only.
A token acts as the person who approved the app, with that person's permissions in that company, like a key acts as its creator. It stops working when the company disconnects the app (Settings → Developers → Connected apps), when the app is deleted, or when that person leaves the company or can no longer manage its settings. A refused token answers 401 invalid_token.
Ready to build?
API access is part of the Business and Enterprise plans. Create a key in Settings → API keys and make your first call in minutes.