# Alya Books developer docs (full text) Every page of the Alya Books developer docs as plain Markdown, followed by the API quick reference. The machine-readable API is at /docs/api-reference/openapi.json. Links that start with / are on the same site as this file. Each page also exists on its own: the page's address followed by .md. --- # Alya Books developer documentation > UAE e-invoicing for your product: create PINT AE invoices, report them to the FTA through an accredited service provider, receive supplier invoices, and react to events. Source: /docs Alya Books is the e-invoicing layer for businesses in the United Arab Emirates. Your application sends plain JSON; Alya Books turns it into a PINT AE (UBL 2.1) document, validates it against the business rules, reports it to the Federal Tax Authority (FTA) through an accredited service provider (ASP), delivers it to the buyer, and tells you what happened. - [Getting started](/docs/getting-started): Create a key, send your first invoice, follow its status. - [Outgoing invoices](/docs/guides/outgoing-invoices): Tax invoices, credit and debit notes, validation, submission, delivery. - [Incoming invoices](/docs/guides/incoming-invoices): Import received documents, read their content, accept or reject them. - [Webhooks](/docs/guides/webhooks): Signed notifications for reporting, payments, imports and more. - [MCP server](/docs/guides/mcp-server): Let AI assistants operate your invoicing with about 150 tools, including the ERP intake. - [CLI](/docs/guides/cli): Script everything from a terminal or a CI pipeline. ## What the API covers | Area | Routes | Notes | | --- | --- | --- | | Invoices | `/invoices`, `/invoices/{id}/{status,validation,validate,submit,send,payments}` | Batch creation, JSON/PDF/XML download, ASP and FTA timeline | | Numbers | `/invoices/next-number`, `/invoices/verify-number`, `/settings/invoice-number-template` | Series with `{YYYY}`, `{MM}`, `{n}` tokens | | Incoming | `/import/incoming`, `/import/incoming/{importId}/...`, `/incoming` | UBL, PDF or image; validation; review; payments to suppliers | | Catalogue | `/customers`, `/products` | Contacts and items, batch import | | Custom fields | `/metadata/fields`, `/metadata/templates` | Head and line level values validated per document kind | | Webhooks | `/webhooks` | 24 events, HMAC signatures, retries, delivery log | | Account | `/me`, `/organizations/managed` | Key scopes, environment, clients of a CA firm | ## Environments There is one API. **Test keys** (`sk_test_…`) create test documents that are excluded from reports and can only be submitted to the simulated ASP; **live keys** (`sk_live_…`) create real documents. Both are created under **API access** in the app. | | URL | | --- | --- | | Production | `https://api.alyabooks.ai/api/v1` | | Self-hosted (Supabase) | `https://.supabase.co/functions/v1/api-v1` | ## The UAE mandate in one paragraph The FTA's e-invoicing programme uses the Peppol five-corner model with the PINT AE specification. Businesses exchange invoices through accredited service providers, which validate the document and report tax data to the FTA. Pilot from 1 July 2026; businesses with revenue of AED 50 million or more must appoint an ASP by 30 October 2026 and go live on 1 January 2027; the second phase (all other businesses) follows on 1 July 2027 and government entities on 1 October 2027. Alya Books is integrated with Taxilla as ASP and exposes the process through this API. ## Reading these docs with an AI assistant Every page has **Copy as Markdown**, **View as Markdown**, **Open in Claude** and **Open in ChatGPT** under its title. Each page is also plain Markdown at its address followed by `.md` (this page is `/docs/index.md`), and the whole developer site is one file at `/docs/llms-full.txt`, with the condensed guide at `/docs/llms.txt`. For an assistant that should act on your account rather than read about it, use the [MCP server](/docs/guides/mcp-server). --- # Getting started > Create an API key, send an invoice, and follow it to the FTA in ten minutes. Source: /docs/getting-started ## 1. Create an API key Sign in to Alya Books, open **API access** in the sidebar and click **New key**. Choose a name, the scopes it needs, and whether it is a **test key**. Copy the key when it is shown: it is stored hashed and cannot be displayed again. > Start with a test key. Test documents behave exactly like real ones (validation, numbering, PDF, XML, webhooks) but are marked `isTest`, are excluded from reports and can only be submitted while the organization's e-invoicing connection is in **Test (simulated)** mode. ## 2. Call the API Every request carries the key in the `X-API-KEY` header. ```bash export BASE=https://api.alyabooks.ai/api/v1 export KEY=sk_test_… curl "$BASE/me" -H "X-API-KEY: $KEY" ``` ```json { "organization": { "id": "…", "name": "Seller LLC", "trn": "100000000000003", "peppolId": "0235:1000000000" }, "key": { "name": "Integration", "scopes": ["invoices:read", "invoices:write", "…"], "isTest": true }, "environment": "test", "einvoicing": { "provider": "taxilla", "mode": "mock" } } ``` ## 3. Create an invoice ```bash curl -X POST "$BASE/invoices" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" -d '{ "invoices": [{ "buyer": { "name": "Buyer LLC", "trn": "100000000000011", "email": "ap@buyer.ae", "address": { "street": "Al Maryah Island", "city": "Abu Dhabi", "country": "AE" } }, "issueDate": "2026-09-23", "payment": { "termsDays": 30, "accountId": "AE070331234567890123456" }, "items": [ { "description": "Consulting, September", "quantity": 10, "unitPrice": 500 }, { "description": "Travel (zero rated)", "quantity": 1, "unitPrice": 850, "taxCategory": "zero_rated" } ], "submit": true, "send": { "to": ["ap@buyer.ae"] } }] }' ``` The response is `202 Accepted` with one result per invoice: ```json [{ "index": 0, "success": true, "invoiceId": "3f2e…", "invoiceNumber": "INV-0042", "status": "submitted", "total": 6100, "validation": { "validity": true, "errors": [], "warnings": [] }, "submission": { "status": "processing", "requestId": "…", "mode": "mock" }, "delivery": { "state": "skipped", "notes": ["No email provider configured…"] }, "detailsPage": "https://app.alyabooks.ai/invoices/3f2e…/edit" }] ``` Omit `invoiceNumber` and Alya Books allocates the next number of the series. Omit `submit` to keep the invoice as **validated** and submit later with `POST /invoices/{id}/submit`. Add `draftOnly: true` to store an invoice even when it fails validation. ## 4. Follow the status ```bash curl "$BASE/invoices/3f2e…/status" -H "X-API-KEY: $KEY" ``` `invoiceStatus` moves through `draft → validated → submitted → delivered → accepted` (or `rejected`). `authority[]` lists the ASP submissions with their stage, `delivery[]` the emails, `validation` the last PINT AE result. See [ASP and FTA lifecycle](/docs/reference/asp-fta-lifecycle). Prefer push over polling: create a [webhook](/docs/guides/webhooks) for `invoice.reported`, `invoice.rejected` and `invoice.paid`. ## 5. Download the documents ```bash curl "$BASE/invoices/3f2e…?format=xml" -H "X-API-KEY: $KEY" -o INV-0042.xml curl "$BASE/invoices/3f2e…?format=pdf" -H "X-API-KEY: $KEY" -o INV-0042.pdf ``` The XML is the PINT AE UBL document (`urn:peppol:pint:billing-1@ae-1`). For issued invoices it is byte-identical to what was sent to the ASP. To hand a file to a browser or another system, `GET /invoices/{id}/download-link?format=pdf` returns a signed URL that works for 15 minutes without a key. ## Conventions - JSON in and out, camelCase keys, dates as `YYYY-MM-DD` (`YYYYMMDD` is accepted on input), amounts as numbers in the invoice currency (AED by default, two decimals). - Lists: `?page=1&perPage=25&sort=issueDate&order=desc&keyword=…` and a `{ data, meta }` envelope. - Errors: `{ code, statusCode, message, extra?, field? }`; see [Errors](/docs/reference/errors). - Async operations return `202`; poll `/status` or use webhooks. - Rate limit: 600 requests per minute per key, `429` with `Retry-After` beyond that. - CA firms: send `X-Organization-Id: ` to act for a managed client; `GET /organizations/managed` lists them. ## Prefer clicking Import the Postman collection at `/docs/api.postman_collection.json` on the Alya Books site, set its `baseUrl` and `apiKey` variables, and start with the folder "Start here: ERP integration". The site's test console (`/console`) runs freight samples and a dummy data feed without any tool at all. ## Next steps - [Outgoing invoices](/docs/guides/outgoing-invoices) - [Webhooks](/docs/guides/webhooks) - [API reference](/docs/api-reference/introduction) --- # Outgoing invoices > Create tax invoices and credit notes; validate, submit, deliver, and record payments. Source: /docs/guides/outgoing-invoices ## The invoice object `POST /invoices` takes `{ invoices: [...] }` with up to 100 inputs. Each input describes one document: | Field | Meaning | | --- | --- | | `invoiceTypeCode` | UNCL 1001. Valid on UAE e-invoices: `380` tax invoice (default), `381` credit note, `480` out-of-scope invoice, `81` credit note for out-of-scope goods or services; with self-billing switched on (**Settings → Codes & regions**), `389` self-billed invoice and `261` self-billed credit note. Other codes such as `383` are only accepted when the organization switched them on, and are rejected at submission. `documentKind: "credit_note"` is accepted as an alias. | | `creditNoteReasonCode` | BTAE-03, required on credit notes: `DL8.61.1.A` to `DL8.61.1.E` or `VD` (volume discount, which needs no preceding invoice). | | `transactionTypes` | BTAE-02, any combination: `free_trade_zone`, `deemed_supply`, `profit_margin`, `summary_invoice`, `continuous_supply`, `disclosed_agent`, `e_commerce`, `exports`. Empty for a standard supply. `transactionType` (one value, or `standard`) is still accepted. Some combinations with the document kind are not allowed and some types change the document; see [Document kind and transaction type rules](#document-kind-and-transaction-type-rules). | | `currency`, `exchangeRate` | ISO 4217, default `AED`. For any other currency the rate is AED per 1 unit, at most 6 decimals (for USD, `3.6725`); leave `exchangeRate` out to use the UAE Central Bank rate. The XML then also states VAT and the total in AED; see [Other currencies](#other-currencies). | | `invoiceNumber` | Leave empty to allocate from the series. A supplied number must be unused. Accepted even when the organization option `allowManualNumbers` is off (that option only locks the number field in the app editor). | | `issueDate`, `dueDate`, `deliveryDate`, `taxPointDate` | `YYYY-MM-DD`. `payment.termsDays` computes `dueDate` when it is missing. A due date is required whenever an amount is due (IBR-127-AE); on a deemed supply it is dropped. | | `buyer` | `{ id }` of an existing customer, or an inline `{ name, trn, email, peppolId, address, reference }`. Inline buyers are matched by TRN, then by name, otherwise created. | | `items[]` | `description`, `quantity`, `unitPrice` (net), `unitCode` (UN/ECE Rec 20, default `EA`), `taxCategory` (`standard`, `zero_rated`, `exempt`, `reverse_charge`, `out_of_scope`; `margin` only with `profit_margin`), `taxRate` (defaults from the category), `itemId`, `hsnCode`, `metadata`. On `480` and `81` only `zero_rated`, `exempt` and `out_of_scope` pass validation. | | `allowances[]` | Document-level discounts and surcharges: `indicator` (`discount` / `surcharge`), `amount` (net), `reason`, `taxCategory`, `taxRate`. | | `paymentMeans[]` | Up to 10 payment instructions (BG-16): `code` (UNCL 4461; valid in the UAE: 1, 10, 20, 21, 30, 49, 54, 55, 68), `reference`, `information`, `accountId` (IBAN or account number), `accountName`, `bic`, `bankName`, `cardLastDigits`, `cardNetwork`, `cardHolder`, `mandateReference`, `debitedAccount`, `creditorId`, `instructionId`. A credit transfer (`30`) without `accountId` uses the organization's bank account from Settings. At most one card and one direct debit. | | `payment` | `termsDays`, `termsText`; and, for a single payment method, `meansCode`, `accountId`, `accountName`, `bic`, `reference` (ignored when `paymentMeans` is given). | | `precedingInvoice` | For credit and debit notes: `{ id }` of the Alya Books invoice being corrected, or `{ number, issueDate }`. | | `billingPeriod`, `delivery`, `payee`, `taxRepresentative` | Optional groups mapping to BG-14, BG-13, BG-10 and BG-11. | | `notes`, `subject`, `introduction`, `closing`, `language` | Texts; `language` (`en`, `ar`, `bilingual`) controls the PDF. The option `showTextBlocks` only decides whether the app editor shows the salutation, introduction and closing blocks on new invoices; texts you send through the API are stored as sent. | | `metadata` | Custom field values of the active template; see [Custom fields](/docs/guides/custom-fields). | | `externalId` | Your own reference, searchable with `?externalId=`. | | `projectId` | Link to a [project](/docs/guides/projects); `projectReference` (BT-11) defaults to its code. | | `supplyEmirate` | Emirate reported in box 1 of the VAT 201 (defaults to the organization's emirate); see [VAT return](/docs/guides/vat-and-exports). | | Options | `draftOnly`, `reviewMode`, `validate`, `submit`, `send`, `isTest`. | Totals are computed server-side with the same rounding as the editor: line net = quantity × unit price rounded to 2 decimals; VAT per category on (lines − allowances + charges); document total = taxable + VAT. ## Validation Every invoice is validated against PINT AE before it is stored. Blocking errors (severity `error`) reject the element with `E_VALIDATION`, unless `draftOnly` is set; warnings are stored with the invoice. ```json { "code": "E_VALIDATION", "statusCode": 400, "message": "The invoice does not pass PINT AE validation", "extra": { "errors": [{ "field": "seller_trn", "rule": "BT-31", "message": "Seller TRN is required (set in Organization Settings)", "severity": "error" }], "warnings": [{ "field": "due_date", "rule": "BT-9", "message": "Due date is recommended", "severity": "warning" }] } } ``` Seller data (BT-27 to BT-43, the trade licence and its licensing authority) comes from the organization profile, not from the payload. IBANs are checked for length, structure and check digits (rule `IBAN`); a plain account number that is not an IBAN is accepted. The [PINT AE field reference](/docs/reference/pint-ae-fields) lists every business term, its UBL path and the API field that carries it. `GET /invoices/{id}/validation` re-runs the checks; `POST /invoices/{id}/validate` also moves a draft to `validated`. ## Document kind and transaction type rules The web editor greys these out before you can pick them; the API accepts the input, runs the same validation and stores the invoice as a draft with the finding, so a client sees the rule number in `validation.errors` rather than a 4xx. | Situation | What happens | | --- | --- | | `480` or `81` with `summary_invoice`, `deemed_supply` or `profit_margin` | Validation error `IBR-157-AE`; the invoice stays a draft. | | `480` or `81` with a `standard`, `reverse_charge` or `margin` line | Validation error `IBR-122-AE`: out-of-scope documents carry only `zero_rated`, `exempt` and `out_of_scope` lines. | | `deemed_supply` | A supply without a payment. `dueDate`, `payment.termsDays` and `paymentMeans` are removed on save whatever was sent (IBR-127-AE, IBR-191-AE); the response has `dueDate: null`, `paymentMeans: []`, `paymentStatus: "nothing_due"` and `outstanding: 0`. It is never overdue, `invoice.paid` is never emitted for it and `POST /invoices/{id}/payment-link` is refused. The buyer endpoint becomes `0235:9900000097`. | | `profit_margin` | Every line must use `taxCategory: "margin"` (IBR-116-AE), which states 5 % and no VAT amount; document-level `allowances` are refused (IBR-114-AE). Send `marginVatAmount` per line for the VAT return. | | Organization not registered for VAT | Only `480` and `81` validate (IBR-134-AE). | ## Other currencies A document in another currency than AED needs an exchange rate (validation rule IBR-159-AE; at most 6 decimals, IBR-002-AE; a USD rate more than 1 % away from the 3.6725 peg gives a warning). Send `exchangeRate` to use your own, or leave it out and Alya Books fills in the UAE Central Bank rate "for VAT related obligations" for the date of supply: `taxPointDate`, else `deliveryDate`, else `issueDate`. The Central Bank publishes on working days around 18:00 UAE time; on weekends, holidays and before publication the last earlier rate is used. The response says which: `exchangeRateSource` is `cbuae` (with `exchangeRateDate`, the publication date) or `manual`. When the Central Bank has no rate for the currency the invoice fails IBR-159-AE, or stays a draft with `draftOnly`. All amounts in the payload and the response stay in the document currency. Alya Books adds the AED figures the UAE specification asks for: | XML | Value | | --- | --- | | `cbc:TaxCurrencyCode` | `AED` (set automatically; `vatAccountingCurrency` other than `AED` is rejected) | | `cac:TaxExchangeRate` | source currency, `AED`, and the rate (BTAE-04) | | second `cac:TaxTotal` | total VAT in AED (IBT-111) | | `cac:AdditionalDocumentReference` type `aedtotal-incl-vat` | total with VAT in AED (BTAE-20) | | `cac:ItemPriceExtension` on each line | line total with VAT and line VAT in AED (BTAE-10, BTAE-08); present on AED documents too | AED amounts are the document amounts × rate, rounded to 2 decimals. `GET /invoices/{id}` returns them as `aedTotals: { currency, rate, taxTotal, total }` (`null` for AED documents). The rate is stored on the invoice, so issued invoices never change when later rates are published. ## Batches and partial failure Elements succeed or fail independently. When all succeed the response is `202` with an array. When any element fails, the status mirrors the first failure (usually `400`) and `extra.results` holds one entry per element: ```json { "code": "E_VALIDATION", "statusCode": 400, "message": "…", "extra": { "results": [ { "index": 0, "success": true, "invoiceId": "…", "invoiceNumber": "INV-0042" }, { "index": 1, "success": false, "code": "E_VALIDATION", "statusCode": 400, "message": "…", "field": "invoiceNumber" } ] } } ``` > **Warning:** > Elements with `success: true` **have been created**. Reconcile with `extra.results` and retry only the failed elements; a blind retry of the whole batch creates duplicate invoices when numbers are auto-allocated. ## Submission and delivery - `submit: true` on creation, or `POST /invoices/{id}/submit`, hands a validated invoice to the ASP. The response carries the ASP request id and the first status; the invoice becomes `submitted` and is locked. - `send: { to, cc, bcc, subject, message }` on creation, or `POST /invoices/{id}/send`, emails the PDF and (for tax documents) the XML. `state` is `sent`, `skipped` (no email provider configured on the environment) or `failed`. - `reviewMode: true` creates the invoice with `reviewStatus: pending`; an approver must approve it in the app before it can be submitted. The [ASP and FTA lifecycle](/docs/reference/asp-fta-lifecycle) page explains the stages and the `authority[]` entries in `GET /invoices/{id}/status`. ## After issue Issued invoices (`submitted` and later) are immutable. What you can still do: | Action | Route | | --- | --- | | Record a payment | `POST /invoices/{id}/payments` `{ amount, date, method, reference }`; negative amounts are refunds | | Category, cost centre, archive, custom fields | `PATCH /invoices/{id}` | | Correct the amounts | Create a credit note (`381`) with `precedingInvoice: { id }` and `creditNoteReasonCode`; for an additional charge issue a new tax invoice (`380`) | | Download | `GET /invoices/{id}?format=xml|pdf|html`; `GET /invoices/{id}/download-link?format=pdf|xml|html` returns a signed URL that works for 15 minutes without a key | | PDF design | `designTemplateId` on create or `PATCH /invoices/{id}` (null = the organisation's default); `GET /design-templates` lists the designs and their layout | Drafts and validated invoices can be replaced entirely with `PUT /invoices/{id}` or deleted with `DELETE /invoices/{id}`. ## Listing and search `GET /invoices` supports `keyword` (number, buyer, TRN, order reference), `invoiceNumber`, `status`, `paymentStatus` (comma-separated: `unpaid`, `partially_paid`, `paid`, `overpaid`, `nothing_due`; the last is a deemed supply, which has no payment), `documentKind`, `buyerId`, `startIssueDate` / `endIssueDate`, `startDate` / `endDate` (creation), `includeArchived`, `isTest`, `externalId`, `sort` (`issueDate`, `createdAt`, `invoiceNumber`, `dueDate`, `total`) and `order`. Test keys only see test documents unless `isTest=false` is passed explicitly. --- # Invoice numbers > Number series, templates, previews and verification. Source: /docs/guides/invoice-numbers Every organization has one number series per document kind: `invoice`, `credit_note`, `debit_note`, `proforma`, `quote`, `order_confirmation`, `reminder` and `customer` (default `C-{n}`). The app also keeps an `item` series (default `IT-{n}`) for item numbers; both the customer and item series are edited in the app under **Settings → Outgoing invoices → Document Numbering**. Numbers are allocated inside the database under a row lock, so two integrations creating invoices at the same time never get the same number. ## Templates A template is text with tokens: | Token | Meaning | | --- | --- | | `{n}` | The counter, zero-padded to `padLength` digits (required) | | `{YYYY}`, `{YY}` | Year of the issue date | | `{MM}`, `{DD}` | Month and day | Examples: `INV-{n}` → `INV-0042`; `{YYYY}/{MM}/{n}` → `2026/09/0042`; `CN-{YY}-{n}` → `CN-26-0007`. `resetYearly: true` restarts the counter at 1 when the year changes. faktoora-style templates ending in `-id` or `/id` (for example `YYYY/MM/id`) are accepted and converted. ## Routes ```bash # Preview without allocating curl "$BASE/invoices/next-number?series=invoice" -H "X-API-KEY: $KEY" # → { "invoiceNumber": "INV-0043", "series": "invoice", "template": "INV-{n}", "counter": 43, "padLength": 4 } # Is a number free? curl "$BASE/invoices/verify-number?value=INV-0043" -H "X-API-KEY: $KEY" # → { "available": true, "invoiceNumber": "INV-0043", "series": "invoice" } # Read and change series settings (scopes settings:read / settings:write) curl "$BASE/settings/invoice-number-template" -H "X-API-KEY: $KEY" curl -X PUT "$BASE/settings/invoice-number-template" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "series": "invoice", "template": "INV-{YYYY}-{n}", "padLength": 5, "resetYearly": true }' ``` ## Behaviour on create - No `invoiceNumber` in the payload: the next number of the series is allocated at insert time. - `invoiceNumber` supplied: it must be unused (`verify-number` semantics); after the insert the series counter moves past it when the number matches the template. - Numbers are allocated even for test documents and drafts, so gaps can appear when a draft is deleted. UAE VAT rules require sequential numbering of issued tax invoices; keep drafts out of the invoice series by creating quotes or proformas first when the document may not be issued. ## Manual numbers in the app The organization option `allowManualNumbers` (default `false`) controls whether people may type a document number in the Alya Books editor. When it is `false`, the number field is read-only and every new invoice, credit or debit note, proforma, quote and other document takes the next number of its series, in order. It does not restrict integrations: an `invoiceNumber` supplied through the API or an import is accepted either way, as long as it is unused. Customer numbers are not affected. ```bash curl "$BASE/settings/options" -H "X-API-KEY: $KEY" # → { ..., "reserveNumbersInDrafts": true, "allowManualNumbers": false, ... } curl -X PATCH "$BASE/settings/options" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "allowManualNumbers": true }' ``` --- # Custom fields > Metadata templates: define your own fields on documents and lines, validate them, print them, and get them back in webhooks. Source: /docs/guides/custom-fields Custom fields let you carry information Alya Books does not model itself (purchase order numbers, cost codes, project phases, sales representatives) on invoices, quotes, order confirmations, proformas and incoming invoices, at document ("head") level and per line. ## Concepts - A **field** has a `key` (lowercase letters, digits, underscores; used in the API), a `label` (and optional Arabic label), a `type` (`text`, `number`, `date`, `boolean`, `select` with options), a `level` (`head` or `line`), and flags `required` and `showOnPdf`. - A **template** groups fields for one `documentKind` (`invoice`, `credit_note`, `debit_note`, `proforma`, `quote`, `order_confirmation`, `incoming_invoice`). Exactly one template per kind can be **active**. - Values travel as `metadata: { key: value }` on the document and on each line, are validated against the active template when created through the API or the editor, and are returned on every read and in webhook payloads. ## Define fields and a template ```bash curl -X POST "$BASE/metadata/fields" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "label": "PO number", "type": "text", "level": "head", "required": true, "showOnPdf": true }' # → { "id": "f1…", "key": "po_number", … } curl -X POST "$BASE/metadata/fields" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "label": "Cost code", "type": "select", "level": "line", "options": [{ "value": "OPS", "label": "Operations" }, { "value": "RND", "label": "R&D" }] }' # → { "id": "f2…", "key": "cost_code", … } curl -X POST "$BASE/metadata/templates" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "name": "Standard", "documentKind": "invoice", "fieldIds": ["f1…", "f2…"], "isActive": true }' ``` ## Use them ```json { "invoices": [{ "buyer": { "id": "…" }, "metadata": { "po_number": "PO-4711" }, "items": [{ "description": "Consulting", "quantity": 10, "unitPrice": 500, "metadata": { "cost_code": "OPS" } }] }] } ``` Validation errors come back as `E_VALIDATION` with `field: "metadata"` (or `items[i].metadata`) and `extra.fields[]` naming the offending keys. Values are coerced: numbers from strings, `yes`/`no` to booleans, `YYYYMMDD` to `YYYY-MM-DD`. Unknown keys are kept as-is, so you can attach data that is not part of the template. Head-level values on issued invoices can still be changed with `PATCH /invoices/{id}` (`mergeMetadata` defaults to `true`), because they are not part of the reported document. Fields flagged `showOnPdf` print under "Additional information" on the PDF. ## Routes | Route | Purpose | | --- | --- | | `GET/POST /metadata/fields`, `GET/PATCH/DELETE /metadata/fields/{id}` | Field definitions | | `GET/POST /metadata/templates`, `GET/PATCH/DELETE /metadata/templates/{id}` | Templates with their fields | | `POST /metadata/templates/{id}/activate` | Make a template the active one for its kind | Scopes: `metadata:read`, `metadata:write`. Business users manage the same objects under Settings → Documents → Custom fields. --- # Recurring invoices > Templates with a schedule that generate invoices through the same path as POST /invoices. Source: /docs/guides/recurring-invoices A recurring invoice is an invoice template plus a schedule and automation flags. The daily jobs (02:00 and 18:45 UAE time) turn due templates into invoices; each generated invoice goes through the normal creation path (PINT AE validation, number allocation, optional submission and email), so everything documented for [outgoing invoices](/docs/guides/outgoing-invoices) applies. ## Create and activate ```bash curl -X POST "$BASE/recurring-invoices" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" -d '{ "name": "Monthly retainer – Buyer LLC", "buyer": { "id": "…" }, "items": [{ "description": "Retainer", "quantity": 1, "unitPrice": 5000, "unitCode": "MON" }], "subject": "Monthly retainer", "payment": { "termsDays": 14 }, "projectId": "…", "schedule": { "frequency": "monthly", "dayOfMonth": 1, "startDate": "2027-01-01", "periodOffset": -1, "periodTextTemplate": "for {{periodMonth}} {{periodYear}}" }, "automation": { "validate": true, "submit": true, "send": true, "sendTo": ["ap@buyer.ae"] }, "status": "active" }' ``` Templates are created as `draft` unless `status: "active"` is given. `POST /recurring-invoices/{id}/activate`, `/pause` and `/cancel` change the status; `PATCH` edits the template (`items` replaces all lines; schedule changes recompute `nextRunOn`). ## Schedule | Field | Meaning | | --- | --- | | `frequency` | `weekly`, `monthly`, `quarterly`, `half_yearly`, `yearly`, `custom` | | `intervalCount` | Every N periods (default 1) | | `dayOfMonth` | 1–31, or `0` for the last day; clamped in shorter months | | `weekday` | ISO weekday 1 (Monday) to 7 (Sunday), weekly only | | `customDates` | Explicit dates for `custom` | | `startDate`, `endDate`, `maxOccurrences` | Bounds; the template becomes `completed` when they are reached | | `periodOffset` | Billed period relative to the run: `-1` previous (in arrears), `0` current, `1` next (in advance) | | `issueDateOffsetDays` | Issue date = run date + offset | | `fillInvoicingPeriod` | Write the billed period into BT-73/BT-74 (default true) | | `periodTextTemplate` | Appended to the subject; tokens `{{periodStart}}`, `{{periodEnd}}`, `{{periodMonth}}`, `{{periodYear}}`, `{{runDate}}`, `{{n}}` | `GET /recurring-invoices/{id}/preview` returns the next occurrences with their periods and the exact `POST /invoices` payload of the next run. The MCP resource `einvoicing://reference/recurring-schedules` documents the same fields. ## Runs - The daily jobs process templates whose `nextRunOn` is today or earlier, at most twelve catch-up occurrences per template. - `POST /recurring-invoices/{id}/run` (body `{ "date": "YYYY-MM-DD" }` optional) generates now and returns `201` with `invoiceId`, or `422` with the error. - `GET /recurring-invoices/{id}/runs` lists runs (`success`, `failed`, `skipped`) with the invoice id and number. One invoice per template and date is guaranteed; failed runs are retried at the next run. - Generated invoices carry `recurringInvoiceId` and `externalId: "recurring::"`; filter with `GET /invoices?recurringInvoiceId=`. - `POST /invoices/{id}/make-recurring` copies an existing invoice into a draft template. ## Webhooks and MCP Events `recurring_invoice.generated` (with `invoiceId`, `nextRunOn`) and `recurring_invoice.failed` (with `error`). MCP tools: `list_recurring_invoices`, `get_recurring_invoice`, `create_recurring_invoice`, `update_recurring_invoice`, `activate_recurring_invoice`, `pause_recurring_invoice`, `cancel_recurring_invoice`, `run_recurring_invoice_now`, `list_recurring_runs`, `preview_recurring_invoice`, `make_invoice_recurring`, `delete_recurring_invoice`. --- # VAT return and accounting exports > Tax rates, the VAT 201 worksheet, saved returns and import files for Zoho Books, Xero, QuickBooks Online and Tally. Source: /docs/guides/vat-and-exports ## Tax rates Every organization has one rate per UAE VAT category, created on first use. Rates carry the default exemption reason (BT-121 code, BT-120 text) written into the XML tax breakdown for non-standard categories. ```bash curl "$BASE/settings/tax-rates" -H "X-API-KEY: $KEY" curl -X POST "$BASE/settings/tax-rates" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "category": "zero_rated", "name": "Zero rated – export of goods", "exemptionReasonCode": "VATEX-AE-EXPORT", "exemptionReason": "Export of goods outside the GCC" }' ``` Invoice lines accept `taxExemptionReasonCode` and `taxExemptionReason`; when omitted, the rate's default (or a built-in text per category) is used. The standard category never carries a reason. ## VAT 201 worksheet `GET /vat/worksheet?from=2027-01-01&to=2027-03-31` (or `?date=2027-02-15` to pick the organization's tax period containing that date) returns the boxes of the UAE VAT return: | Box | Source | | --- | --- | | 1a–1g | Standard-rated lines of issued invoices (`submitted`, `delivered`, `accepted`; not test, not archived), per emirate. The invoice's `supplyEmirate` wins; otherwise the organization's emirate. Credit notes are negative. Document allowances and charges are included. | | 2, 6, 7 | Manual figures (tourist refunds, imports, import adjustments) taken from the saved return of the period. | | 3, 10 | Reverse-charge lines of accepted incoming invoices, VAT at the standard rate. | | 4, 5 | Zero-rated and exempt lines of issued invoices. | | 9 | Standard-rated lines of accepted incoming invoices with their VAT. | | 8, 11, 12, 13, 14 | Computed totals and net VAT (`totals.netVat`, `totals.payable`). | Each box carries `amount`, `vat`, `adjustmentAmount`, `adjustmentVat`, `source` and `documents`. `info` lists reverse-charge and out-of-scope sales that are shown but not reported. `format=csv` returns the worksheet as CSV. Tax is computed per invoice and category exactly as on the documents (taxable amount rounded, then multiplied by the rate). Documents in another currency are converted to AED at their own exchange rate, per invoice and category (taxable and VAT each rounded to fils). A rate of exactly 1 on a non-AED document (the former default) is ignored in favour of the stored UAE Central Bank rate for the document's date. Documents with no usable rate are left out of the boxes and listed in `info.unconverted.sales` / `info.unconverted.purchases` (`id`, `number`, `currency`, `date`); fetch their rates or set one on the document and request the worksheet again. `GET /vat/periods` lists recent periods with filing due dates and the status of the saved return; the period settings (`months` 1 or 3, `startMonth`, `dueDays`) live on the organization. ## Saved returns ```bash curl -X POST "$BASE/vat/returns" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" -d '{ "periodStart": "2027-01-01", "periodEnd": "2027-03-31", "status": "filed", "reference": "EMT-2027-Q1-0042", "manual": { "importsAmount": 12000, "importsVat": 600, "adjustments": { "1b": { "amount": -500, "vat": -25 } } } }' ``` One return per period (upsert). Saving recomputes the worksheet and stores it in `figures`; `status: "filed"` records `filedAt`, keeps the figures in `filedFigures` and **locks the period**. `GET /vat/returns`, `GET /vat/returns/{id}`, `DELETE` (drafts only). ## Locked periods A filed return locks its period, and the organization can set lock dates (`lockDateAll`, `lockDateNonAdmin` in `GET/PATCH /settings/options`). Creating, changing or deleting an invoice, credit or debit note (issue date or date of supply), an accepted incoming invoice, or a payment (lock dates only) dated in a locked period returns `409 period_locked`: ```json { "code": "period_locked", "statusCode": 409, "message": "Invoice INV-0042 is dated 2027-02-15, in the VAT period 2027-01-01 to 2027-03-31, whose VAT return is filed. Use a date on or after 2027-04-01, or ask an owner or admin to reopen the return.", "extra": { "lockSource": "vat_return_filed", "periodStart": "2027-01-01", "periodEnd": "2027-03-31", "vatReturnId": "…", "firstOpenDate": "2027-04-01" } } ``` Submitting or sending a draft dated in a locked period returns the same error. Received documents are never refused; an incoming invoice accepted after its period was locked counts in the first open period (`vatRecoveryDate`). - `GET /vat/check-date?date=2027-02-15`: lock state of a date for your key (`locked`, `blocked`, `lockSource`, `firstOpenDate`). - `GET /vat/periods`: each period has `locked` and `lockSource`; the response adds `lockDates` and `fiscalYear`. - `POST /vat/returns/{id}/reopen` with `{ "reason": "…", "acknowledgeVoluntaryDisclosure": true }` reopens a filed return (409 `lock_date_covers_period` while `lockDateAll` covers it). - The `periods:manage` scope is needed to reopen, to change lock dates, and to pass `lockDateNonAdmin`. - Webhooks: `vat_return.filed`, `vat_return.reopened`, `lock_date.changed`. ## Accounting exports ```bash curl "$BASE/exports/accounting?system=xero&kind=sales&from=2027-01-01&to=2027-03-31&taxCode.standard=5%25%20VAT%20on%20Income&accountCode=200" \ -H "X-API-KEY: $KEY" -o sales-q1.csv curl "$BASE/exports/accounting?system=tally&kind=purchases&date=2027-02-01" -H "X-API-KEY: $KEY" -o purchases.xml ``` | `system` | Format | Layout | | --- | --- | --- | | `zoho` | CSV | Zoho Books invoice / bill import, one row per line, place of supply, TRN, project name | | `xero` | CSV | Xero sales invoice / bill template (`*ContactName`, `*InvoiceNumber`, `*AccountCode`, `*TaxType`, …), project as tracking option | | `quickbooks` | CSV | QuickBooks Online invoice / bill import (`*InvoiceNo`, `*Item(Product/Service)`, `*ItemTaxCode`, …) | | `tally` | XML | Tally `Import Data` envelope with Sales / Purchase / Credit Note / Debit Note vouchers, party ledger, per-category sales or purchase ledger, VAT ledger | `kind=sales` exports issued invoices (credit notes negative); `kind=purchases` exports accepted incoming invoices. Override tax code labels with `taxCode.=`, the income/expense account or ledger with `accountCode`, the Tally VAT ledger with `vatLedger`. `format=json` wraps the file in `{ filename, mime, content }` for MCP clients. `GET /exports/accounting/systems` lists the systems with their default tax codes. ## MCP and CLI Tools: `list_tax_rates`, `create_tax_rate`, `update_tax_rate`, `list_vat_periods`, `get_vat_worksheet`, `list_vat_returns`, `save_vat_return`, `reopen_vat_return`, `check_period_date`, `export_accounting`. CLI: `alyabooks vat worksheet --from … --to … [--csv -o file]`, `alyabooks vat save --from … --to … --filed --reference …`, `alyabooks export accounting --system xero --kind sales --from … --to … -o file`, `alyabooks tax-rate list`. See also the [rounding reference](/docs/reference/rounding). --- # Incoming invoices > Import received e-invoices and PDFs, read their content, validate, review and pay them. Source: /docs/guides/incoming-invoices Incoming invoices are documents your organization receives from suppliers. Alya Books records them from three channels: the app (upload), the organization's inbox address and the ASP's Peppol inbox, and this API. ## Import `POST /import/incoming` accepts: - `multipart/form-data` with a `file` field (plus optional `extra` files, `sender`, `reference`), - raw `application/xml` or `application/pdf` bodies (`?filename=` optional), - JSON `{ "xml": "" }` or `{ "base64": "…", "filename": "invoice.pdf", "mime": "application/pdf" }`. ```bash curl -X POST "$BASE/import/incoming" -H "X-API-KEY: $KEY" -F file=@supplier-invoice.xml -F reference=PO-4711 ``` What happens next depends on the file: | Input | Processing | | --- | --- | | UBL XML (Invoice or CreditNote) | Parsed, validated against PINT AE and against your organization's TRN as buyer, stored with lines, VAT breakdown and embedded attachments; a visualisation is rendered. | | PDF or image | Stored; structured data is extracted by the AI extraction service when it is configured on the environment, otherwise the document waits in **draft** for manual completion. | The response is `202` when the document was recorded (even with warnings), or `422` with `code: "E_VALIDATION"` when it failed (duplicate supplier TRN and number, wrong buyer TRN, unparsable file). In both cases the import is logged and `extra`/the body carry `importId`, `incomingInvoiceId`, `status` and `validation[]`. ## Reading an import All sub-resources take the import id; the incoming invoice id is accepted too. | Route | Returns | | --- | --- | | `GET /import/incoming/{importId}` | Import record and invoice summary | | `…/status` | `status` (`pending`, `accepted`, `failed`), validation and visualisation status, review and payment status | | `…/validation` | `validity`, `errors[]`, `warnings[]` (PINT AE rules such as `BT-31`, `BT-131`, `BR-CO-15`) | | `…/content` | Full content: `seller`, `buyer`, `lineItems[]`, `taxes[]`, `paymentTerms`, `paymentMeans`, `delivery`, `metadata` | | `…/content/summary`, `/lineitems`, `/seller`, `/buyer`, `/taxes`, `/paymentterms`, `/paymentmeans`, `/delivery` | One part of the content | | `…/content/pdf` | The original PDF, or the visualisation rendered to PDF (needs an HTML-to-PDF service; without one it answers 503, so use `?format=html`) | | `…/source` | The original file as received | | `…/attachments`, `…/attachment/{id}`, `…/attachment/{id}/file` | Files that came with the document | `GET /import/incoming` lists imports (`status`, `channel`, `keyword`, `startDate`, `endDate`); `GET /incoming` lists the incoming invoices themselves (`status`, `paymentStatus`, `supplierId`, `keyword`, issue date range). ## Other currencies For documents in another currency than AED the incoming invoice carries `exchangeRate` (AED per 1 unit) and `exchangeRateSource`: `document` when the supplier's XML stated it (BTAE-04, `cac:TaxExchangeRate`; rates stated from AED or with `MathematicOperatorCode` `Divide` are turned around), `cbuae` when Alya Books took the UAE Central Bank rate for the delivery date or else the issue date (`exchangeRateDate` is the publication date), or `manual` when a reviewer set it. `aedTotals` is VAT and the total in AED at that rate; `statedAed` holds the AED amounts from the supplier's XML (IBT-111, BTAE-20). Validation warns when the supplier stated no rate (IBR-159-AE), no AED amounts, or AED VAT that does not match VAT × rate (IBR-175-AE). PDFs and images read by AI get the Central Bank rate for the issue date. Send `exchangeRate` in `PATCH` to change it while the invoice is in review (`null` removes it); changing `currency` without a rate looks up the Central Bank rate again. ## Review `PATCH /import/incoming/{importId}` corrects the content while the invoice is a draft or in review, and changes the review state and bookkeeping fields at any time. Content edits return `409` once the invoice is accepted or rejected; send the decision in a separate request. ```json { "invoiceNumber": "GOS-2026-0006", "dueDate": "2026-10-26", "seller": { "name": "Gulf Office Supplies LLC", "street": "Al Quoz Industrial Area 3", "city": "Dubai", "country": "AE", "trn": "100555666777003", "email": "billing@gulfoffice.ae" }, "buyer": { "name": "SAB Engineering LLC", "city": "Dubai", "trn": "100123456789003" }, "delivery": { "name": "Warehouse 4", "street": "Plot 598-1024, Dubai Investments Park", "city": "Dubai", "deliveryNote": "Gate 2" }, "lineItems": [ { "description": "A4 copier paper, 80gsm", "itemDescription": "Box of 5 reams", "quantity": 3, "unitCode": "BX", "unitPrice": 95, "taxCategory": "S", "taxRate": 5 }, { "description": "Toner cartridge HP 26A", "quantity": 4, "unitPrice": 285, "taxRate": 5 } ], "paymentTerms": "Net 30", "earlyPaymentDiscounts": [{ "days": 10, "percent": 2 }], "paymentMeans": [{ "code": "30", "accountId": "AE560330000001234567890", "accountName": "Gulf Office Supplies LLC", "bic": "EBILAEAD" }] } ``` - Parties (`seller`, `buyer`, `delivery`) take `name`, `title`, `firstName`, `lastName`, `legalForm`, `street`, `additional`, `city`, `emirate`, `postalCode`, `country`, `email`, `phone`, `contactName`, `trn`, `tin`, `peppolId`, `licenseNumber`, `gln`, `mandateReference` and, for delivery, `deliveryNote`. A party you send replaces the stored one. - `lineItems` replaces all lines and recalculates the VAT breakdown and the totals. `vatBreakdown` (`category`, `rate`, `taxable`, `tax`, `exemptionReason`) replaces the breakdown and recalculates the totals; `subtotal`, `taxTotal` and `total` override them. - `paymentMeans` replaces all payment means (`code` from UNTDID 4461, `accountId` or `iban`, `accountName`, `bic`, `bankName`, `reference`, `information`, `mandateReference`). `earlyPaymentDiscounts` holds up to three tiers. - `GET …/content` returns the same shapes, so you can read, change and send back. Review decisions and bookkeeping: ```json { "status": "accepted" } { "status": "rejected", "rejectionReason": "Duplicate of INV-2201" } { "status": "draft" } { "status": "archived", "archiveNote": "Paid outside Alya Books" } { "categoryId": "…", "costCentreId": "…", "projectId": "…", "notes": "…", "metadata": { "cost_code": "OPS-3" } } ``` Accepted invoices are locked in the app (members need the *edit accepted incoming invoices* permission to change them). Payments to the supplier are recorded with `POST /import/incoming/{importId}/payments` once the invoice is accepted; `paymentStatus` and `paidAmount` update automatically. Imports that were not accepted can be removed with `DELETE`. ## Events `incoming.created`, `incoming.validated`, `incoming.accepted`, `incoming.rejected` and `incoming.paid` are emitted as [webhooks](/docs/guides/webhooks). The payload carries the incoming invoice summary, including `importId`. ## Receiving without the API - **Inbox address**: each organization can create `@in.alyabooks.ai` under Settings → Sending. Attachments of emails to that address run through the same pipeline with channel `email`. - **Peppol**: the app's *Fetch from Peppol* action, and automatically every 15 minutes for UAT and live connections, asks the ASP for documents addressed to the organization's participant id (channel `peppol`). Both channels produce the same imports and the same webhooks, so an integration built on `GET /import/incoming` sees every document regardless of how it arrived. ## Simulate deliveries in Test mode While the organization's e-invoicing connection is in Test (simulated) mode, you can make Alya Books deliver supplier invoices to you, to exercise your integration end to end (webhooks, polling, accept/reject, payments) without waiting for real suppliers: ```bash curl -X POST "$BASE/incoming/simulate" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "scenario": "mixed_vat", "supplier": "it", "count": 2 }' ``` | Scenario | Result | | --- | --- | | `standard`, `with_pdf`, `mixed_vat`, `reverse_charge`, `credit_note`, `foreign_currency` | Valid PINT AE documents; an incoming invoice in status `draft` (to review). `with_pdf` embeds the supplier's PDF in the XML, which the app shows as the supplier copy | | `wrong_buyer`, `totals_mismatch` | Import recorded with `validationStatus: failed` and the issues | | `duplicate` | Repeats the supplier's previous number; the import fails as a duplicate | Use `contactId` instead of `supplier` to have one of your contacts (with a TRN) act as the supplier. The response lists the import and incoming invoice ids; `incoming.created`, `incoming.validated` and failure webhooks fire as for real deliveries. In UAT and Live mode the endpoint returns `409`. --- # Customers > Manage the contacts you invoice and receive from. Source: /docs/guides/customers Customers are contacts of the organization. The same records serve as buyers on outgoing invoices and as suppliers on incoming ones (`type`: `buyer`, `seller`, `both`). The app shows these types with the organization's own words (default Customer and Supplier; for example Client and Vendor). The words are the `terminology` option of `GET` / `PATCH /settings/options`; the stored and accepted `type` values never change: ```bash curl -X PATCH "$BASE/settings/options" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "terminology": { "buyer": { "singular": "client", "plural": "clients" }, "seller": { "singular": "vendor", "plural": "vendors" } } }' ``` Words are stored in lower case, up to 30 characters each; a role you leave out keeps its current words, and `"terminology": null` resets everything. The same option also names received supplier documents in the app: `"incoming": { "singular": "bill" }` (one of `incoming invoice`, `bill`, `receipt`, `purchase invoice`). ## Fields | Field | Notes | | --- | --- | | `name`, `nameAr`, `tradingName` | Legal name (BT-44), Arabic name for bilingual PDFs, trading name | | `type` | `buyer` (default), `seller`, `both` | | `trn` | 15-digit Tax Registration Number (BT-48); validated | | `peppolId`, `peppolSchemeId` | Peppol participant id, `0235:`, where the TIN is the first 10 digits of the TRN; `peppolId` holds the 10-digit TIN without the prefix and `peppolSchemeId` is `0235` | | `customerNumber` | Your reference; `generateNumber: true` allocates the next number of the `customer` series | | `email`, `contactName`, `phone`, `website` | Contact details; `email` is the default recipient when sending; if it is empty, the contact person with `receivesInvoices` (primary first) is used | | `address` | `street`, `additional`, `city`, `emirate`, `postalCode`, `country` (ISO 3166-1 alpha-2, default `AE`) | | `paymentTermsDays`, `defaultCurrency`, `notes`, `isActive` | Defaults applied in the editor | | `isCompany`, `legalForm`, `firstName`, `lastName` | Company (default) with legal form, or natural person | | `category` | Free-text group, e.g. `wholesale` | | `defaultPriceListId` | Price list applied to this customer (see [Products](/docs/guides/products)) | | `peppolVerifiedAt` | Set by `POST /customers/{id}/verify-peppol` when the SML confirms the registration | ## Routes ```bash curl "$BASE/customers?keyword=buyer&type=buyer&perPage=50" -H "X-API-KEY: $KEY" curl -X POST "$BASE/customers" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "name": "Buyer LLC", "trn": "100000000000011", "email": "ap@buyer.ae", "address": { "city": "Dubai", "country": "AE" }, "generateNumber": true }' curl -X PATCH "$BASE/customers/{id}" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" -d '{ "paymentTermsDays": 45 }' curl -X DELETE "$BASE/customers/{id}" -H "X-API-KEY: $KEY" ``` Changing `trn` or `name` updates the contact's draft and validated invoices; a value set differently on one invoice is kept, and submitted invoices never change. `DELETE` removes a contact without invoices; a contact that has invoices is deactivated instead (`deactivated: true` in the response) so history stays intact. `GET /customers/next-number` previews the next customer number and `GET /customers/verify-number?value=` checks one. `POST /customers/import` creates up to 500 contacts in one call and returns a per-element result (`201` when all succeed, `207` otherwise). ## Inline buyers on invoices When you create an invoice with a `buyer` object instead of `buyer.id`, Alya Books matches an existing contact by TRN, then by exact name, and otherwise creates one from the inline data. The `customer.created` webhook fires in that case as well. ## Contact persons and addresses ```bash curl -X POST "$BASE/customers/{id}/contact-persons" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "salutation": "Ms", "firstName": "Amal", "lastName": "Hassan", "position": "Finance manager", "email": "amal@buyer.ae", "isPrimary": true }' curl -X POST "$BASE/customers/{id}/addresses" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "kind": "delivery", "label": "Warehouse", "street": "Al Quoz 3", "city": "Dubai", "emirate": "Dubai", "isDefault": true }' ``` `GET`, `PATCH` and `DELETE` on `/customers/{id}/contact-persons/{personId}` and `/customers/{id}/addresses/{addressId}` complete the set. One person per contact is `isPrimary`; one address per `kind` (`delivery`, `billing`, `other`) is `isDefault`. Recurring invoices reference them by `contactPersonId` (email recipient) and `deliveryAddressId` (BT-70 to BT-80). ## Peppol lookup `GET /customers/peppol-lookup?participantId=0235:1000000000` (a bare 15-digit TRN is accepted and converted to its TIN) validates the format and, when DNS is available, checks the Peppol SML for the participant. The response carries `valid`, `registered` (`true`, `false`, or `null` when the check could not run), `checked` (`sml`, `smk`, `format-only`) and a message. `POST /customers/{id}/verify-peppol` runs the same check for a customer and stores `peppolVerifiedAt` on success. In `mock` ASP mode every well-formed id is reported as registered. --- # Products and bundles > Items used on invoice lines, with VAT category, unit and price. Source: /docs/guides/products Products (and services) are the items the invoice editor and the API can put on lines. Referencing an item by `itemId` on an invoice line keeps reporting per product consistent; the line still carries its own description, price and VAT values. ## Fields | Field | Notes | | --- | --- | | `name`, `nameAr`, `description` | Names; the Arabic name is used on bilingual PDFs | | `type` | `product` (default) or `service` | | `sku`, `barcode` | Your codes; `GET /products/valid-code?sku=` checks that a SKU is free, `POST` returns `409` on a duplicate | | `unitCode` | UN/ECE Rec 20 (`EA`, `HUR`, `KGM`, …), default `EA` | | `unitPrice`, `currency` | Net price, default currency `AED` | | `taxCategory`, `taxRate` | `standard` (5 %), `zero_rated`, `exempt`, `reverse_charge`; the rate defaults from the category | | `hsnCode` | HS code (commodity classification, BT-158) | | `itemNumber` | Your item number; the `item` number series allocates them in the app | | `gln` | GS1 GLN / GTIN, 8–14 digits | | `costPrice` | Purchase price; the response adds `margin` in percent of the net price | | `trackInventory`, `inventoryCount`, `lowStockThreshold` | Stock is reduced by the invoiced quantity when an invoice leaves draft (not for test documents or credit notes) | | `category` | Free-text group | | `isActive` | Inactive items stay on old invoices but are hidden from pickers | ## Routes ```bash curl "$BASE/products?keyword=consulting&sort=unitPrice&order=desc" -H "X-API-KEY: $KEY" curl -X POST "$BASE/products" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "name": "Consulting hour", "type": "service", "sku": "CONS-H", "unitCode": "HUR", "unitPrice": 500 }' curl -X POST "$BASE/products/import" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" -d '{ "products": [ … ] }' ``` `DELETE /products/{id}` deactivates items that are used on invoice lines instead of deleting them. ## Price lists A price list holds net prices per item with optional quantity breaks, a validity window, and either `isDefault: true` (applies to every customer without an assignment) or a list of `customerIds`. A customer can also carry `defaultPriceListId`. ```bash curl -X POST "$BASE/price-lists" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" -d '{ "name": "Wholesale 2027", "validFrom": "2027-01-01", "validTo": "2027-12-31", "items": [{ "itemId": "…", "unitPrice": 90 }, { "itemId": "…", "unitPrice": 80, "minQuantity": 10 }], "customerIds": ["…"] }' curl "$BASE/products/{id}/price?customerId=…&quantity=12&date=2027-03-01" -H "X-API-KEY: $KEY" ``` `GET /products/{id}/price` resolves the price the editor would use: customer-assigned lists (highest `priority` first), then default lists, then the item's own price; within a list the row with the largest `minQuantity` that fits wins. The response names the `source` and the list. `PUT /price-lists/{id}/items` and `PUT /price-lists/{id}/customers` replace the prices or the assignment in one call. Invoice inputs still take an explicit `unitPrice` per line; price lists are a lookup for your integration (and for the editor), not a server-side override. ## Bundles A bundle is a set of items with quantities. `isSplittable: true` (default) means the bundle is invoiced as one line per component, each priced through the price lists; otherwise it is one line at the bundle price (`priceMode: "sum"` of components or `"fixed"` with `fixedPrice`, plus the bundle's own VAT category). ```bash curl -X POST "$BASE/bundles" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "name": "Starter kit", "items": [{ "itemId": "…", "quantity": 3 }, { "itemId": "…", "quantity": 1 }] }' curl "$BASE/bundles/{id}/lines?quantity=2&customerId=…" -H "X-API-KEY: $KEY" ``` `GET /bundles/{id}/lines` returns ready-made `items[]` for `POST /invoices`. MCP: `expand_bundle`, `get_product_price`, `create_price_list`, `create_bundle`. --- # Projects > Group invoices, quotes and incoming invoices by project and read totals per project. Source: /docs/guides/projects A project is a first-class resource with a code, a customer, a status, dates and a budget. Its code doubles as the PINT AE project reference (BT-11, `cac:ProjectReference/cbc:ID`) on invoices. ## Routes ```bash curl "$BASE/projects?status=active&includeTotals=true" -H "X-API-KEY: $KEY" curl -X POST "$BASE/projects" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "name": "Tower A fit-out", "customerId": "…", "startDate": "2027-01-01", "budget": 250000 }' curl "$BASE/projects/{id}/documents" -H "X-API-KEY: $KEY" curl "$BASE/projects/{id}/invoices?paymentStatus=unpaid" -H "X-API-KEY: $KEY" ``` | Field | Notes | | --- | --- | | `code` | Unique per organization. Allocated from the `project` number series when omitted (`P-{n}` by default). | | `name`, `nameAr`, `description`, `color` | Display | | `customerId` | Optional contact | | `status` | `active` (default), `on_hold`, `completed`, `archived` | | `startDate`, `endDate`, `budget`, `currency` | Planning data | | `metadata` | Free JSON | | `totals` (response) | `invoiceCount`, `invoicedTotal` (issued invoices minus credit notes), `paidTotal`, `outstandingTotal`, `incomingCount`, `incomingTotal` (accepted supplier invoices), `quoteCount`. Test and archived documents are excluded. | `DELETE /projects/{id}` deletes a project without documents; a project with linked documents is archived instead (`archived: true`). ## Linking documents - `POST /invoices`: set `projectId` on the input. `projectReference` (BT-11) defaults to the project code and can be overridden. `PATCH /invoices/{id}` accepts `projectId` on issued invoices as well (it is housekeeping data, not part of the reported XML). - `GET /invoices?projectId=` filters the list; `projectId=none` returns invoices without a project. - Creating a project whose code matches existing `projectReference` values links those invoices automatically. - Recurring invoices carry `projectId` and pass it to every generated invoice. ## Webhooks and MCP `project.created` fires on creation. MCP tools: `list_projects`, `get_project`, `create_project`, `update_project`, `delete_project`, `list_project_documents`. --- # Accounting connectors > QuickBooks Online, Xero and Odoo: OAuth or credentials, pull contacts and invoices, push issued invoices, and the link table that keeps both sides in step. Source: /docs/guides/connectors Alya Books syncs with QuickBooks Online, Xero and Odoo (plus the older Zoho Books and Tally connectors). A connector is set up in the app under **Connectors**; the API can list connectors, run syncs and inspect the record links. Credentials never leave the database: `GET /connectors` strips `config.credentials`, and in the app only the server reads them (members see the settings without tokens or keys). ## Connection models | Provider | Auth | Where it is set up | | --- | --- | --- | | `quickbooks` | OAuth 2.0 (Intuit) | App → Connectors → Connect. The `oauth-initiate` function builds the authorize URL, `oauth-callback` exchanges the code and stores tokens; needs `QUICKBOOKS_CLIENT_ID` / `QUICKBOOKS_CLIENT_SECRET` and `APP_URL` on the server. | | `xero` | OAuth 2.0 | Same flow with `XERO_CLIENT_ID` / `XERO_CLIENT_SECRET`. | | `odoo` | URL, database, login, API key (JSON-RPC) | App → Connectors → Connect Odoo (form). **Test** signs in and stores the user id. | Every provider also has a **sandbox** mode (`config.mode = "sandbox"`) that serves sample data and accepts pushes without contacting anyone. Use it to exercise sync and push in CI or on a demo organization. ## Run a sync ```bash curl -X POST "$BASE/connectors/$CONNECTOR_ID/sync" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "mode": "incremental", "pushInvoices": true }' ``` `mode` is `full`, `incremental` (changes since `lastSyncAt`) or `date_range` (`dateFrom`, `dateTo`). `pullContacts`, `pullInvoices` and `pushInvoices` override the connector's configuration for this run; `pushSince` (YYYY-MM-DD) limits which issued invoices are pushed (default: the connector's `push_since`, else the last 30 days). Only keys with `settings:write` may sync; Zoho Books and Tally keep their own functions and return `409`. The response is a summary: ```json { "contacts": { "imported": 12, "updated": 3, "skipped": 1, "errors": 0 }, "invoices": { "imported": 8, "skipped": 2, "errors": 0 }, "pushed": { "created": 5, "skipped": 0, "errors": 1, "details": [{ "invoiceId": "…", "number": "INV-2027-00012", "error": "Unknown tax code" }] }, "companyName": "Acme Trading LLC", "messages": [] } ``` The sync is recorded in the connector's history and emits `connector.synced`. ## What moves - **Contacts** are matched by TRN, then case-insensitively by name; a match links the existing contact instead of creating one. Later runs update linked contacts. - **Invoices pulled** become drafts with `sourceSystem` = provider and `sourceId` = the remote id. Lines keep quantity, unit price, VAT category (mapped from the remote tax) and the remote number. A remote invoice whose number already exists in Alya Books is skipped. - **Invoices pushed** are issued invoices (`submitted`, `delivered`, `accepted`; not test, not archived, not themselves imported from that provider) issued on or after `pushSince`. Each is created once; the link table prevents a second push. Tax codes and income account come from the connector config (`tax_code_standard`, `tax_code_zero_rated`, `tax_code_exempt`, `income_account`). ## Record links `GET /connectors/{id}/links?entityType=invoice` pages the `Alya Books id ↔ remote id` pairs with `direction` (`pull` or `push`), `remoteUpdatedAt` and `lastSyncedAt`. Use it to reconcile or to find the QuickBooks/Xero/Odoo id of a Alya Books invoice. ## Scheduling Connectors with `sync_frequency` = `daily` are synced by the daily jobs (`daily-jobs`, at 02:00 and 18:45 UAE time). Manual connectors sync only on request. Tokens are refreshed automatically before each run; a refresh failure sets the connector to `error` with the message in `lastSyncMessage`, and **Test** in the app (or a new OAuth round trip) repairs it. ## MCP and CLI MCP tools `list_connectors` and `sync_connector`. CLI: `alyabooks connector list`, `alyabooks connector sync --mode incremental --push`. --- # ERP intake: CargoWise and pushed messages > Point your ERP's outbound integration at one endpoint. Alya Books stores the message, maps it, validates it, creates and submits the invoice, and keeps a log you can replay. Source: /docs/guides/erp-intake Integrating CargoWise? Start with the [CargoWise integration kit](/docs/guides/cargowise-integration). The intake endpoint takes the message your ERP already produces when an invoice is posted and turns it into a Alya Books invoice through the same path as `POST /invoices`: numbering, PINT AE validation, submission to the service provider and webhooks are all shared. Every message is stored with its raw body, so a mapping fix can be replayed without asking the ERP to resend. ``` POST {BASE}/erp/{connector}/messages ``` | Connector | Content type | What it reads | |---|---|---| | `cargowise` | any (see below) | A CargoWise **UniversalTransaction** (AR invoice, credit or debit note) as eAdaptor or eAdaptor Next delivers it: bare, in a UniversalInterchange, a SOAP envelope, a JSON wrapper, base64, gzip or a form post | | `json` | `application/json` | One invoice in the [InvoiceInput](/docs/api-reference/introduction) shape, keyed by `externalId` | ## Authentication for push transports Every API route accepts the key in `X-API-KEY`. The intake routes also accept **HTTP Basic authentication** with the API key as the password (any user name), because outbound integrations such as CargoWise eAdaptor can only send basic credentials to a partner endpoint: ``` Authorization: Basic base64("api:sk_test_…") ``` Use a **test key** while integrating: the invoices it creates are test documents and never reach the live service provider. ## What the CargoWise endpoint accepts eAdaptor (SOAP) and eAdaptor Next (REST) can deliver the same Universal XML in different shapes, depending on how the outbound endpoint is configured. `POST /erp/cargowise/messages` accepts any `Content-Type` and finds the document itself: | Shape | Example | |---|---| | Bare Universal XML | `UniversalInterchange` or `UniversalTransaction` as the body (`application/xml`, `text/xml`) | | SOAP envelope | The document as elements in the `Body`, as escaped text in a field such as `Payload`, or as base64 of the gzip-compressed stream | | JSON | `{ "MessageId": "…", "Payload": "" }`; the XML may sit in any field, as text or base64 | | Base64 | The body is base64 of the XML, or of the gzip-compressed XML | | Form post | `application/x-www-form-urlencoded` with the XML in a field | | Compressed | `Content-Encoding: gzip` | Up to four layers are unwrapped. The message log shows how each message arrived (for example `json:Payload → base64 → gzip`). A body with no Universal XML inside is stored as a failed message with the reason. **Message id.** Send your message id in `X-Message-Id` (also read: `Message-Id`, `X-Correlation-Id`, `X-Request-Id`), or as `MessageId` / `TrackingId` in a JSON wrapper or the SOAP header. Alya Books stores it on the message and returns it as `messageId`, so both sides can trace a document. Resending the same document is still recognised by its CargoWise number, whatever the message id. ## Configure the connector once Charge codes decide the VAT treatment, so the mapping is set per organisation before the first message. Have your tax adviser sign it. ```bash curl -X PUT "$BASE/erp/connectors/cargowise" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" -d '{ "chargeMap": { "FRT": { "category": "zero_rated", "itemType": "S", "serviceAccountingCode": "996521", "label": "Ocean freight" }, "THC": { "category": "standard", "itemType": "S", "serviceAccountingCode": "996751" }, "DUTY": { "category": "out_of_scope", "itemType": "S", "serviceAccountingCode": "996799", "label": "Customs duty disbursement" } }, "vatCodeMap": { "S": "standard", "Z": "zero_rated", "O": "out_of_scope", "E": "exempt", "RC": "reverse_charge" }, "submit": true, "alwaysAck": false, "designTemplateId": "6f0b0123-…", "mappingSignedOffBy": "A. Adviser, Firm LLP" }' ``` As shown, `chargeMap` replaces the stored map, which is what a settings screen sending every row needs. To add or change a few codes and keep the rest, send `"mode": "merge"`; delete codes in merge mode with `"removeChargeCodes": ["OLD"]` (and `removeVatCodes` for the VAT-code map). Scripts and assistants should always use merge. The audit log keeps the previous maps, so any change can be undone. Resolution order for each charge line: `chargeMap[chargeCode]`, then `vatCodeMap[vatCode]`, then the built-in VAT code list (S, Z, E, O, RC…), then `defaultCategory`. When nothing decides, the message fails and names the charge code, so a new charge code in the ERP never slips through as the wrong category. `designTemplateId` gives the connector's invoices a PDF design (unless the message names one); choose one with the **Logistics** layout to print the job number, house bill and Incoterms in a Shipment panel. `GET /design-templates` lists the ids. `GET /erp/connectors` lists the connectors, the endpoint URL for each and your settings. ## Send a message ```bash curl -X POST "$BASE/erp/cargowise/messages" -u "api:$KEY" -H "Content-Type: application/xml" --data-binary @universal-transaction.xml ``` What comes back: | Status | Meaning | |---|---| | `202` with `status: processed` | The invoice was created (and submitted when `submit` is on). `invoiceId`, `invoiceNumber` and the full create result are in the body. | | `200` with `status: duplicate` | The same document was already processed (same branch, type and number). Nothing was created; the original invoice is referenced. | | `400` `E_VALIDATION` | The message could not be mapped (unknown charge code, missing customer) or the invoice failed a PINT AE rule. The message is stored as `failed` with the reason; `extra.message` has it. | | `202` with `status: failed` | Same as above when `?ack=always` or the connector's `alwaysAck` is on: push transports that retry on non-2xx get an acknowledgement and the failure stays in the log. | The job, house bill and branch from the message land in the invoice's custom fields `job`, `house_bill` and `branch` (define them under Settings → Custom fields so they are validated and shown). ## Check before you send `POST /invoices/validate?connector=cargowise` takes the same XML, maps it and runs every rule without creating anything. The answer carries the validation report, mapping notes and a preview of the mapped invoice (type, dates, buyer, totals, lines with categories). The same route takes JSON without the `connector` parameter. ## Try it without writing code The **test console** on the Alya Books site (`/console`, under Developers in the menu) sends the freight samples behind this guide, CargoWise XML or JSON, through the dry run, the intake and `POST /invoices`. It runs in the browser without a key (same reader, mapper and rules, against a sample seller), or against your sandbox with a test key that stays in the page. Its **dummy data feed** generates a day of invoices with planted mistakes at a rate you choose, sends each one, and checks the answer against what the sample promises; with **send each twice**, every valid document is posted again and must come back as a duplicate. **Add the sample charge codes** adds those of the samples' charge codes (FRT, AIR, THC, TRK, WHS, DOC, DUTY) that the CargoWise connector does not have yet; codes already mapped are left exactly as they are. ## The message log - `GET /erp/messages?status=failed` lists what needs attention; `keyword` searches document numbers, keys and errors. - `GET /erp/messages/{id}` returns the message with its raw body (the document inside, for a wrapped message), the outcome, the sender's `messageId` and the `envelope` it arrived in. - `POST /erp/messages/{id}/replay` processes a failed message again after the charge map or the customer master was fixed. A processed message cannot be replayed, so a replay never creates a duplicate. Each outcome is written to the audit log, and the invoice events (`invoice.created`, `invoice.submitted`, `invoice.reported`, `invoice.rejected`) fire as for any other invoice, so the ERP's write-back can run from webhooks. ## What the CargoWise reader looks at Local element names only; namespaces and wrappers are ignored. The full table, with PINT AE terms and rules, is in [CargoWise field mapping](/docs/reference/cargowise-field-mapping). | Alya Books field | Elements read (first found) | |---|---| | Number | `TransactionInfo/Number`, `TransactionNumber`, `InvoiceNumber` | | Type | `TransactionType`: `INV` → tax invoice, `CRD`/`CRN` → credit note, `DBN` → debit note; a three-digit code is taken as is | | Dates | `TransactionDate`/`PostDate`, `DueDate` (ISO or dd/mm/yyyy) | | Currency, rate | `Currency/Code`, `ExchangeRate` (used for non-AED) | | References | `JobNumber`/`Job/Key`, `HouseBill`, `Branch/Code`, Incoterms from `ShipmentIncoTerm/Code`, `IncoTerm/Code` or `Incoterms` (an unknown code is left out with a note); credit notes: `OriginalTransactionNumber` and the UAE reason code in `CreditNoteReason` (or `ReasonCode`), which a credit note must carry | | Customer | `Organization/FullName`, `RegistrationNumber[@Type="TRN"]`, `Address/Address1`, `City`, `State/Code`, `Country/Code`, `Email`, `Code` | | Lines | `PostingJournalCollection/PostingJournal` or `TransactionLineCollection/TransactionLine`: `ChargeCode`, `Description`, `Quantity`, `Amount`, `VATCode` | The Universal XML schema is published by WiseTech to licensees; send us a sample from your tenant and we confirm the mapping against it before UAT. --- # Pay-by-link and bank feeds > Create payment links for invoices, receive gateway confirmations, and reconcile bank transactions with automatic match suggestions. Source: /docs/guides/payments-and-bank-feeds ## Payment gateways Gateways are configured in the app under **Settings → Payments** (owners and admins): Stripe, Telr, PayTabs, Tap, or the built-in **mock** gateway for development. `GET /payment-gateways` lists them without credentials and returns the supported providers with their credential fields. Each gateway has `mode` = `test` or `live`. Test documents can only use test-mode or mock gateways. ## Create a payment link ```bash curl -X POST "$BASE/invoices/$INVOICE_ID/payment-link" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "gatewayId": null, "expiresInDays": 30 }' ``` Rules enforced by the database function behind it: - the invoice must be issued (not `draft`), not a credit note, and have an outstanding amount; - `gatewayId` defaults to the organization's default active gateway; - a pending, unexpired link for the same invoice is returned instead of creating a second one. The response contains `url` (`{APP_URL}/pay/{token}`), `amount`, `currency`, `status`, `expiresAt`. Share the URL or let Alya Books add it: while a pending link exists, `POST /invoices/{id}/send` adds a **Pay now** button to the email. `GET /invoices/{id}/payment-links` lists an invoice's links; `GET /payment-links?status=pending` pages all of them; `POST /payment-links/{id}/cancel` cancels a pending link. ## Settlement The public pay page calls the `pay-link` function, which asks the gateway for a checkout session and redirects the buyer. Confirmation arrives on `payment-webhook?provider=&org=`, verified with the gateway's signature (Stripe `Stripe-Signature`, Telr and Tap HMAC secrets, PayTabs server key). The `settle_payment_link` function then: 1. marks the link `paid` with the provider reference (idempotent: a repeated notification is ignored); 2. inserts a payment on the invoice with `method: online`, `source: gateway`; 3. updates `paidAmount` / `paymentStatus` and emits `payment_link.paid` (and `invoice.paid` when fully settled). If the buyer returns before the webhook fired, the pay page reconciles by fetching the status from the gateway. The mock gateway settles through **Simulate payment** on the page. ## Bank feeds Transactions arrive from a live connection (Lean Technologies, Tarabut, or the mock feed; synced twice a day and on demand) or from statement rows you post yourself: ```bash curl -X POST "$BASE/bank/import" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" -d '{ "currency": "AED", "rows": [ { "Date": "2027-02-03", "Description": "TRF INV-2027-00012 ACME TRADING", "Credit": "5250.00", "Debit": "", "Reference": "FT27034ABC" } ] }' ``` Rows are objects keyed by the bank's own column names; Alya Books recognises date, amount or debit/credit, description, reference, counterparty name and IBAN in the usual spellings. A row id is derived from date, amount and text, so re-importing overlapping statements never duplicates. The response counts `imported`, `duplicates`, `suggested`, `autoMatched`. `POST /bank/sync` pulls the live connections (`connectionId` to restrict) and, with `rematch: true`, rescores everything still open. ## Matching For every transaction the matcher scores open documents in the same direction (credits against issued invoices, debits against accepted incoming invoices): | Signal | Score | | --- | --- | | Invoice number found in description/reference | +0.6 | | Amount equals the outstanding balance | +0.3 (invoice total +0.2, partial +0.1) | | Counterparty name overlaps the customer/supplier name | up to +0.2 | | TRN in the text | +0.2 | | Booked within 60 days after the issue date | +0.05 | Candidates ≥ 0.5 make the transaction `suggested` (top five kept in `suggestions`); ≥ 0.9 with a clear lead over the runner-up is `matched` automatically when the organization's `bankAutoMatch` switch is on. `GET /bank/transactions?status=suggested` pages transactions with their candidates. Confirm, change or undo: ```bash curl -X POST "$BASE/bank/transactions/$TXN_ID/match" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" -d '{ "invoiceId": "…" }' curl -X POST "$BASE/bank/transactions/$TXN_ID/unmatch" -H "X-API-KEY: $KEY" curl -X POST "$BASE/bank/transactions/$TXN_ID/ignore" -H "X-API-KEY: $KEY" ``` A confirmed match inserts a payment (`source: bank_feed`, the bank reference, the transaction amount unless `amount` is given) and emits `bank.transaction_matched`; unmatching deletes that payment and returns the transaction to `unmatched`. ## MCP and CLI MCP: `create_payment_link`, `list_payment_links`, `cancel_payment_link`, `list_bank_transactions`, `match_bank_transaction`, `ignore_bank_transaction`, `import_bank_statement`, `sync_bank_feeds`. CLI: `alyabooks pay-link create `, `alyabooks pay-link list --status pending`, `alyabooks bank transactions --status suggested`, `alyabooks bank match --invoice-id `, `alyabooks bank import -f rows.json`, `alyabooks bank sync --rematch`. --- # Webhooks > Receive signed HTTP notifications when invoices are reported, paid, received and more. Source: /docs/guides/webhooks Webhooks push events to an https endpoint you control. Create them in the app under **API access → Webhooks** or with the API (scope `webhooks:manage`). ## What an invoice event carries Every `invoice.*` payload has the invoice header (id, number, type, status, dates, currency, totals, buyer, `isTest`, `metadata`, `externalId`) and two blocks that let a receiver act without calling back: - `validation`: `validity` and the blocking `errors[]` from the last check, each with `rule`, `field`, `message` and a public `docsUrl`. - `rejection`: on `invoice.rejected`, the `stage` (`taxilla`, `buyer` or `fta`), the provider's `error` text, the `submissionId` and when it happened; `null` otherwise. For the explained version of each error (API field path, English and Arabic explanation and fix) call `GET /invoices/{id}/validation`. ## Create a subscription ```bash curl -X POST "$BASE/webhooks" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" -d '{ "name": "ERP sync", "targetUrl": "https://erp.example.com/hooks/einvoicing", "events": ["invoice.reported", "invoice.rejected", "invoice.paid", "incoming.validated"], "headers": { "Authorization": "Bearer my-inbound-token" } }' ``` The response includes `secret` (`whsec_…`) **once**. Use `["*"]` to subscribe to every event. Targets must be public `https` hosts; private networks, localhost and cloud metadata addresses are rejected, and DNS answers pointing to private ranges are refused at delivery time. ## Events | Event | When | | --- | --- | | `invoice.created` | An invoice, credit note or debit note was created (draft or validated) | | `invoice.sent` | Emailed or logged as sent | | `invoice.submitted` | Handed to the ASP | | `invoice.delivered` | Accepted by the buyer's access point | | `invoice.reported` | Accepted by the FTA (status `accepted`) | | `invoice.rejected` | Rejected by the ASP, the buyer's access point or the FTA | | `invoice.paid` | Payments cover the full amount | | `invoice.overdue` | The daily job flagged the invoice as overdue | | `invoice.deleted` | A draft was deleted | | `validation.completed` | An API-triggered validation finished (`validate: true` or `POST …/validate`) | | `incoming.created`, `incoming.validated`, `incoming.accepted`, `incoming.rejected`, `incoming.paid` | Received documents | | `quote.created`, `quote.sent`, `order_confirmation.created`, `proforma.sent`, `reminder.created`, `reminder.sent`, `document.sent` | Other documents | | `customer.created`, `product.created` | Catalogue | | `payment_link.paid` | A pay-by-link payment was confirmed by the gateway (`data.invoiceId`, `data.paymentId`, `data.provider`) | | `bank.transaction_matched` | A bank transaction was matched to an invoice or supplier invoice and a payment recorded | | `connector.synced` | An accounting connector finished a sync (`data.provider`, counts, errors) | `GET /webhooks/events` returns the same list with descriptions. ## Delivery format ```http POST /hooks/einvoicing HTTP/1.1 Content-Type: application/json User-Agent: Webhook-Delivery/1.0 X-Webhook-Event: invoice.paid X-Webhook-Delivery: 7c0d… (unique per delivery attempt series) X-Webhook-Event-Id: 9b1e… (unique per event; the same for every subscriber) X-Webhook-Attempt: 1 X-Webhook-Signature: t=1700000000,v1=5f1a… { "id": "9b1e…", "event": "invoice.paid", "createdAt": "2026-09-23T10:15:00Z", "organizationId": "…", "data": { "id": "3f2e…", "invoiceNumber": "INV-0042", "status": "accepted", "paymentStatus": "paid", "total": 6100, "paidAmount": 6100, "currency": "AED", "buyer": { "id": "…", "name": "Buyer LLC", "trn": "100000000000011" }, "isTest": false } } ``` Respond with any `2xx` within 10 seconds. Redirects are not followed. ## Verify the signature `v1` is the hex HMAC-SHA256 of `"."` with your secret. Reject timestamps older than five minutes to prevent replays. ```ts import { createHmac, timingSafeEqual } from "node:crypto"; export function verify(secret: string, header: string, rawBody: string): boolean { const parts = Object.fromEntries(header.split(",").map((p) => p.split("="))); if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex"); return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? "")); } ``` ```python import hmac, hashlib, time def verify(secret: str, header: str, raw_body: bytes) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) if abs(time.time() - int(parts["t"])) > 300: return False expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", "")) ``` Use the raw request body, not a re-serialised JSON object. ## Retries and monitoring Failed deliveries (non-2xx, timeout, connection error) are retried after at least 1 min, 5 min, 30 min, 2 h and 12 h (a job sends due retries every 5 minutes); after six attempts the delivery is marked `failed`. A webhook that accumulates 25 exhausted deliveries in a row is paused with a `disabledReason`; fix the endpoint and set `isActive: true` to resume. | Route | Purpose | | --- | --- | | `GET /webhooks/{id}/deliveries?status=failed` | Delivery log with attempts, response status, error, duration | | `GET /webhooks/{id}/deliveries/{deliveryId}` | One delivery including its payload | | `POST /webhooks/{id}/deliveries/{deliveryId}/retry` | Retry now | | `POST /webhooks/{id}/test` | Send a `webhook.test` event and return the result | | `POST /webhooks/{id}/rotate-secret` | New signing secret | Deliveries are idempotent from your side when you key on `X-Webhook-Event-Id`: the same event may be delivered more than once after a retry. ## How delivery runs Events are written to an outbox by database triggers the moment the change is committed. A worker pushes them out immediately after API and app actions (submission, sending, imports, payments) and every minute from the scheduler, so a delivery normally arrives within seconds and never later than a minute. --- # MCP server > Use Alya Books from Claude, Cursor, ChatGPT or any MCP client: invoices, incoming documents, the ERP intake, customers, products, webhooks and VAT as tools. Source: /docs/guides/mcp-server Alya Books exposes its API as a [Model Context Protocol](https://modelcontextprotocol.io) server. Every tool maps onto one REST route, so validation, permissions and behaviour are identical to the API. ## Endpoint ```text POST https://api.alyabooks.ai/api/v1/mcp ``` Transport: **Streamable HTTP**, stateless. Requests are JSON-RPC 2.0 messages (single or batched); responses are JSON. The server never opens a server-sent events stream, so `GET /mcp` answers `405`. Headers on every request: ```http Content-Type: application/json Accept: application/json X-API-KEY: sk_live_… X-Organization-Id: (optional, CA firms) ``` Authentication is the same API key as the REST API; a tool is only callable when the key has its scope (`invoices:write`, `incoming:read`, `webhooks:manage`, …). Test keys make the assistant work on test documents. ## Connect from claude.ai or ChatGPT (sign-in) Chat apps that add MCP servers as **custom connectors** cannot send an API key, so the endpoint also supports OAuth 2.1, the way the MCP specification describes: 1. In Claude (Settings → Connectors → Add custom connector) or ChatGPT (connectors in developer mode), enter the MCP URL: `https:///functions/v1/api-v1/mcp` (`https://api.alyabooks.ai/api/v1/mcp` once the API domain is live). 2. The app registers itself and opens Alya Books's sign-in. After signing in, you choose the **organisation**, **Test or Live** and the **permissions**, then click **Allow**. 3. The app receives a one-hour access token and a refresh token; it refreshes on its own. Revoke it any time under **API access → API keys → Connected apps**. What the server publishes, for client developers: | | | |---|---| | Protected resource metadata | `{API}/.well-known/oauth-protected-resource` (also linked from the `WWW-Authenticate` header of every 401 from `/mcp`) | | Authorization server metadata | `{API}/.well-known/openid-configuration` and `{API}/.well-known/oauth-authorization-server`; issuer `{API}` | | Registration | `POST {API}/oauth/register` (RFC 7591), public clients only (`token_endpoint_auth_method: none`), https redirect URIs or http on localhost | | Authorization | the Alya Books app's `/oauth/authorize`; authorization code with PKCE `S256` required | | Token | `POST {API}/oauth/token`: `authorization_code` and `refresh_token`; refresh tokens rotate on every use; codes are single-use and a reused code revokes what it produced | | Revocation | `POST {API}/oauth/revoke` (RFC 7009) | Access tokens (`oat_…`) work on `/mcp` only; the REST API keeps using API keys. A token belongs to one organisation, so `X-Organization-Id` cannot switch it. Clients that only look for discovery documents at the host's root, rather than under the API's path, need the API domain to forward `/.well-known/…` to the API. ## Connect a client with an API key ```json { "mcpServers": { "alyabooks": { "type": "http", "url": "https://api.alyabooks.ai/api/v1/mcp", "headers": { "X-API-KEY": "sk_live_…" } } } } ``` ```json { "mcpServers": { "alyabooks": { "url": "https://api.alyabooks.ai/api/v1/mcp", "headers": { "X-API-KEY": "sk_live_…" } } } } ``` ```ts import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const transport = new StreamableHTTPClientTransport(new URL("https://api.alyabooks.ai/api/v1/mcp"), { requestInit: { headers: { "X-API-KEY": process.env.ALYABOOKS_API_KEY! } }, }); const client = new Client({ name: "my-agent", version: "1.0.0" }); await client.connect(transport); const { tools } = await client.listTools(); const result = await client.callTool({ name: "list_invoices", arguments: { status: "accepted", perPage: 5 } }); ``` ```bash curl -X POST https://api.alyabooks.ai/api/v1/mcp -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_account", "arguments": {} } }' ``` Claude Code: `claude mcp add --transport http alyabooks https://api.alyabooks.ai/api/v1/mcp --header "X-API-KEY: sk_live_…"`. ## Tools 149 tools in 17 groups: 72 only read, the rest change data. Tool descriptions state their scope; `tools/list` returns full JSON Schemas for the arguments and the annotations below. | Group | Tools | | --- | --- | | Invoice creation | `create_tax_invoice`, `create_credit_note`, `create_debit_note`, `create_invoices_batch`, `replace_draft_invoice` | | Invoice management | `list_invoices`, `get_invoice`, `get_invoice_status`, `get_invoice_validation`, `validate_invoice`, `submit_invoice`, `send_invoice`, `download_invoice`, `delete_draft_invoice`, `update_invoice_housekeeping`, `list_design_templates`, `list_invoice_payments`, `record_invoice_payment` | | Invoice numbers | `get_next_invoice_number`, `verify_invoice_number`, `get_invoice_number_settings`, `update_invoice_number_settings` | | Incoming invoices | `simulate_incoming_invoice`, `import_incoming_invoice`, `list_incoming_imports`, `list_incoming_invoices`, `get_import`, `get_import_status`, `get_import_validation`, `get_import_content`, `get_import_summary`, `get_import_line_items`, `get_import_seller`, `get_import_buyer`, `get_import_taxes`, `get_import_payment_terms`, `get_import_payment_means`, `get_import_delivery`, `get_import_attachments`, `get_import_source`, `review_incoming_invoice`, `update_incoming_invoice`, `record_incoming_payment`, `delete_import` | | Customers | `list_customers`, `get_customer`, `create_customer`, `update_customer`, `delete_customer`, `get_next_customer_number`, `verify_customer_number`, `lookup_peppol_participant`, `verify_customer_peppol`, `list_contact_persons`, `add_contact_person`, `update_contact_person`, `delete_contact_person`, `list_contact_addresses`, `add_contact_address`, `update_contact_address`, `delete_contact_address`, `import_customers` | | Products | `list_products`, `get_product`, `create_product`, `update_product`, `delete_product`, `check_product_code`, `get_product_price`, `list_bundles`, `get_bundle`, `create_bundle`, `update_bundle`, `delete_bundle`, `expand_bundle`, `list_price_lists`, `get_price_list`, `create_price_list`, `update_price_list`, `delete_price_list`, `import_products` | | Webhooks | `list_webhooks`, `list_webhook_events`, `create_webhook`, `update_webhook`, `delete_webhook`, `test_webhook`, `list_webhook_deliveries`, `retry_webhook_delivery` | | Custom fields | `list_custom_fields`, `create_custom_field`, `update_custom_field`, `delete_custom_field`, `list_field_templates`, `create_field_template`, `update_field_template`, `activate_field_template`, `delete_field_template` | | Projects | `list_projects`, `get_project`, `create_project`, `update_project`, `delete_project`, `list_project_documents` | | Recurring invoices | `list_recurring_invoices`, `get_recurring_invoice`, `create_recurring_invoice`, `update_recurring_invoice`, `activate_recurring_invoice`, `pause_recurring_invoice`, `cancel_recurring_invoice`, `run_recurring_invoice_now`, `list_recurring_runs`, `preview_recurring_invoice`, `make_invoice_recurring`, `delete_recurring_invoice` | | Settings | `list_email_templates`, `save_email_template`, `get_options` | | VAT | `list_tax_rates`, `create_tax_rate`, `update_tax_rate`, `list_vat_periods`, `get_vat_worksheet`, `list_vat_returns`, `save_vat_return`, `reopen_vat_return`, `check_period_date`, `export_accounting` | | ERP intake | `dry_run_invoice`, `dry_run_erp_message`, `send_erp_message`, `list_erp_messages`, `get_erp_message`, `replay_erp_message`, `list_erp_connectors`, `update_erp_connector` | | Integrations | `list_connectors`, `sync_connector` | | Payments | `create_payment_link`, `list_payment_links`, `cancel_payment_link` | | Bank feeds | `list_bank_transactions`, `match_bank_transaction`, `ignore_bank_transaction`, `import_bank_statement`, `sync_bank_feeds` | | Account | `get_account`, `list_managed_organizations` | **Marked destructive** (`destructiveHint`, so clients ask before running them): `submit_invoice`, `send_invoice`, `delete_draft_invoice`, `delete_import`, `delete_customer`, `delete_contact_person`, `delete_contact_address`, `delete_product`, `delete_bundle`, `delete_price_list`, `delete_webhook`, `delete_custom_field`, `delete_field_template`, `delete_project`, `delete_recurring_invoice`, `reopen_vat_return`, `send_erp_message`, `replay_erp_message`. Submitting, sending and replaying reach the FTA or a buyer and cannot be undone; deletes remove data. The dry runs (`dry_run_invoice`, `dry_run_erp_message`) are marked read-only: they store nothing. **Test or live.** Every `tools/call` result carries `_meta: { "einvoicing/environment": "test" }` or `"live"`, from the key in use. With a test key nothing reaches the live service provider. **Not available to assistants**, on purpose: API keys, team members, the service-provider connection, the company profile, and the adviser's sign-off on an ERP charge map. Those are set in the app by the people responsible. Tool results contain the API response as text and as `structuredContent`; API errors are returned as tool results with `isError: true` and the usual `{ code, statusCode, message, extra }` envelope, with each validation error's path, explanation and fix, so the assistant can correct its input. ## Resources | URI | Content | | --- | --- | | `einvoicing://reference/pint-ae-fields` | PINT AE business terms with UBL paths and the API field that carries each | | `einvoicing://reference/editor-fields` | Optional field groups and their input keys | | `einvoicing://reference/tax-categories` | VAT categories, rates and UBL codes | | `einvoicing://reference/statuses` | Invoice, payment, review and incoming statuses; document kinds; transaction types | | `einvoicing://reference/webhook-events` | Event catalogue | | `einvoicing://openapi.json` | The OpenAPI document | | `einvoicing://docs/llms.txt` | Short integration guide for language models | ## Example: a CargoWise message from a chat > "Here is a UniversalTransaction from our CargoWise UAT. Check it, tell me what fails, fix our charge map and send it." The assistant calls `dry_run_erp_message` with the XML and reads the mapping notes and failed rules (for example "Charge PSC has no mapping"). It proposes the mapping, calls `update_erp_connector` after you agree, runs the dry run again until it is valid, then calls `send_erp_message`. With a live key, `send_erp_message` is marked destructive, so the client asks you before it runs. If the message had already failed in the log, `replay_erp_message` processes it again instead. ## Example conversation > "Create a tax invoice for Buyer LLC (TRN 100000000000011) for 10 consulting hours at AED 500, due in 30 days, submit it and tell me when the FTA accepts it." The assistant calls `create_tax_invoice` with `submit: true`, reads `validation` and `submission` from the result, then polls `get_invoice_status` until `invoiceStatus` is `accepted` or `rejected`. With a webhook on `invoice.reported` your own system learns about it at the same moment. ## Limits - Stateless: no sessions, no subscriptions, no server-initiated messages. Prompts are not provided (`prompts/list` is empty). - Same rate limit as the API (600 requests per minute per key). - `download_invoice` with `format: "pdf"` returns a download link (`url`, `expiresAt`, `filename`) that works for 15 minutes without a key, so an assistant can hand the PDF to the user instead of a very long base64 string. `delivery: "inline"` returns the base64 instead, and `delivery: "link"` gives a link for XML or HTML too. XML and HTML come back as text by default; `get_import_source` returns binary files as base64 in `structuredContent`. --- # CLI > @alyabooks/cli: manage invoices, imports, customers, products and webhooks from the terminal or CI. Source: /docs/guides/cli `@alyabooks/cli` is a thin, dependency-free command-line client for the REST API. Use it for scripts, cron jobs and CI pipelines; use the [MCP server](/docs/guides/mcp-server) for AI-driven workflows. ## Install and configure ```bash npm install -g @alyabooks/cli # Node.js 18 or later alyabooks config set api-key sk_live_… alyabooks config get # key is masked alyabooks me ``` | Setting | `alyabooks config set …` | Environment variable | | --- | --- | --- | | API key | `api-key ` | `ALYABOOKS_API_KEY` | | API URL | `api-url ` | `ALYABOOKS_API_URL` (default `https://api.alyabooks.ai/api/v1`; a Supabase project URL is accepted and completed) | | Client organization | `organization ` | `ALYABOOKS_ORGANIZATION_ID` (CA firms) | Configuration is stored in `~/.alyabooks/config.json` (mode 600). Environment variables win over the file, and `--api-key`, `--api-url`, `--organization` win for one call. ## Output Commands print a table (lists) or key/value lines (single objects). `--json` prints the raw response: ```bash alyabooks invoice list # table + "page 1 of 3 · 57 items" alyabooks invoice list --json | jq '.data[].invoiceNumber' ``` Exit codes: `0` success, `1` usage error or partial failure (batch results are printed), `2` API error, `3` authentication error (401). ## Commands ### invoice ```bash alyabooks invoice list --status accepted --start-issue-date 2026-01-01 --per-page 50 --sort issueDate --order desc alyabooks invoice create -f invoice.json # file = one invoice, an array, or { invoices: [...] } alyabooks invoice create -f invoice.json --wait # poll status until accepted/rejected alyabooks invoice get 3f2e… alyabooks invoice status 3f2e… alyabooks invoice validation 3f2e… alyabooks invoice validate 3f2e… alyabooks invoice submit 3f2e… --wait --wait-timeout 120000 alyabooks invoice send 3f2e… --to ap@buyer.ae --cc me@seller.ae --subject "Invoice INV-0042" alyabooks invoice download 3f2e… --format xml|pdf|html -o INV-0042.xml alyabooks invoice payments 3f2e… alyabooks invoice pay 3f2e… --amount 5250 --date 2026-10-01 --method bank_transfer --reference "TRF 123" alyabooks invoice delete 3f2e… ``` Filters map one-to-one onto the API query parameters in kebab-case: `--payment-status`, `--document-kind`, `--buyer-id`, `--include-archived`, `--is-test`, `--external-id`, `--keyword`. ### number ```bash alyabooks number next --series credit_note alyabooks number verify --value INV-0043 alyabooks number settings alyabooks number set --template "INV-{YYYY}-{n}" --pad-length 5 --reset-yearly ``` ### import (incoming invoices) ```bash alyabooks import incoming -f supplier.xml --wait # XML is sent as text, other files as base64 alyabooks import incoming -f scan.pdf --reference PO-4711 alyabooks import list --status failed alyabooks import invoices --payment-status unpaid alyabooks import get 9a1b… alyabooks import status 9a1b… alyabooks import validation 9a1b… alyabooks import content 9a1b… --part lineitems|seller|buyer|delivery|taxes|paymentterms|paymentmeans|summary alyabooks import source 9a1b… -o original.xml alyabooks import update 9a1b… -f fix.json # correct a draft: seller, buyer, delivery, lineItems, vatBreakdown, paymentMeans… alyabooks import accept 9a1b… alyabooks import reject 9a1b… --reason "Duplicate of INV-2201" alyabooks import pay 9a1b… --amount 1050 alyabooks import delete 9a1b… ``` ### customer / product ```bash alyabooks customer list --keyword buyer --type buyer alyabooks customer get alyabooks customer create -f customer.json alyabooks customer update -f patch.json alyabooks customer delete alyabooks customer next-number alyabooks customer import -f customers.json # array or { customers: [...] } alyabooks product list --sku CONS-H alyabooks product create -f product.json alyabooks product valid-code --sku CONS-H alyabooks product import -f products.json ``` ### webhook ```bash alyabooks webhook list alyabooks webhook events alyabooks webhook create --url https://example.com/hooks --events invoice.reported,invoice.paid --name erp alyabooks webhook update --events "*" --no-active alyabooks webhook test alyabooks webhook deliveries --status failed alyabooks webhook retry alyabooks webhook rotate-secret alyabooks webhook delete ``` ### fields, me, config ```bash alyabooks fields list --level line alyabooks fields templates --document-kind invoice alyabooks me alyabooks config path ``` ## In CI ```yaml # GitHub Actions: report the month's invoices produced by your billing job - run: npm install -g @alyabooks/cli - run: alyabooks invoice create -f out/invoices.json --wait --json > result.json env: ALYABOOKS_API_KEY: ${{ secrets.API_KEY }} ``` The command exits non-zero when any element failed, and `result.json` contains the per-element results for reconciliation. --- # CargoWise integration kit > Everything a CargoWise integrator needs to connect eAdaptor or eAdaptor Next to Alya Books: the endpoint, authentication, environments, the documents to read, what Alya Books needs from you, and what comes later. Source: /docs/guides/cargowise-integration This kit is for the team configuring a freight forwarder's CargoWise to report its UAE e-invoices through Alya Books. CargoWise keeps posting AR invoices as it does today. eAdaptor or eAdaptor Next pushes each one to Alya Books, which validates it against PINT AE, submits it to the accredited service provider for reporting to the FTA, renders the PDF and tells your side the outcome. ## How it fits together 1. An AR invoice, credit note or debit note is posted in CargoWise. 2. eAdaptor (SOAP) or eAdaptor Next (REST) pushes the Universal XML to Alya Books's endpoint. 3. Alya Books stores the message, maps it through the organisation's charge map, validates it and answers at once: processed, duplicate or failed with the reasons. 4. A processed invoice is submitted to the service provider; the FTA status arrives within minutes. 5. Alya Books reports each status change by webhook (or your side polls), with the XML and PDF available by download link. 6. Later phase: Alya Books writes the status and documents back into the CargoWise job (see [What comes later](#what-comes-later)). ## The endpoint ``` POST {BASE}/erp/cargowise/messages ``` `{BASE}` is the API address we send you with your test key. The same endpoint serves every environment; the key decides which one. - **Body:** the Universal XML in whatever shape your transport delivers: bare `UniversalInterchange` or `UniversalTransaction`, a SOAP envelope, a JSON wrapper, base64, gzip or a form post. Any `Content-Type` is accepted. See [What the endpoint accepts](/docs/guides/erp-intake#what-the-cargowise-endpoint-accepts). - **Authentication:** HTTP Basic with any user name and the API key as the password, or the header `X-API-KEY`. OAuth client credentials can be added if your outbound configuration requires it; tell us. - **Message id:** send yours in `X-Message-Id` (or `MessageId` in a JSON wrapper, or the SOAP header). Alya Books stores it and returns it, so both sides can trace a document. - **Replies:** see [Replies to the ERP](/docs/reference/erp-replies). With `?ack=always` every message is acknowledged with 2xx and failures stay in the log, for transports that retry on any error. ## Environments | Environment | Key | What happens on submit | |---|---|---| | Simulated | `sk_test_…` | Alya Books plays the service provider and the FTA. Statuses and webhooks are real; nothing leaves Alya Books. | | Provider test (UAT) | `sk_test_…` | The invoice goes to the service provider's test system. Nothing reaches the live FTA. | | Live | `sk_live_…` | Real reporting to the FTA. Issued by the organisation owner when go-live is signed off. | A test key only ever creates test documents, which are excluded from VAT reports and cannot be submitted live. ## Read these | Document | What it gives you | |---|---| | [ERP intake guide](/docs/guides/erp-intake) | The endpoint, the connector settings, the message log, replays and the dry run | | [CargoWise field mapping](/docs/reference/cargowise-field-mapping) | Every element Alya Books reads, the PINT AE term it fills and the rule that checks it ([CSV](/docs/downloads/cargowise-field-mapping.csv)) | | [Charge-code template](/docs/downloads/cargowise-charge-map-template.csv) | The sheet the forwarder's finance team fills: charge code → VAT treatment. It becomes the connector's charge map, signed off by the tax adviser | | [Replies to the ERP](/docs/reference/erp-replies) | Status codes, reply bodies, duplicates, retries and the error format | | [Status updates back to the ERP](/docs/guides/erp-status-updates) | Webhooks or polling for submitted, reported and rejected, with the XML and PDF | | [Testing a CargoWise integration](/docs/guides/cargowise-testing) | Set-up, the smoke test, the test console and the sign-off checklist | | [API reference](/docs/api-reference/introduction) | Every endpoint; the OpenAPI file is at `{BASE}/openapi.json` and a Postman collection at [/docs/api.postman_collection.json](/docs/api.postman_collection.json) | | [Errors](/docs/reference/errors) and [PINT AE fields](/docs/reference/pint-ae-fields) | What each rule means and how to fix it | Every page is also plain Markdown at its address plus `.md`, and the whole site is in [/docs/llms-full.txt](/docs/llms-full.txt) for your developers' AI tools. ## The charge map CargoWise charge codes decide each line's VAT treatment, so they are mapped once per organisation, before the first live message: 1. The forwarder's finance team fills the [charge-code template](/docs/downloads/cargowise-charge-map-template.csv): VAT category (`standard`, `zero_rated`, `exempt`, `reverse_charge`, `out_of_scope`, `margin`), item type (`S` service, `G` goods, `B` both), the service accounting code or HS code, an exemption reason code where needed, and a UAE transaction type where one applies (`exports`, `free_trade_zone`, `disclosed_agent` …). 2. The audit firm reviews it against the forwarder's transaction types. 3. It is loaded in Alya Books (Connectors → CargoWise → Configure, or `PUT {BASE}/erp/connectors/cargowise`) and signed off. Live messages can be refused until the sign-off exists. An unmapped charge falls back to the line's VAT code; when nothing decides, the message fails and names the code, so no invoice is reported with a guessed treatment. ## What we need from you 1. How eAdaptor Next delivers the outbound message: bare XML, JSON wrapper or SOAP, and the `Content-Type`. Alya Books accepts all of them; we want to test the one you will use. 2. Which outbound authentication you will configure: Basic with our key, OAuth client credentials, or none plus an IP allowlist. 3. The header or field carrying your message id, the reply eAdaptor Next expects on success, and its retry behaviour. 4. Anonymised sample messages: a standard AR invoice, a credit note against it, a foreign-currency invoice, and one with shipment details and Incoterms. 5. For the write-back phase: the EDI client registration Alya Books needs in CargoWise (token address, certificate, message type for status and eDocs), and access to a test CargoWise instance. ## What comes later - **Write-back into CargoWise.** Alya Books posts the FTA status, the PINT AE XML and the PDF back to the job through eAdaptor's inbound interface. It needs the registration in point 5 above. - **The forwarder's approved PDF layout.** Today invoices use Alya Books's standard or logistics layout (job number, house bill and Incoterms in a Shipment panel); the forwarder's approved layout can be rebuilt as a Alya Books design. - **OAuth client credentials** for the endpoint, if your configuration needs them. ## Support Send questions, sample messages and test results to your Alya Books contact. Include the message id or the Alya Books message id from the reply; every message is kept in the log with its raw body, so we can see exactly what arrived. --- # Testing a CargoWise integration > Set up a test organisation, run the smoke test, use the test console, and prove each part of a CargoWise integration before go-live. Source: /docs/guides/cargowise-testing For the Alya Books team and the integrator. It says which environment to use, how to set it up, and how to prove that the endpoint, the mapping, the submission, the webhooks and the documents work before anything goes live. ## Environments The same API and organisation serve all three; the API key and the organisation's e-invoicing connection decide what happens. | Level | Key | Connection (Admin → E-invoicing → Connection) | On submit | Use it for | |---|---|---|---|---| | 1. Simulated | `sk_test_…` | Test (simulated), scenario `accept`, `slow`, `reject_taxilla`, `reject_buyer` or `reject_fta` | Alya Books plays the service provider, the buyer's access point and the FTA. Statuses, acknowledgements and webhooks are real; nothing leaves Alya Books. | Building the integration, mapping checks, webhooks, rejection handling | | 2. Provider test (UAT) | `sk_test_…` | UAT, with the service provider's test credentials | The XML goes to the provider's test system and its test Peppol network. Nothing reaches the live FTA. | Final acceptance before go-live | | 3. Live | `sk_live_…` | Live | Real reporting to the FTA | Production only | - A test key only creates test documents (`isTest: true`), excluded from reports and the VAT return. - A test document cannot be submitted while the connection is live (400), so a test key is safe to hand to an integrator. - Live keys are issued by the organisation owner when go-live is signed off. ## Set-up 1. **Organisation.** A test organisation with a complete company profile: name, TRN, trade licence, address with emirate, Peppol ID, bank IBAN and account name. The seller side of every invoice comes from here. 2. **Connection.** Admin → E-invoicing → Connection: Test (simulated), scenario `accept`. 3. **API key.** API access → API keys → New key, type Test, scopes `invoices:read`, `invoices:write`, `settings:read`, `settings:write`, `webhooks:manage`. The key is shown once. 4. **CargoWise connector.** Connectors → CargoWise → Configure: - the charge map (load the sample codes from the test console (`/console` on the Alya Books site) with **Add the sample charge codes**, or from the forwarder's filled [template](/docs/downloads/cargowise-charge-map-template.csv)); - **Submit on receipt** on, so a processed message is submitted at once; - **PDF design**: a design with the **Logistics** layout, so the job number, house bill and Incoterms print in a Shipment panel. 5. **Custom fields.** Settings → Custom fields: `job`, `house_bill`, `branch` as text fields in the invoice template, so the references from the message are validated and shown. 6. **Webhook receiver.** For a first run, a public https inbox such as webhook.site. Private addresses and plain http are refused. ## The smoke test `scripts/sandbox/cargowise-smoke.sh` (from the Alya Books repository, or ask us for it) runs the flow against a test key and reports PASS, FAIL or SKIP per check. It refuses live keys. ```bash API_BASE={BASE} \ API_KEY=sk_test_… \ WEBHOOK_URL=https://webhook.site/ \ scripts/sandbox/cargowise-smoke.sh ``` | # | Check | Expected | |---|---|---| | 1 | `GET /openapi.json` | 200 | | 2 | `GET /webhooks/events`, `POST /webhooks`, `POST /webhooks/{id}/test` | the events; a subscription with its `whsec_` secret; a queued test delivery | | 3 | `POST /invoices` with a freight invoice (zero-rated freight, standard-rated handling, out-of-scope disbursement, `submit: true`) | 202, `validity: true`, a number | | 4 | `GET /invoices/{id}/status` for up to two minutes | `accepted`, with the submission in `authority` | | 5 | `?format=xml`, `?format=pdf` and `/download-link` | PINT AE XML, a PDF, and a 15-minute link to the PDF | | 6 | The same invoice with a 14-digit buyer TRN | 400 or 422, refused before submission | | 7 | `POST /incoming/simulate`, `POST /import/incoming` | the receiving pipeline (SKIP on UAT) | | 8 | `POST /invoices/validate?connector=cargowise` with the sample message | mapped and valid (fails if the charge map is not set) | | 9 | `POST /erp/cargowise/messages` with Basic authentication | `processed`, or `duplicate` on a rerun | | 10 | The sample wrapped in JSON with `X-Message-Id`, and as base64 text | `processed`, with `messageId` and `envelope` echoed | | 11 | `GET /erp/messages` | the messages just sent | | 12 | `GET /webhooks/{id}/deliveries` | `invoice.created`, `invoice.submitted`, `invoice.reported`, … | Outputs (replies, status, XML, PDF) are written to `scripts/sandbox/out/`. On level 1 the run takes under a minute. On level 2 step 4 waits for the provider's test system; rerun the status call if the script times out. Then switch the connection's scenario to `reject_taxilla`, `reject_buyer` and `reject_fta` in turn and confirm the invoice ends `rejected`, the status carries the reason, and an `invoice.rejected` webhook arrives. That is what the write-back must handle. ## The test console The test console (`/console` on the Alya Books site) covers the same ground by clicking: - **In the browser**, without a key: each sample through the same reader, mapper and rules as the API. - **Your sandbox**, with the test key: check the connection, add the sample charge codes (only missing codes are added), and send single requests (dry run, ERP intake, `POST /invoices`) with every failed rule explained. - **Dummy data feed**: up to 200 freight documents in CargoWise XML or JSON, a chosen share with planted mistakes (mistyped TRN, unmapped charge code, 5 % on zero-rated freight, credit note without a reason), each checked against what it must produce; with **send each twice**, every resend must answer `duplicate`. - **Status and webhooks**: follow an invoice to accepted or rejected and read the webhook deliveries. The key stays in the page's memory and is never shown in the log; live keys are refused. ## What the integration team tests 1. **Your transport.** Configure eAdaptor or eAdaptor Next exactly as for production and post a real AR invoice in CargoWise UAT. It must arrive as one Alya Books invoice with the right lines, VAT categories, references and currency. The reply and the message log show how it arrived (`envelope`) and your message id. 2. **Failures.** Post an invoice with a charge code missing from the map. The reply is 400 (or 202 `failed` with always-acknowledge) naming the code. Map it, then resend or replay: it is processed. 3. **Duplicates.** Send the same invoice twice: one invoice, the second reply `duplicate`. 4. **Credit notes.** Post a credit note with its reason against the test invoice: the preceding reference is carried and it is accepted. 5. **Foreign currency.** Bill in USD with the AED rate and check the AED tax amounts in the XML. 6. **Status back.** Handle `invoice.reported` and `invoice.rejected` (or poll the status): attach the XML and PDF to the job and set its e-invoice status; on a rejection, show the rule and message to the person who posted it. 7. **Webhook signature.** Verify `X-Webhook-Signature` on a delivery and reject timestamps older than five minutes. ## Sign-off checklist - [ ] Smoke test passes on level 1 with scenario `accept`, and ends `rejected` for each rejection scenario. - [ ] Smoke test passes on level 2 (provider UAT): status `accepted` with the acknowledgements on the invoice timeline. - [ ] Every transaction type in the forwarder's signed readiness matrix has been posted from CargoWise UAT and accepted. - [ ] The charge map is complete and signed off by the audit firm. - [ ] Failures, duplicates, credit notes and foreign currency behave as above. - [ ] Status back (webhooks or polling) handled for accepted and rejected invoices; signatures verified; the receiver answers 2xx within 10 seconds. - [ ] The PDF design approved by the forwarder. --- # CargoWise field mapping > Every UniversalTransaction element Alya Books reads, the API field and PINT AE term it fills, whether it is required, and the rule that checks it. Source: /docs/reference/cargowise-field-mapping Alya Books reads the Universal XML that eAdaptor sends for an AR transaction. WiseTech publishes the schema to licensees only, so the reader is deliberately tolerant: namespaces and envelopes are ignored, several spellings are read for each element (in brackets), and anything not listed here is ignored. Download the table as [CSV](/docs/downloads/cargowise-field-mapping.csv). The seller (name, TRN, address, Peppol ID, bank details) always comes from the organisation's profile in Alya Books, never from the message. Lines get their VAT treatment from the connector's charge map; see the [charge-code template](/docs/downloads/cargowise-charge-map-template.csv). Check a message without creating anything: `POST {BASE}/invoices/validate?connector=cargowise` with the message as the body returns the mapped invoice, the notes and every failed rule. ## Document | CargoWise element | Required | Alya Books field | PINT AE | Rule or behaviour | |---|---|---|---|---| | TransactionInfo/Number (TransactionNumber; InvoiceNumber) | Yes | `invoiceNumber` | BT-1 Invoice number | Unique per seller. With the branch and type it forms the message key cargowise:::, so a resend is answered as a duplicate. | | TransactionInfo/TransactionType (TransactionType/Code; Type) | No (default INV) | `invoiceTypeCode` | BT-3 Invoice type code | INV, ARI, AR → 380 tax invoice; CRD, CRN, ARC → 381 credit note; DBN, DEB → 383 debit note; or a 3-digit code. Anything else is refused. | | TransactionInfo/TransactionDate (PostDate; InvoiceDate) | Yes | `issueDate` | BT-2 Issue date | YYYY-MM-DD or DD/MM/YYYY. | | TransactionInfo/DueDate (PaymentDueDate) | Yes for invoices with a payment due | `dueDate` | BT-9 Payment due date | Missing due date fails IBR-127-AE unless payment terms are given. | | TransactionInfo/Currency/Code (Currency; LocalCurrency/Code) | No (default AED) | `currency` | BT-5 Invoice currency code | ISO 4217. | | TransactionInfo/ExchangeRate (LocalExchangeRate) | For non-AED invoices | `exchangeRate` | BT-111 / AED tax totals | When absent, Alya Books uses the UAE Central Bank rate for the date of supply (the issue date when no supply date is given). | | TransactionInfo/Branch/Code (Branch; BranchCode) | No | `metadata.branch` | — | Part of the message key; printed in the logistics Shipment panel. | | TransactionInfo/JobNumber (Job/Key; Job/JobNumber; JobKey) | No | `metadata.job` | — | Kept as a custom field and in the invoice note; printed in the logistics Shipment panel. | | TransactionInfo/HouseBill (HouseBillNumber; Shipment/HouseBill) | No | `metadata.house_bill` | — | Kept as a custom field and in the invoice note; printed in the logistics Shipment panel. | | TransactionInfo/ShipmentIncoTerm/Code (IncoTerm/Code; Incoterms; or Shipment/ShipmentIncoTerm/Code) | No | `incoterms` | Delivery terms (cac:DeliveryTerms) | EXW, FCA, CPT, CIP, DAP, DPU, DDP, FAS, FOB, CFR, CIF. An unknown code is left out with a note. | | TransactionInfo/OriginalTransactionNumber (ReferenceTransactionNumber; AppliedInvoiceNumber) | Yes for credit and debit notes | `precedingInvoice.number` | BT-25 Preceding invoice reference | | | TransactionInfo/OriginalTransactionDate (ReferenceTransactionDate) | Yes for credit and debit notes | `precedingInvoice.issueDate` | BT-26 Preceding invoice issue date | | | TransactionInfo/CreditNoteReason (CreditNoteReason/Code; ReasonCode; CreditReason) | Yes for credit notes | `creditNoteReasonCode` | BTAE-03 Credit note reason | Alya Books never guesses a tax reason; a credit note without one fails validation. | | TransactionInfo/PK (Key; TransactionKey) | No | — | — | Kept as a note on the message for tracing. | ## Buyer | CargoWise element | Required | Alya Books field | PINT AE | Rule or behaviour | |---|---|---|---|---| | Organization/FullName (Name; CompanyName; OrganizationName) | Yes | `buyer.name` | BT-44 Buyer name | The Organization block is the customer of the AR transaction. | | Organization/RegistrationNumber with Type TRN\|VAT\|TAXREG (or TRN; TaxRegistrationNumber; VATNumber) | Yes for VAT-registered UAE buyers | `buyer.trn` | BT-48 Buyer VAT identifier | 15 digits. A UAE buyer needs a TRN or a legal registration (IBR-135-AE). | | Organization/Code (OrganizationCode) | No | `buyer.externalId` | — | Matches the Alya Books customer created for this CargoWise organisation. | | Organization/Email (Contact/Email) | No | `buyer.email` | — | Used to send the invoice when sending is on. | | Address/Address1 (AddressLine1; Street) | Yes for UAE buyers | `buyer.address.street` | BT-50 Buyer address line 1 | IBR-144-AE needs street; city and emirate. | | Address/Address2 (AddressLine2) | No | `buyer.address.additional` | BT-51 Buyer address line 2 | | | Address/City (CityOrSuburb) | Yes for UAE buyers | `buyer.address.city` | BT-52 Buyer city | | | Address/State/Code (State; Emirate) | Yes for UAE buyers | `buyer.address.emirate` | BT-54 Buyer country subdivision | Emirate code or name, e.g. DXB or Dubai. | | Address/Postcode (PostCode; PostalCode) | No | `buyer.address.postalCode` | BT-53 Buyer post code | | | Address/Country/Code (CountryCode; Country) | Yes | `buyer.address.country` | BT-55 Buyer country code | AE is assumed when a TRN is present. | ## Lines | CargoWise element | Required | Alya Books field | PINT AE | Rule or behaviour | |---|---|---|---|---| | PostingJournalCollection/PostingJournal (TransactionLineCollection/TransactionLine; InvoiceLineCollection/InvoiceLine; ChargeLineCollection/ChargeLine) | At least one | `items[]` | BG-25 Invoice line | | | ChargeCode (ChargeCode/Code; ChargeLine/ChargeCode; Code) | Yes | `charge map key` | BT-151 VAT category; item type (G, S, B); service or HS code | Looked up in the connector's charge map (VAT category, item type, service accounting code, HS code, exemption reason, transaction type). | | VATCode (VATCode/Code; TaxCode; Tax/Code; VAT/Code) | Fallback | `items[].taxCategory` | BT-151 Invoiced item VAT category | Used only when the charge code is not mapped: the connector's VAT-code map, then S/STD/5/VAT5 standard, Z/ZR/ZERO zero rated, E/EX/EXEMPT exempt, O/OS/OOS/NA/DISB out of scope, RC/RCM/AE reverse charge, then the connector's default category. Otherwise the message fails and names the code. | | Quantity (ChargeLine/Quantity) | No (default 1) | `items[].quantity` | BT-129 Invoiced quantity | | | Amount (NetAmount; OSAmount; LocalAmount; ChargeLine/SellOSAmount; ChargeLine/SellLocalAmount) | Yes | `line net amount` | BT-131 Invoice line net amount | Net of VAT, in the invoice currency. | | UnitPrice (SellRate; ChargeLine/SellRate) | No (amount ÷ quantity) | `items[].unitPrice` | BT-146 Item net price | Rounded to 2 decimals. | | Description (ChargeLine/Description; ChargeCode/Description) | No (charge code) | `items[].description` | BT-153 Item name | | | PK (Key; LineNumber) | No | `items[].externalId` | — | Kept for tracing. | ## Seller | CargoWise element | Required | Alya Books field | PINT AE | Rule or behaviour | |---|---|---|---|---| | — (not read from the message) | — | `Organisation profile in Alya Books` | BG-4 Seller | Seller name, TRN, address, Peppol ID and bank details come from Settings → Company, never from the message. | ## Message key and duplicates Each message gets the key `cargowise:::` (for example `cargowise:DXB:INV:INV-2027-00412`). It is the invoice's `externalId`, it appears in webhook payloads, and a second message with the same key answers `duplicate` without creating anything. To correct an invoice after it was reported, post a credit note; to fix a mapping before it was processed, replay the stored message from the log. --- # Replies to the ERP > What POST /erp/{connector}/messages answers: status codes, the reply body, duplicates, failures, always-acknowledge, retries and rate limits. Source: /docs/reference/erp-replies Every message gets an immediate reply. A processed message has created the invoice (and, when the connector's submit setting is on, handed it to the service provider); the FTA outcome follows by [webhook or polling](/docs/guides/erp-status-updates). ## Status codes | Code | `status` in the body | Meaning | What your side does | |---|---|---|---| | `202` | `processed` | Mapped, validated and created. `invoiceNumber` and `invoiceId` are set. | Record `invoiceId`; wait for the status update | | `200` | `duplicate` | A message with the same key was processed before. Nothing was created; the reply points at the existing invoice. | Treat as delivered | | `400` | `failed` (in `extra.message`) | The message could not be unwrapped, mapped or validated. The body explains each problem. The message is kept in the log. | Do not retry unchanged: fix the data in CargoWise (or the charge map in Alya Books) and resend, or replay from the log | | `400` | — | Empty body, or larger than 2 MB. Nothing stored. | Fix the transport | | `401` | — | Missing or invalid key. | Check the key and the Basic credentials | | `403` | — | The key lacks the `invoices:write` scope. | Use a key with the scope | | `409` | — | Live messages are refused until the charge map is signed off, or the invoice number already exists outside this connector. | Contact the organisation | | `429` | — | More than 600 requests a minute for the key. `Retry-After` says when to retry. | Retry after the delay | | `5xx` | — | Alya Books could not finish. A stored message stays in the log. | Retry with back-off; a resend is safe (it answers `duplicate` once processed) | With `?ack=always` on the endpoint URL (or **Always acknowledge** in the connector settings), failures answer `202` with `status: "failed"` instead of `400`, for transports that would otherwise resend a message that cannot succeed. The failure stays in the log and in the reply body. ## The reply body ```jsonc { "id": "5f0c…", // Alya Books's message id "connector": "cargowise", "status": "processed", // processed | duplicate | failed "documentNumber": "INV-2027-00412", "documentType": "INV", "idempotencyKey": "cargowise:DXB:INV:INV-2027-00412", "messageId": "EAN-0001", // yours, from X-Message-Id or the envelope "envelope": ["json:Payload"], // how the XML arrived; [] for bare XML "contentType": "application/json", "bytes": 5555, "invoiceId": "ac20fac3-…", // the invoice "invoiceNumber": "INV-2027-00412", "error": null, "notes": ["Received as json:Payload", "CargoWise key 7f3c…"], "result": { "invoiceId": "…", "invoiceNumber": "…", "validation": { "validity": true, "errors": [] }, "submission": { … } }, "receivedAt": "2026-10-13T07:10:02Z", "processedAt": "2026-10-13T07:10:03Z", "replayOf": null } ``` A failure (`400`) uses the [error envelope](/docs/reference/errors) with the stored message in `extra.message`: ```json { "code": "E_VALIDATION", "statusCode": 400, "message": "Charge NEW has no mapping and VAT code XX is unknown: add it to the connector's charge map", "field": "charges.NEW", "extra": { "kind": "mapping", "message": { "id": "…", "status": "failed", "error": "…", "notes": [] } } } ``` When validation fails, `extra.errors[]` lists each PINT AE rule with `rule`, `field`, `path`, the message and an explanation of how to fix it, in English and Arabic. ## Duplicates and corrections - The key is `cargowise:::`. Resending the same document, with or without the same message id, answers `duplicate`. - A failed message is not a duplicate: once the data is fixed, resend it (or replay it in Alya Books) and it is processed. - A reported invoice cannot change. Post a credit note in CargoWise; it arrives as a 381 against the original. ## The message log `GET {BASE}/erp/messages` lists every message (filter by `status`, `connector`, or search by number, key or error), `GET {BASE}/erp/messages/{id}` returns one with its raw body, and `POST {BASE}/erp/messages/{id}/replay` processes a stored message again after a mapping fix. The same log is in the app under Connectors → CargoWise → Messages. --- # Status updates back to the ERP > How the ERP learns what happened to an invoice after Alya Books accepted the message: webhooks or polling, the statuses, rejections, and the XML and PDF to attach to the job. Source: /docs/guides/erp-status-updates The reply to a pushed message says whether Alya Books created the invoice. What happens next (submission to the service provider, reporting to the FTA, a rejection) arrives later, usually within minutes. Alya Books checks the provider every five minutes and reports each change by webhook; your side can also poll. ## The statuses | `invoiceStatus` | Meaning | Webhook event | |---|---|---| | `validated` | Created and valid, not yet submitted (the connector's submit setting is off, or approval is pending) | `invoice.created` | | `submitted` | Handed to the accredited service provider | `invoice.submitted` | | `delivered` | The buyer's access point accepted it | `invoice.delivered` | | `accepted` | Reported to the FTA | `invoice.reported` | | `rejected` | Refused by the provider, the buyer's access point or the FTA | `invoice.rejected` | `invoice.reported` and `invoice.rejected` are the two your write-back needs; the others are optional progress. ## Webhooks (recommended) Create a subscription once: ```bash curl -X POST {BASE}/webhooks -H "X-API-KEY: sk_test_…" -H "Content-Type: application/json" \ -d '{"targetUrl":"https://your-middleware.example/einvoicing","events":["invoice.reported","invoice.rejected","invoice.submitted"]}' ``` The reply contains the signing secret (`whsec_…`) once. Each delivery is a signed `POST`: ```jsonc { "id": "9b1e…", // event id: the same for every subscriber, use it to de-duplicate "event": "invoice.rejected", "createdAt": "2026-10-13T07:15:00Z", "organizationId": "…", "data": { "id": "ac20fac3-…", // invoiceId "invoiceNumber": "INV-2027-00412", "externalId": "cargowise:DXB:INV:INV-2027-00412", // match it to the CargoWise transaction "invoiceTypeCode": "380", "status": "rejected", "issueDate": "2026-10-13", "currency": "AED", "total": 17140, "buyer": { "id": "…", "name": "…", "trn": "100123456700003" }, "metadata": { "job": "S00123456", "house_bill": "DXBSHA2701234", "branch": "DXB" }, "validation": { "validity": true, "errors": [] }, "rejection": { "stage": "fta", "error": "…", "submissionId": "…", "at": "2026-10-13T07:14:58Z" }, "isTest": true } } ``` - Match on `data.externalId` (the message key) or `data.invoiceNumber` (the CargoWise number). - On `invoice.rejected`, `data.rejection.stage` says who refused it (`taxilla` for the service provider, `buyer`, `fta`) and `error` why. Show it to the person who posted the invoice; the fix is a corrected document or a credit note. - Verify `X-Webhook-Signature` (HMAC-SHA256 of `"."`) and reject timestamps older than five minutes. See [Webhooks](/docs/guides/webhooks#verify-the-signature) for code. - Answer `2xx` within 10 seconds. Failed deliveries are retried with back-off (checked every five minutes) and stay visible under `GET {BASE}/webhooks/{id}/deliveries`. ## Polling Without a webhook receiver, poll the invoices still in progress: ``` GET {BASE}/invoices/{invoiceId}/status ``` ```jsonc { "invoiceId": "ac20fac3-…", "invoiceNumber": "INV-2027-00412", "invoiceStatus": "accepted", "jobStatus": "done", // processing | ready | done | failed "authority": [ { "type": "fta", "provider": "taxilla", "mode": "uat", "status": "accepted", "stage": null, "referenceNumber": "90d455ab-…", "requestId": "205a32b2-…", "submittedAt": "…", "completedAt": "…", "error": null } ], "validation": { "validity": true, "errors": [], "warnings": [] } } ``` Poll every few minutes, and stop once `jobStatus` is `done` or `failed`. To find invoices by message key, use `GET {BASE}/invoices?externalId=cargowise:DXB:INV:INV-2027-00412`. ## The documents to attach to the job | Document | Request | Notes | |---|---|---| | PINT AE XML as reported | `GET {BASE}/invoices/{id}?format=xml` | After submission this is the exact XML that was sent | | PDF | `GET {BASE}/invoices/{id}?format=pdf` | The same PDF the app downloads and emails, in the invoice's design | | A link instead of the file | `GET {BASE}/invoices/{id}/download-link?format=pdf` (or `xml`) | A signed URL that works for 15 minutes without a key, for middleware that fetches files separately | ## Writing back into CargoWise In the first phase your middleware (or a person) attaches the documents and sets the job's e-invoice status from these events. In the next phase Alya Books posts them into CargoWise itself through eAdaptor's inbound interface; see [What comes later](/docs/guides/cargowise-integration#what-comes-later). --- # PINT AE field reference > Every business term of the UAE PINT AE profile, its UBL path, whether it is required, and the API field that carries it. Source: /docs/reference/pint-ae-fields Generated from the catalogue that also drives the editor's field configuration and the MCP resource `einvoicing://reference/pint-ae-fields`. Seller terms (BG-4 to BG-6) come from the organization profile; computed terms are derived from lines and allowances. Specification: `urn:peppol:pint:billing-1@ae-1`, profile `urn:peppol:bis:billing`, UBL 2.1 Invoice and CreditNote. ## Document | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BT-1 | Invoice number | `cbc:ID` | yes | `invoiceNumber` | | | BT-2 | Issue date | `cbc:IssueDate` | yes | `issueDate` | | | BT-3 | Invoice type code | `cbc:InvoiceTypeCode` | yes | `invoiceTypeCode` | UAE: 380, 381, 480, 81; self-billing: 389, 261 | | BT-5 | Currency code | `cbc:DocumentCurrencyCode` | yes | `currency` | | | BT-6 | VAT accounting currency | `cbc:TaxCurrencyCode` | no | `vatAccountingCurrency` | Set to AED automatically when the document currency is not AED | | BT-7 | Tax point date | `cbc:TaxPointDate` | no | `taxPointDate` | | | BT-9 | Due date | `cbc:DueDate` | no | `dueDate` | | | BT-10 | Buyer reference | `cbc:BuyerReference` | no | `buyerReference` | PO number or buyer's internal reference | | BT-11 | Project reference | `cac:ProjectReference > cbc:ID` | no | `projectReference` | | | BT-12 | Contract reference | `cac:ContractDocumentReference > cbc:ID` | no | `contractReference` | | | BT-13 | Order reference | `cac:OrderReference > cbc:ID` | no | `orderReference` | | | BT-14 | Sales order reference | `cac:OrderReference > cbc:SalesOrderID` | no | `salesOrderReference` | | | BT-15 | Receiving advice reference | `cac:ReceiptDocumentReference > cbc:ID` | no | `receiptReference` | | | BT-16 | Despatch advice reference | `cac:DespatchDocumentReference > cbc:ID` | no | `despatchReference` | | | BT-17 | Tender or lot reference | `cac:OriginatorDocumentReference > cbc:ID` | no | `tenderReference` | | | BT-19 | Buyer accounting reference | `cbc:AccountingCost` | no | `buyerAccountingReference` | | | BTAE-05 | Customs reference number | `cac:StatementDocumentReference > cbc:ID` | no | `customsReference` | | | BT-20 | Payment terms text | `cac:PaymentTerms > cbc:Note` | no | `payment.termsText` | | | BT-22 | Notes | `cbc:Note` | no | `notes` | | | BT-23 | Business process type | `cbc:ProfileID` | yes | `(auto-set)` | urn:peppol:bis:billing | | BT-24 | Specification ID | `cbc:CustomizationID` | yes | `(auto-set)` | urn:peppol:pint:billing-1@ae-1 | ## Delivery (BG-13) | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BTAE-22 | Incoterms | `cac:DeliveryTerms > cbc:ID` | no | `incoterms` | Incoterms 2020 code | | BT-72 | Delivery date | `cac:Delivery > cbc:ActualDeliveryDate` | no | `deliveryDate` | | | BT-70 | Delivery name | `cac:Delivery > cac:DeliveryLocation > cbc:Description` | no | `delivery.name` | | | BT-75 | Delivery address street | `cac:Delivery > cac:DeliveryLocation > cac:Address > cbc:StreetName` | no | `delivery.street` | | | BT-77 | Delivery city | `cac:Delivery > cac:DeliveryLocation > cac:Address > cbc:CityName` | no | `delivery.city` | | | BT-80 | Delivery country code | `cac:Delivery > cac:DeliveryLocation > cac:Address > cac:Country > cbc:IdentificationCode` | no | `delivery.country` | | | BT-79 | Delivery emirate, state or region | `cac:Delivery > cac:DeliveryLocation > cac:Address > cbc:CountrySubentity` | no | `deliveryAddressEmirate` | Required on e-commerce supplies and exports (IBR-142-AE, IBR-152-AE) | ## Preceding Invoice (BG-3) | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BT-25 | Preceding invoice reference | `cac:BillingReference > cac:InvoiceDocumentReference > cbc:ID` | no | `precedingInvoice.number` | Required for credit/debit notes (381, 81, 383) | | BT-26 | Preceding invoice issue date | `cac:BillingReference > cac:InvoiceDocumentReference > cbc:IssueDate` | no | `precedingInvoice.issueDate` | | ## Seller (BG-4) | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BT-27 | Seller name | `cac:AccountingSupplierParty > cac:Party > cac:PartyName > cbc:Name` | yes | `organization profile` | | | BT-31 | Seller tax ID (TRN) | `cac:AccountingSupplierParty > cac:PartyTaxScheme > cbc:CompanyID` | yes | `organization profile` | Exactly 15 digits for UAE | | BT-34 | Seller endpoint ID | `cac:AccountingSupplierParty > cac:EndpointID` | no | `organization profile` | Peppol participant ID | | BTAE-14 | Principal ID | `cac:SellerSupplierParty > cac:Party > cac:PartyIdentification > cbc:ID` | no | `principalId` | Disclosed agent billing (IBR-137-AE) | ## Seller Address (BG-5) | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BT-35 | Seller address street | `cbc:StreetName` | yes | `organization profile` | | | BT-37 | Seller city | `cbc:CityName` | yes | `organization profile` | | | BT-38 | Seller postal code | `cbc:PostalZone` | no | `organization profile` | | | BT-39 | Seller emirate/state | `cbc:CountrySubentity` | no | `organization profile` | | | BT-40 | Seller country code | `cac:Country > cbc:IdentificationCode` | yes | `organization profile` | Default: AE | ## Seller Contact (BG-6) | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BT-41 | Seller contact name | `cac:Contact > cbc:Name` | no | `organization profile` | | | BT-42 | Seller contact telephone | `cac:Contact > cbc:Telephone` | no | `organization profile` | | | BT-43 | Seller contact email | `cac:Contact > cbc:ElectronicMail` | no | `organization profile` | | ## Buyer (BG-7) | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BT-44 | Buyer name | `cac:AccountingCustomerParty > cac:PartyName > cbc:Name` | yes | `buyer.name` | | | BT-48 | Buyer tax ID (TRN) | `cac:AccountingCustomerParty > cac:PartyTaxScheme > cbc:CompanyID` | no | `buyer.trn` | Required for VAT-registered buyers | | BT-49 | Buyer endpoint ID | `cac:AccountingCustomerParty > cac:EndpointID` | no | `buyer.peppolId` | | | BTAE-01 | Beneficiary ID | `cac:BuyerCustomerParty > cac:Party > cac:PartyIdentification > cbc:ID` | no | `beneficiaryId` | Free zone supplies (IBR-007-AE); the buyer when left empty | ## Payee (BG-10) | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BT-59 | Payee name | `cac:PayeeParty > cac:PartyName > cbc:Name` | no | `payee.name` | Required when payee differs from seller | | BT-60 | Payee identifier | `cac:PayeeParty > cac:PartyIdentification > cbc:ID` | no | `payee.id` | | ## Tax Representative (BG-11) | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BT-62 | Tax representative name | `cac:TaxRepresentativeParty > cac:PartyName > cbc:Name` | no | `taxRepresentative.name` | | | BT-63 | Tax representative TRN | `cac:TaxRepresentativeParty > cac:PartyTaxScheme > cbc:CompanyID` | no | `taxRepresentative.trn` | Required when tax rep name is provided | ## Invoicing Period (BG-14) | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BTAE-06 | Frequency of billing | `cac:InvoicePeriod > cbc:DescriptionCode` | no | `billingFrequency` | DLY, WKY, Q15, MTH, Q45, Q60, QTR, YRL, HYR, OTH | | BT-73 | Invoicing period start | `cac:InvoicePeriod > cbc:StartDate` | no | `billingPeriod.startDate` | | | BT-74 | Invoicing period end | `cac:InvoicePeriod > cbc:EndDate` | no | `billingPeriod.endDate` | | ## Payment (BG-16) | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BT-81 | Payment means code | `cac:PaymentMeans > cbc:PaymentMeansCode` | yes | `paymentMeans[].code` | Several payment methods per invoice. UAE: 1, 10, 20, 21, 30, 49, 54, 55, 68. Required except on credit notes and deemed supplies (IBR-191-AE) | | BT-82 | Payment means text | `cac:PaymentMeans > cbc:PaymentMeansCode/@name` | no | `paymentMeans[].information` | | | BT-83 | Remittance information (payment reference) | `cac:PaymentMeans > cbc:PaymentID` | no | `paymentMeans[].reference` | | | BT-84 | Payment account (IBAN) | `cac:PayeeFinancialAccount > cbc:ID` | no | `paymentMeans[].accountId` | Required for credit transfer (IBR-192-AE); defaults to the account in Settings → Bank details | | BT-85 | Payment account name | `cac:PayeeFinancialAccount > cbc:Name` | no | `paymentMeans[].accountName` | | | BT-86 | Payment BIC/SWIFT | `cac:PayeeFinancialAccount > cac:FinancialInstitutionBranch > cbc:ID` | no | `paymentMeans[].bic` | | | BT-87 | Payment card number (last digits) | `cac:CardAccount > cbc:PrimaryAccountNumberID` | no | `paymentMeans[].cardPan` | Last 4 to 6 digits only; at most one card per invoice (IBR-066) | | BT-88 | Payment card holder | `cac:CardAccount > cbc:HolderName` | no | `paymentMeans[].cardHolder` | | | BT-89 | Mandate reference | `cac:PaymentMandate > cbc:ID` | no | `paymentMeans[].mandateReference` | At most one direct debit per invoice (IBR-067) | | BT-90 | Bank-assigned creditor identifier | `cac:AccountingSupplierParty > cac:Party > cac:PartyIdentification > cbc:ID[@schemeID='SEPA']` | no | `paymentMeans[].creditorId` | | | BT-91 | Debited account | `cac:PaymentMandate > cac:PayerFinancialAccount > cbc:ID` | no | `paymentMeans[].debitedAccount` | | | BT-178 | Payment instructions ID | `cac:PaymentMeans > cbc:ID` | no | `paymentMeans[].instructionId` | | ## Allowances/Charges (BG-20/21) | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BT-92 | Allowance amount | `cac:AllowanceCharge > cbc:Amount` | yes | `allowances[].amount` | Must be > 0 | | BT-93 | Allowance base amount | `cac:AllowanceCharge > cbc:BaseAmount` | no | `allowances[].baseAmount` | | | BT-94 | Allowance percentage | `cac:AllowanceCharge > cbc:MultiplierFactorNumeric` | no | `allowances[].percentage` | | | BT-97 | Allowance reason | `cac:AllowanceCharge > cbc:AllowanceChargeReason` | no | `allowances[].reason` | Reason or reason code required | | BT-99 | Charge amount | `cac:AllowanceCharge > cbc:Amount` | yes | `allowances[].amount` | charge_indicator=true | | BT-104 | Charge reason | `cac:AllowanceCharge > cbc:AllowanceChargeReason` | no | `allowances[].reason` | charge_indicator=true | ## Totals (BG-22) | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BT-106 | Sum of line net amounts | `cac:LegalMonetaryTotal > cbc:LineExtensionAmount` | yes | `(computed)` | | | BT-109 | Tax exclusive amount | `cbc:TaxExclusiveAmount` | yes | `(computed)` | | | BT-110 | Tax total amount | `cac:TaxTotal > cbc:TaxAmount` | yes | `(computed)` | | | BT-112 | Tax inclusive amount | `cbc:TaxInclusiveAmount` | yes | `(computed)` | | | BT-115 | Payable amount | `cbc:PayableAmount` | yes | `(computed)` | | ## Line (BG-25) | BT | Name | UBL path | Required | API field | Notes | | --- | --- | --- | --- | --- | --- | | BT-126 | Line ID | `cac:InvoiceLine > cbc:ID` | yes | `items[].lineNumber` | | | BT-129 | Invoiced quantity | `cbc:InvoicedQuantity` | yes | `items[].quantity` | Must be > 0 | | BT-130 | Quantity unit code | `cbc:InvoicedQuantity@unitCode` | yes | `items[].unitCode` | UN/ECE Rec 20 codes (EA, KGM, etc.) | | BT-131 | Line net amount | `cbc:LineExtensionAmount` | yes | `items[].(computed)` | Must match qty × price | | BT-146 | Item net price | `cac:Price > cbc:PriceAmount` | yes | `items[].unitPrice` | | | BT-151 | Tax category code | `cac:ClassifiedTaxCategory > cbc:ID` | yes | `items[].taxCategory` | S=Standard, Z=Zero-rated, E=Exempt, AE=Reverse charge | | BT-121 | VAT exemption reason code | `cac:ClassifiedTaxCategory > cbc:TaxExemptionReasonCode` | no | `items[].taxExemptionReasonCode` | UAE list DL8.46.1 to DL8.46.4; required on exempt lines (IBR-167-AE) | | BT-152 | Tax rate (%) | `cac:ClassifiedTaxCategory > cbc:Percent` | yes | `items[].taxRate` | UAE standard = 5% | | BT-153 | Item description | `cac:Item > cbc:Description` | yes | `items[].description` | | | BT-127 | Line note | `cac:InvoiceLine > cbc:Note` | no | `items[].note` | | | BT-136 | Line allowance (discount) | `cac:InvoiceLine > cac:AllowanceCharge[ChargeIndicator=false] > cbc:Amount` | no | `items[].allowanceAmount` | With reason IBT-139 / IBT-140 | | BT-141 | Line charge | `cac:InvoiceLine > cac:AllowanceCharge[ChargeIndicator=true] > cbc:Amount` | no | `items[].chargeAmount` | With reason IBT-144 / IBT-145 | | BT-156 | Buyer's item identifier | `cac:BuyersItemIdentification > cbc:ID` | no | `items[].buyerItemId` | | | BT-159 | Item country of origin | `cac:OriginCountry > cbc:IdentificationCode` | no | `items[].originCountry` | | | BTAE-23 | Batch number | `cac:ItemInstance > cac:LotIdentification > cbc:LotNumberID` | no | `items[].batchNumber` | | | BTAE-13 | Item type | `cac:CommodityClassification > cbc:CommodityCode` | no | `items[].itemType` | G goods, S services, B both | | BTAE-17 | Service accounting code | `cac:AdditionalItemIdentification > cbc:ID[@schemeID='SAC']` | no | `items[].serviceAccountingCode` | Required for services (IBR-185-AE) | | BT-158 | Item classification (HS code) | `cac:CommodityClassification > cbc:ItemClassificationCode` | no | `items[].hsnCode` | | ## Optional field groups in the editor These switches in the editor's **Fields** dialog reveal groups of business terms; the API accepts them at any time. | Group | Business terms | API keys | | --- | --- | --- | | Buyer reference / PO number | BT-10 | `buyerReference` | | Order reference | BT-13 | `orderReference` | | Sales order reference | BT-14 | `salesOrderReference` | | Contract reference and value | BT-12 | `contractReference`, `contractValue` | | Despatch and receiving advice | BT-16, BT-15 | `despatchReference`, `receiptReference` | | Tender or lot reference | BT-17 | `tenderReference` | | Customs reference and Incoterms | BTAE-22 | `customsReference`, `incoterms` | | Buyer accounting reference | BT-19 | `buyerAccountingReference` | | Project reference | BT-11 | `projectReference` | | Preceding invoice reference and date | BT-25, BT-26 | `precedingInvoice.number`, `precedingInvoice.issueDate` | | Invoicing period | BT-73, BT-74 | `billingPeriod.startDate`, `billingPeriod.endDate` | | Frequency of billing | BTAE-06 | `billingFrequency` | | Date of supply (tax point) | BT-7 | `taxPointDate` | | Delivery date and location | BT-72, BT-70 | `deliveryDate`, `delivery.name` | | Delivery address | BT-75, BT-77, BT-80 | `delivery.street`, `delivery.city`, `deliveryAddressEmirate`, `delivery.country` | | Payee (when different from seller) | BT-59, BT-60 | `payee.name`, `payee.id` | | Tax representative | BT-62, BT-63 | `taxRepresentative.name`, `taxRepresentative.trn`, `sellerTaxRepStreet`, `sellerTaxRepCity`, `sellerTaxRepEmirate`, `sellerTaxRepCountryCode` | | Beneficiary (free zone supplies) | BTAE-01 | `beneficiaryId` | | Principal (disclosed agent billing) | BTAE-14 | `principalId` | | Early payment discount tiers | BT-20 | `earlyPaymentDiscounts` | | Salutation, introduction and closing text | PDF only | `salutation`, `introduction`, `closing`, `signatureText` | ## MCP tools per group | Group | Tools | | --- | --- | | Invoice creation | `create_tax_invoice`, `create_credit_note`, `create_debit_note`, `create_invoices_batch`, `replace_draft_invoice` | | Invoice management | `list_invoices`, `get_invoice`, `get_invoice_status`, `get_invoice_validation`, `validate_invoice`, `submit_invoice`, `send_invoice`, `download_invoice`, `delete_draft_invoice`, `update_invoice_housekeeping`, `list_design_templates`, `list_invoice_payments`, `record_invoice_payment` | | Invoice numbers | `get_next_invoice_number`, `verify_invoice_number`, `get_invoice_number_settings`, `update_invoice_number_settings` | | Incoming invoices | `simulate_incoming_invoice`, `import_incoming_invoice`, `list_incoming_imports`, `list_incoming_invoices`, `get_import`, `get_import_status`, `get_import_validation`, `get_import_content`, `get_import_summary`, `get_import_line_items`, `get_import_seller`, `get_import_buyer`, `get_import_taxes`, `get_import_payment_terms`, `get_import_payment_means`, `get_import_delivery`, `get_import_attachments`, `get_import_source`, `review_incoming_invoice`, `update_incoming_invoice`, `record_incoming_payment`, `delete_import` | | Customers | `list_customers`, `get_customer`, `create_customer`, `update_customer`, `delete_customer`, `get_next_customer_number`, `verify_customer_number`, `lookup_peppol_participant`, `verify_customer_peppol`, `list_contact_persons`, `add_contact_person`, `update_contact_person`, `delete_contact_person`, `list_contact_addresses`, `add_contact_address`, `update_contact_address`, `delete_contact_address`, `import_customers` | | Products | `list_products`, `get_product`, `create_product`, `update_product`, `delete_product`, `check_product_code`, `get_product_price`, `list_bundles`, `get_bundle`, `create_bundle`, `update_bundle`, `delete_bundle`, `expand_bundle`, `list_price_lists`, `get_price_list`, `create_price_list`, `update_price_list`, `delete_price_list`, `import_products` | | Webhooks | `list_webhooks`, `list_webhook_events`, `create_webhook`, `update_webhook`, `delete_webhook`, `test_webhook`, `list_webhook_deliveries`, `retry_webhook_delivery` | | Custom fields | `list_custom_fields`, `create_custom_field`, `update_custom_field`, `delete_custom_field`, `list_field_templates`, `create_field_template`, `update_field_template`, `activate_field_template`, `delete_field_template` | | Projects | `list_projects`, `get_project`, `create_project`, `update_project`, `delete_project`, `list_project_documents` | | Recurring invoices | `list_recurring_invoices`, `get_recurring_invoice`, `create_recurring_invoice`, `update_recurring_invoice`, `activate_recurring_invoice`, `pause_recurring_invoice`, `cancel_recurring_invoice`, `run_recurring_invoice_now`, `list_recurring_runs`, `preview_recurring_invoice`, `make_invoice_recurring`, `delete_recurring_invoice` | | Settings | `list_email_templates`, `save_email_template`, `get_options` | | VAT | `list_tax_rates`, `create_tax_rate`, `update_tax_rate`, `list_vat_periods`, `get_vat_worksheet`, `list_vat_returns`, `save_vat_return`, `reopen_vat_return`, `check_period_date`, `export_accounting` | | ERP intake | `dry_run_invoice`, `dry_run_erp_message`, `send_erp_message`, `list_erp_messages`, `get_erp_message`, `replay_erp_message`, `list_erp_connectors`, `update_erp_connector` | | Integrations | `list_connectors`, `sync_connector` | | Payments | `create_payment_link`, `list_payment_links`, `cancel_payment_link` | | Bank feeds | `list_bank_transactions`, `match_bank_transaction`, `ignore_bank_transaction`, `import_bank_statement`, `sync_bank_feeds` | | Account | `get_account`, `list_managed_organizations` | --- # ASP and FTA lifecycle > What happens between 'submit' and 'accepted', and how it shows in the API. Source: /docs/reference/asp-fta-lifecycle The UAE model has five corners: the seller (C1), the seller's accredited service provider (C2), the buyer's ASP (C3), the buyer (C4) and the FTA (C5). Alya Books is the seller's software; Taxilla is the ASP it submits through. ```mermaid sequenceDiagram participant App as Your system participant F as Alya Books API participant ASP as Taxilla (C2) participant B as Buyer ASP (C3) participant FTA as FTA (C5) App->>F: POST /invoices { submit: true } F->>F: validate PINT AE, allocate number, build UBL F->>ASP: submit XML ASP-->>F: requestId (status processing) Note over F: invoice.status = submitted (locked) ASP->>B: deliver document ASP->>FTA: report tax data F->>ASP: poll status (app, scheduler) ASP-->>F: delivered / accepted / rejected + stage Note over F: invoice.status = delivered → accepted | rejected F-->>App: webhook invoice.delivered / invoice.reported / invoice.rejected ``` ## Statuses | `invoiceStatus` | Meaning | Locked | | --- | --- | --- | | `draft` | Stored, not validated or failed validation | no | | `validated` | Passed PINT AE; can be submitted | no | | `submitted` | Accepted by the ASP for processing | yes | | `delivered` | The buyer's access point accepted the document | yes | | `accepted` | The FTA accepted the tax data ("reported") | yes | | `rejected` | Rejected by the ASP (`stage: taxilla`), the buyer access point (`buyer`) or the FTA (`fta`) | no; fix and resubmit, or credit | `GET /invoices/{id}/status` returns `authority[]`, newest first: ```json { "type": "fta", "provider": "taxilla", "mode": "live", "status": "rejected", "stage": "fta", "documentKind": "invoice", "referenceNumber": "INV-0042", "requestId": "TX-…", "submittedAt": "2026-09-23T10:00:05Z", "completedAt": "2026-09-23T10:00:41Z", "error": "Buyer TRN not registered" } ``` `jobStatus` summarises it: `ready` (validated), `processing` (submitted/delivered), `done` (accepted), `failed` (rejected); it is omitted for drafts. ## Modes The organization's e-invoicing connection has three modes (Settings → E-invoicing): | Mode | Behaviour | | --- | --- | | `mock` (Test, simulated) | No network; a simulated ASP answers with configurable scenarios (accept, reject at each stage, slow). Test documents can only be submitted here. | | `uat` | Taxilla's acceptance environment with your UAT credentials. | | `live` | Production. Test documents (`isTest: true`) are refused. | `GET /me` reports the current mode under `einvoicing.mode`. ## Resubmission and correction - A rejected invoice can be edited (it is unlocked) and submitted again; the number stays the same and the new attempt appears as another `authority[]` entry. - An accepted invoice cannot change. Issue a credit note (`381`) to cancel or reduce it, or a debit note (`383`) to increase it, both with `precedingInvoice: { id }`. Credit notes are checked against the original (currency, remaining creditable amount). - Payments, categories, archiving and custom fields are not part of the reported document and remain editable. ## Timing Submission returns after the ASP acknowledged the request and one status poll (about two seconds in UAT and live). Later stages arrive through the status checks the app and the scheduler run (every 5 minutes, less often as a submission ages, stopping after 7 days), and reach you through webhooks: `invoice.submitted`, `invoice.delivered`, `invoice.reported`, `invoice.rejected`. --- # Rounding rules > How Alya Books rounds amounts, VAT and totals, in the editor, the API, the XML and the VAT return. Source: /docs/reference/rounding All amounts are in the document currency with two decimals (fils for AED). One rounding function is used everywhere (`round2` in `_shared/ubl.ts`): **half away from zero**, after snapping the value to six decimals so floating-point noise cannot flip a half. Examples: `1.005 → 1.01`, `-1.005 → -1.01`, `2.675 → 2.68`, `0.1 + 0.2 → 0.30`. ## Order of operations 1. **Line net amount** (BT-131) = quantity × unit price, rounded. Quantities keep three decimals, prices two. 2. **Line VAT** shown on the line = line net × rate ÷ 100, rounded. It is informational; the XML carries no per-line VAT amount. 3. **Document allowances and charges** (BG-20, BG-21) are rounded amounts that join the tax group of their category and rate. 4. **Tax groups**: lines, allowances and charges are grouped by VAT category and rate. Per group, the taxable amount is summed and rounded, then **VAT = taxable × rate ÷ 100, rounded** (BT-116, BT-117). This is the amount in `cac:TaxSubtotal`. 5. **Totals**: line extension (BT-106), tax exclusive (BT-109) = lines − allowances + charges, VAT total (BT-110) = sum of group VAT, tax inclusive (BT-112) = tax exclusive + VAT, each rounded. Because VAT is rounded per group rather than per line, the sum of the line VAT amounts may differ from the group VAT by a fils. The group figure is the reported one; validation (BT-110, BT-112) accepts the recomputed values only. ## Where the same rules apply - The editor (`computeLines`, `computeDocumentTotals`), `POST /invoices` (`computeApiLines`), the PDF and the XML share the code, so the three documents always agree. - The VAT 201 worksheet re-derives each invoice's per-category VAT with the same rule in SQL (`round(taxable, 2)` then `round(taxable × rate / 100, 2)`), so box totals equal the sum of the reported tax subtotals. - Received documents are validated with a tolerance of one fils per total. - Gross entry (**Prices include VAT**) stores net = gross ÷ (1 + rate), rounded, and continues with the net rules. ## Tests `src/test/vat.test.ts` covers the half-up cases, the editor/API parity, the per-group VAT and the worksheet arithmetic; `src/test/api-core.test.ts` covers the document totals. --- # Errors > Status codes, error codes and how to handle them. Source: /docs/reference/errors Every error response has the same shape: ```json { "code": "E_VALIDATION", "statusCode": 400, "message": "buyer.trn must be 15 digits", "field": "buyer.trn", "extra": { } } ``` | HTTP | `code` | When | | --- | --- | --- | | 400 | `BAD_REQUEST` | Malformed JSON, invalid query parameter, nothing to update | | 400 | `E_VALIDATION` | Structural validation of the payload (`field` names the input) or PINT AE business rules (`extra.errors[]` with `rule`, `field`, `message`, `severity`) or custom fields (`extra.fields[]`) | | 401 | `UNAUTHORIZED` | Missing, malformed, unknown, revoked or expired key | | 403 | `FORBIDDEN` | Key lacks the scope (for example `periods:manage` to reopen a VAT return), or `X-Organization-Id` is not a managed client | | 404 | `NOT_FOUND` | Route or object not found in this organization | | 405 | `METHOD_NOT_ALLOWED` | `Allow` header lists the methods | | 409 | `CONFLICT` | Duplicate SKU, deleting an issued invoice, submission already in progress, accepting twice | | 409 | `period_locked` | The document is dated in a locked period (filed VAT return or lock date); `extra` has `lockSource`, `periodStart`, `periodEnd`, `firstOpenDate` | | 409 | `lock_date_covers_period` | Reopening a VAT return that the lock date for everyone covers | | 409 | `fiscal_settings_conflict_locked_periods` | Changing the VAT period settings would cut through a filed return | | 413 | `PAYLOAD_TOO_LARGE` | Import over 20 MB | | 422 | `E_VALIDATION` | Incoming import recorded but failed validation (`extra` carries the import) | | 429 | `RATE_LIMITED` | Over 600 requests per minute; `Retry-After` header | | 501 | `NOT_IMPLEMENTED` | Feature not available yet | | 502 / 503 | `SERVICE_UNAVAILABLE` | ASP unreachable, email provider failed, PDF of a document other than an invoice, credit or debit note or proforma requested where no PDF service is configured (`?format=html` still works) | | 500 | `INTERNAL_SERVER_ERROR` | Unexpected; include `X-Request-Id` when reporting it | ## Validation errors, explained Every PINT AE validation issue the API returns (`extra.errors[]` on a 400, and `errors[]` / `warnings[]` on the validation, status, create and dry-run responses) carries more than the rule and a message: ```json { "rule": "IBR-190-AE", "field": "lines[0].tax_rate", "path": "items[0].taxRate", "scope": "document", "severity": "error", "message": "Line 1: Standard-rated lines carry 5 %", "explanation": "A line taxed at the standard rate must state the UAE standard rate of 5 %.", "fix": "Set the line's VAT rate to 5, or change its VAT category.", "explanationAr": "…", "fixAr": "…", "docsUrl": "https://alyabooks.ai/reference/rules/IBR-190-AE.html" } ``` - `path` is the field as you sent it (`items[0].taxRate`, `buyer.trn`, `precedingInvoice.number`); `field` is the internal name, kept for compatibility. Input checks that run before the rules (a 14-digit TRN, a missing description) answer 400 with `field`, `path` and `scope` at the top level of the envelope. - `scope` says where the fix is: `document` in the request, `organization` under Settings → Company (seller TRN, address, bank account, licence). - `explanation` and `fix` are Alya Books's plain-language text; the Arabic pair is there for finance teams working in Arabic. Business-term codes (BT-…) carry a link to the field catalogue instead of an explanation. - `docsUrl` is public and needs no login, so it can go straight into an ERP job note or a webhook receiver's log. The links are built on the deployment's public site (`PUBLIC_SITE_URL`, else `APP_URL`); the webhook sender rewrites the links in payloads to the same site before signing. ## Batch responses `POST /invoices`, `/customers/import` and `/products/import` process elements independently. For invoices, a failure turns the whole response into an error whose `extra.results[]` holds per-element outcomes; created elements are marked `success: true`. Customer and product imports answer `207` with `results[]` and never roll back created elements. ## Validation rules you will meet | Rule | Message | Fix | | --- | --- | --- | | BT-31 / BT-35 / BT-37 / BT-40 | Seller TRN, street, city or country missing | Complete the organization profile in the app | | BT-44 | Buyer name is required | Provide `buyer.name` or `buyer.id` | | BT-48 | Buyer TRN must be 15 digits | Correct `buyer.trn`, or omit it for unregistered buyers (warning only) | | BT-25 | Preceding invoice reference required | Credit and debit notes need `precedingInvoice` | | BT-129 / BT-146 | Quantity must be > 0, price cannot be negative | Fix the line | | BT-152 | UAE standard VAT rate must be 5 % | Use `taxRate: 5` or another category | | BT-106 / BT-110 / BT-112 | Totals mismatch | Only on `PUT` with hand-computed totals; let Alya Books compute them | | BT-130 | Unit code not in UN/ECE Rec 20 | Warning; use `EA`, `HUR`, `KGM`, … | The [PINT AE field reference](/docs/reference/pint-ae-fields) lists all business terms. ## Idempotency The API has no idempotency keys yet. Use `externalId` on invoices (`GET /invoices?externalId=`) and `reference` on imports to detect repeats before you retry, and reconcile batches with `extra.results`. --- # Diagrams > Flows worth having in your head before you integrate. Source: /docs/reference/diagrams ## Outgoing invoice ```mermaid flowchart LR A[POST /invoices] --> B{PINT AE valid?} B -- no, draftOnly --> D[draft] B -- no --> E[400 E_VALIDATION] B -- yes --> V[validated] V -- submit --> S[submitted] S --> L[delivered] L --> R[accepted / reported] S --> X[rejected] L --> X X -- fix & resubmit --> V R -- credit / debit note --> A ``` ## Incoming invoice ```mermaid flowchart LR U[Upload / email / Peppol / API] --> P[Import record] P --> T{UBL?} T -- yes --> V[Parse + validate PINT AE + buyer TRN] T -- PDF/image --> X[AI extraction or manual] V -- duplicate / invalid --> F[failed 422] V -- ok --> D[incoming invoice draft] X --> D D --> R{Review} R -- accept --> A[accepted, locked, payable] R -- reject --> J[rejected] A --> M[payments → paid] ``` ## Webhook delivery ```mermaid sequenceDiagram participant DB as Database trigger participant Q as Outbox participant W as Worker participant T as Your endpoint DB->>Q: emit event → one delivery per subscriber W->>Q: take due deliveries W->>T: POST signed JSON (10 s timeout) alt 2xx T-->>W: ok W->>Q: delivered else error / timeout W->>Q: retry in 1m, 5m, 30m, 2h, 12h; then failed end ``` ## Keys and permissions ```mermaid flowchart TD K[API key sk_live / sk_test] --> S[Scopes] S --> R1[invoices:read/write] S --> R2[incoming:read/write] S --> R3[customers, products, metadata, settings] S --> R4[webhooks:manage] K --> O[Organization] O -. X-Organization-Id .-> C[Managed client organization] ``` --- # Glossary > Terms used in UAE e-invoicing and in this documentation. Source: /docs/reference/glossary | Term | Meaning | | --- | --- | | **ASP** | Accredited service provider: a company accredited by the UAE Ministry of Finance to validate, exchange and report e-invoices. Alya Books submits through Taxilla. | | **BT / BG** | Business term / business group: numbered data elements of EN 16931 and PINT (`BT-31` seller TRN, `BG-25` invoice line). | | **Credit note** | Document type `381` that reduces or cancels an issued invoice; references the original with BT-25/BT-26. | | **Debit note** | Document type `383` that increases an issued invoice. | | **Delivered** | The buyer's access point accepted the document (corner 3 → 4). | | **EN 16931** | The European semantic model for e-invoices that PINT builds on. | | **Id** | The UUID of an object (`id` in responses; `invoiceId` where a response refers to an invoice). | | **FTA** | Federal Tax Authority of the UAE, corner 5 of the model; receives the tax data. | | **Incoming invoice** | A document received from a supplier (accounts payable). | | **Live / test key** | `sk_live_…` creates real documents; `sk_test_…` creates test documents that never reach the live ASP. | | **Metadata** | Custom field values (`metadata: { key: value }`) defined by templates. | | **Outgoing invoice** | A document issued by the organization (accounts receivable). | | **Peppol** | The network and standards used for exchange; participants are identified by scheme `0235` and their 10-digit TIN (the first 10 digits of the TRN) in the UAE, for example `0235:1002345678`. | | **PINT AE** | The UAE specialisation of the Peppol International Invoice model (`urn:peppol:pint:billing-1@ae-1`). | | **Reported** | The FTA accepted the tax data; invoice status `accepted`. | | **Review status** | Internal approval state (`none`, `pending`, `approved`, `declined`); pending or declined blocks submission. | | **Scope** | Permission attached to an API key (`invoices:write`, `webhooks:manage`, …). | | **Series** | A numbering sequence per document kind with a template such as `INV-{YYYY}-{n}`. | | **Transaction type** | UAE classification of the supply (`standard`, `free_trade_zone`, `exports`, …) written into the XML. | | **TRN** | Tax Registration Number, 15 digits, issued by the FTA. | | **UBL** | Universal Business Language, the XML syntax used by PINT AE (Invoice and CreditNote documents). | | **Validation** | The PINT AE business-rule check Alya Books runs before storing or submitting an invoice. | | **Webhook** | An https endpoint that receives signed event notifications. | --- # FAQ > Answers to the questions integrators ask first. Source: /docs/reference/faq **Is there a sandbox?** Use a test key (`sk_test_…`). It works against the same API and organization, creates documents flagged `isTest`, and can only submit while the e-invoicing connection is in Test (simulated) mode, where a simulated ASP answers. Test documents are excluded from reports and analytics. **Do I have to send seller data with every invoice?** No. Seller name, TRN, address and contact (BT-27 to BT-43) come from the organization profile. If they are incomplete, validation fails with the corresponding business term until the profile is completed in the app. **Can I supply my own invoice numbers?** Yes, with `invoiceNumber`. It must be unused. Without it, the next number of the series is allocated atomically. Keep issued tax invoices sequential as required by UAE VAT rules. **Why did my batch return 400 although some invoices were created?** Elements are processed independently. The status mirrors the first failure and `extra.results[]` shows every element; those with `success: true` exist. Retry only the failed ones. **How do I know when the FTA accepted an invoice?** Subscribe a webhook to `invoice.reported` (and `invoice.rejected`), or poll `GET /invoices/{id}/status` until `invoiceStatus` is `accepted` or `rejected`. **Can I change an invoice after submission?** No. Issue a credit note (`381`) or debit note (`383`) with `precedingInvoice: { id }`. Payments, category, cost centre, archive flag and custom fields remain editable through `PATCH /invoices/{id}`. **Can I get the invoice as a PDF?** Yes. `GET /invoices/{id}?format=pdf` returns the PDF of any invoice, credit note, debit note or proforma, the same document the app downloads and attaches to emails. `?format=xml` gives the PINT AE XML and `?format=html` a printable version. A 503 is only possible for a received invoice's visualisation when the environment has no HTML-to-PDF service. **Why is the delivery state 'skipped' when I send an invoice?** No email provider is configured on that environment. The delivery is logged so the audit trail is complete; configure the provider (Resend) to send for real. **Which currencies are supported?** Any ISO 4217 code with `exchangeRate` against AED; `vatAccountingCurrency` covers BT-6. Reporting is in AED. **How does a CA firm act for its clients?** Create the key in the firm's organization and send `X-Organization-Id` with the client's organization id on each request. `GET /organizations/managed` lists the ids. **What is the rate limit?** 600 requests per minute per key; `429` with `Retry-After` beyond that. Batch endpoints count as one request. **Where is the OpenAPI specification?** `GET /openapi.json` on the API, and in the API reference tab of this site. `GET /llms.txt` returns a condensed guide for language models. --- # Changelog > What changed in the API, the MCP server and the CLI. Source: /docs/reference/changelog ### 13 October 2026 (Brand-neutral identifiers) **New identifiers.** New API keys start with `sk_test_` or `sk_live_`. Webhook deliveries carry `X-Webhook-Signature`, `X-Webhook-Event`, `X-Webhook-Event-Id`, `X-Webhook-Delivery` and `X-Webhook-Attempt`, with the user agent `Webhook-Delivery/1.0`. Create results and ERP message replies name the invoice `invoiceId`. MCP resources use `einvoicing://` URIs and tool results carry `_meta: { "einvoicing/environment": … }`. OAuth access tokens, refresh tokens, authorization codes and client ids start with `oat_`, `ort_`, `oac_` and `ocl_`. **CLI** is published as `@alyabooks/cli` and runs as `alyabooks`; settings live in `~/.alyabooks/config.json` and the `ALYABOOKS_API_KEY`, `ALYABOOKS_API_URL` and `ALYABOOKS_ORGANIZATION_ID` environment variables. **Nothing to migrate.** Keys, tokens, webhook subscriptions and integrations created earlier keep working on deployments that accept the previous identifiers. Verify signatures with the new header names when you next touch your webhook handler. ### September 2026 (v1.0) **Public API v1.** Invoices (batch create, list, JSON/PDF/XML download, status with ASP and FTA timeline, validation, submit, send, payments, housekeeping), invoice numbers and series settings, incoming imports with content sub-resources, customers, products, custom fields, webhooks, account endpoints. API keys with scopes and test/live environments, `X-Organization-Id` for CA firms, rate limiting, request log. **Webhooks.** 24 events, HMAC-SHA256 signatures, retries with back-off, delivery log, test deliveries, automatic pause after repeated failures. **Custom fields.** Field definitions and per-kind templates; values on documents and lines, validated, printed on PDFs and included in webhook payloads. **MCP server** at `POST /api/v1/mcp` with about 70 tools and 7 resources. **CLI** `@alyabooks/cli` 1.0. **Docs.** This site, the OpenAPI document at `/openapi.json`, `llms.txt`, `AGENTS.md` and `CLAUDE.md` for coding agents. ### October 2026 (v1.1) **Catalogue depth.** Customers gain company/person, legal form, category, `defaultPriceListId`, contact persons (`/customers/{id}/contact-persons`) and delivery/billing addresses (`/customers/{id}/addresses`); Peppol lookup (`GET /customers/peppol-lookup`, `POST /customers/{id}/verify-peppol`). Products gain item number, GLN, cost price with margin, inventory tracking and category. New resources: `/bundles` (with `GET /bundles/{id}/lines`), `/price-lists` and `GET /products/{id}/price`. **Projects** (`/projects`) with totals, `projectId` on invoice inputs, `?projectId=` list filter, `GET /projects/{id}/documents`. Webhook `project.created`. **Recurring invoices** (`/recurring-invoices`) with schedules, automation, runs, preview, `POST /invoices/{id}/make-recurring`; daily runner. Webhooks `recurring_invoice.generated` and `recurring_invoice.failed`. **MCP** grows to about 115 tools and adds the `recurring_schedules` resource. **CLI** adds `project`, `recurring`, `bundle`, `price-list`, `customer persons|addresses|peppol` commands. ### October 2026 (v1.2) **VAT.** Tax rates per organization (`/settings/tax-rates`) with exemption reasons written into the XML (`taxExemptionReasonCode` / `taxExemptionReason` on lines); `supplyEmirate` on invoices; VAT 201 worksheet (`GET /vat/worksheet`, JSON or CSV), tax periods (`GET /vat/periods`) and saved returns (`/vat/returns`). Rounding is half away from zero everywhere (see [Rounding rules](/docs/reference/rounding)). **Accounting exports.** `GET /exports/accounting` produces Zoho Books, Xero and QuickBooks Online CSV and Tally XML for issued invoices or accepted incoming invoices. **MCP** adds eight VAT tools; **CLI** adds `tax-rate`, `vat` and `export accounting`. ### November 2026 (v1.3) **Accounting connectors.** QuickBooks Online and Xero (OAuth) and Odoo (JSON-RPC) on the connector framework, each with a sandbox mode: `GET /connectors`, `POST /connectors/{id}/sync` (pull contacts and invoices, push issued invoices once), `GET /connectors/{id}/links`. Webhook `connector.synced`. See [Accounting connectors](/docs/guides/connectors). **Pay-by-link.** Payment gateways (Stripe, Telr, PayTabs, Tap, mock) under Settings → Payments; `POST /invoices/{id}/payment-link`, `GET /payment-links`, `POST /payment-links/{id}/cancel`; public pay page at `/pay/{token}`; gateway confirmations settle the link and record the payment. Webhook `payment_link.paid`. **Bank feeds.** Lean Technologies, Tarabut and mock connections, statement import (`POST /bank/import`), transactions with match suggestions (`GET /bank/transactions`), confirm/unmatch/ignore, optional automatic confirmation. Webhook `bank.transaction_matched`. See [Pay-by-link and bank feeds](/docs/guides/payments-and-bank-feeds). **MCP** adds ten integration, payment and bank tools; **CLI** 1.2 adds `connector`, `pay-link` and `bank`. ### November 2026 (v1.4) **Simulated receiving.** `POST /incoming/simulate` (Test mode only) generates supplier invoices for a scenario — `standard`, `mixed_vat`, `reverse_charge`, `credit_note`, `foreign_currency`, `wrong_buyer`, `totals_mismatch`, `duplicate` — from a sample supplier or one of your contacts, and receives them through the real Peppol pipeline (validation, duplicate check, notifications, webhooks). `GET /incoming/simulate` lists scenarios and suppliers. **Email templates.** `GET /settings/email-templates`, `PUT` and `DELETE /settings/email-templates/{kind}/{locale}`: subject and body per document kind in English and Arabic with `{{placeholders}}`. `POST /invoices/{id}/send` without `subject` or `message` now uses the organization's template. **Options.** `GET` / `PATCH /settings/options`: hidden modules, draft number reservation (drafts may carry a temporary `DRAFT-…` number that becomes the real number on validation), inventory on issue and negative-stock policy (`allow`, `warn`, `block`), duplicate customer numbers, default CC/BCC applied to sends that set none. **MCP** adds `simulate_incoming_invoice`, `list_email_templates`, `save_email_template`, `get_options`; **CLI** 1.3 adds `import simulate`, `email-template` and `options`. ### November 2026 (v1.5) **Options.** `GET` / `PATCH /settings/options` adds three fields: | Field | Type | Default | Meaning | | --- | --- | --- | --- | | `allowManualNumbers` | boolean | `false` | Whether people may type document numbers in the app editor. Off: the number field is read-only and new documents take the next number of the series. The API and imports can always supply `invoiceNumber`. | | `showTextBlocks` | boolean | `false` | Whether new invoices in the app editor show salutation, introduction, closing and signature. The subject line always shows. Quotes and letters are not affected. | | `terminology` | object | `{}` | `{ "buyer": { "singular", "plural" }, "seller": { "singular", "plural" } }`: the words the app uses for contact types (defaults customer/customers and supplier/suppliers). Lower case, up to 30 characters; `null` resets. Stored `type` values stay `buyer`, `seller`, `both`. | **Number series.** The app's Document Numbering settings now also list the `customer` (default `C-{n}`) and `item` (default `IT-{n}`) series. ### November 2026 (v1.6) **Terminology for received documents.** `terminology.incoming` on `GET` / `PATCH /settings/options` sets what the app calls incoming invoices: `{ "incoming": { "singular": "bill" } }`, one of `incoming invoice` (default), `bill`, `receipt`, `purchase invoice`; the plural follows. Only app labels change; the `/incoming` resources and `incoming.*` webhook events keep their names. **Behaviour change.** A `PATCH` with `terminology` now keeps the roles you leave out at their current words (before, they were reset to the defaults). Send `"terminology": null` to reset everything. **E-invoice status.** Submissions still in flight are now checked in the background, so `invoice.delivered`, `invoice.reported` and `invoice.rejected` fire without anyone polling the status endpoint. **Incoming invoice content.** `PATCH /import/incoming/{importId}` edits the content of drafts: header, `seller` / `buyer` / `delivery` parties, `lineItems` (with `itemDescription` and `taxCategory`), `vatBreakdown`, `paymentTerms`, `earlyPaymentDiscounts` and `paymentMeans` (several, with account owner, BIC, bank name and reference). `GET …/content` now returns parties in camelCase with the same keys, `paymentMeans` as an array (was one object with `meansCode` / `accountId`), `paymentTerms.earlyPaymentDiscounts`, and the delivery address under `delivery`. Received XML fills the delivery address, every payment means, BIC, item descriptions and GLN. `projectId` can be set on incoming invoices at any time and is returned in the summary. MCP `update_incoming_invoice` and CLI `import update` cover the same edits. **Simulation.** New scenario `with_pdf` for `POST /incoming/simulate`: a valid invoice with the supplier's PDF embedded in the XML. ### October 2026 (v1.3) **Paid in advance and rounding.** Invoice inputs accept `prepaidAmount`, `roundingAmount`, `prepaymentInvoice` and `items[].billingPeriod`; responses add `payableAmount`, and `outstanding` follows it. **Locked periods.** Filing a VAT return locks its period; lock dates `lockDateAll` and `lockDateNonAdmin` and `fiscalYearStartMonth` in `/settings/options`. Writes into a locked period return `409 period_locked`. New `POST /vat/returns/{id}/reopen`, `GET /vat/check-date`, scope `periods:manage`, webhooks `vat_return.filed`, `vat_return.reopened` and `lock_date.changed`, MCP tools `reopen_vat_return` and `check_period_date`. The `reopen: true` flag on `POST /vat/returns` now needs `periods:manage`. **Errors.** Permission errors from the database return `403` (were `500`). **Account.** Webhook `organization.ownership_transferred`. Two-factor login and section access apply to people signing in to the app; API keys are not affected. ### Planned (Next) CRM lead sync, idempotency keys, OAuth for third-party apps. ### October 2026 (v1.6) **Deemed supplies have nothing due.** An invoice whose `transactionTypes` include `deemed_supply` is stored and returned with no `dueDate`, no `paymentTerms` and an empty `paymentMeans`, whatever the input carried, and its `paymentStatus` is the new value `nothing_due`: `outstanding` is `0`, project `outstandingTotal` leaves it out, it is never overdue, `invoice.paid` is never emitted for it and `POST /invoices/{id}/payment-link` refuses it. The value is additive; code that switches on `paid` / `unpaid` should treat it like `paid`. `paymentStatus` filters accept it. **Rules applied up front.** Transaction types a document kind forbids (`summary_invoice`, `deemed_supply`, `profit_margin` on 480 and 81, IBR-157-AE), line VAT categories a kind or type fixes (IBR-122-AE, IBR-116-AE) and document-level allowances on a margin scheme invoice (IBR-114-AE) are still reported by validation through the API; the web editor now prevents them before validation. ### October 2026 (v1.7) **ERP intake.** `POST /erp/{connector}/messages` takes what an ERP pushes, a CargoWise UniversalTransaction (bare, in a UniversalInterchange or a SOAP envelope, credit notes with their `CreditNoteReason`) or one InvoiceInput as JSON, stores it, maps it through the organisation's charge map (`GET/PUT /erp/connectors/{type}`) and creates and submits the invoice. A resend answers `duplicate`; failures are kept and can be replayed (`GET /erp/messages`, `GET /erp/messages/{id}`, `POST /erp/messages/{id}/replay`). HTTP Basic authentication with the key as password is accepted. See [ERP intake](/docs/guides/erp-intake). **Dry run.** `POST /invoices/validate` maps and validates an InvoiceInput, or an ERP message with `?connector=cargowise`, without storing anything: the report, mapping notes and a preview. **Explained errors.** Every validation issue carries `path` (the field as you sent it), `scope` (`document` or `organization`), Alya Books's `explanation` and `fix` in English and Arabic, and a public `docsUrl`; input-check errors carry `path` and `scope` too. Invoice webhook payloads add `validation` and, on `invoice.rejected`, `rejection` with the stage and the provider's error. See [Errors](/docs/reference/errors). **MCP** adds eight ERP tools (`dry_run_invoice`, `dry_run_erp_message`, `send_erp_message`, `list_erp_messages`, `get_erp_message`, `replay_erp_message`, `list_erp_connectors`, `update_erp_connector`), marks submitting, sending, replaying and reopening a VAT return as destructive, and names the environment (`test` or `live`) in every result's `_meta`. **Docs.** Every page has Copy as Markdown, View as Markdown, Open in Claude and Open in ChatGPT; each page is plain Markdown at its address plus `.md`, and the whole site is `/docs/llms-full.txt`. ### October 2026 (v1.8) **MCP over OAuth.** claude.ai, ChatGPT and other MCP clients can connect as custom connectors with a sign-in instead of an API key: protected resource and authorization server metadata under the API base, dynamic client registration, authorization code with PKCE, rotating refresh tokens, revocation. The user approves the organisation, test or live and the scopes in the Alya Books app; connections are listed and revoked under API access → API keys → Connected apps. Access tokens work on `/mcp` only. See [MCP server](/docs/guides/mcp-server). **Charge maps merge.** `PUT /erp/connectors/{type}` takes `mode: "merge"` with `removeChargeCodes` / `removeVatCodes`; `update_erp_connector` always merges; the audit log keeps the previous maps. ### October 2026 (v1.9) **Invoice PDFs without a PDF service.** `GET /invoices/{id}?format=pdf`, `download_invoice` and email attachments return the PDF of invoices, credit and debit notes and proformas on every environment; Alya Books draws them with the same generator as the app. **Download links.** `GET /invoices/{id}/download-link?format=pdf|xml|html` stores the file privately and returns a signed `url` that works for 15 minutes without an API key. `download_invoice` returns a PDF this way by default (`delivery: "inline"` for base64). **Draft buyer details.** A customer's TRN or name change now reaches its draft and validated invoices. ### October 2026 (v1.10) **Document designs.** `GET /design-templates` lists the organisation's PDF designs with their layout: `standard`, or `logistics` with a shipment panel (job number, house bill, references, custom fields shown on PDF). Invoices take `designTemplateId` on create and through `PATCH /invoices/{id}`; ERP connectors take `designTemplateId` so their invoices use it. MCP: `list_design_templates`, and `designTemplateId` on `update_invoice_housekeeping` and `update_erp_connector`. **Invoice PDF.** Prints the buyer's address and trade licence under Bill To, the design's accent colour and the company logo (PNG or JPEG), and custom fields flagged "show on PDF". ### October 2026 (v1.11) **CargoWise envelopes.** `POST /erp/cargowise/messages` accepts any Content-Type and unwraps the Universal XML from SOAP (elements, escaped text or base64 gzip stream), JSON fields, base64, gzip and form posts. Messages store and return the sender's `messageId` and the `envelope` they arrived in; the dry run (`POST /invoices/validate?connector=cargowise`) unwraps the same way. **Security.** Bank matching and payment links refuse callers who are not signed in; connector credentials are no longer readable by organisation members; scheduled jobs refuse every call when their secret is not configured. ### October 2026 (Docs) **CargoWise integration kit.** New pages for integrators: [CargoWise integration kit](/docs/guides/cargowise-integration), [Testing a CargoWise integration](/docs/guides/cargowise-testing), [CargoWise field mapping](/docs/reference/cargowise-field-mapping) (also as CSV), [Replies to the ERP](/docs/reference/erp-replies) and [Status updates back to the ERP](/docs/guides/erp-status-updates), with a charge-code template to download. Background jobs now run on a schedule: FTA status checks and webhook retries every 5 minutes, the Peppol fetch every 15 minutes, daily jobs at 02:00 and 18:45 UAE time. --- # API reference > Every route of the Alya Books API, generated from the OpenAPI document. Source: /docs/api-reference/introduction The pages in this tab are generated from the OpenAPI 3.0 document served at `GET https://api.alyabooks.ai/api/v1/openapi.json`. Each operation lists its parameters, request body, responses and the scope the API key needs (`x-scope`). - Authentication: `X-API-KEY` header ([Getting started](/docs/getting-started)). - Optional `X-Organization-Id` header on every operation for CA firms. - Error shape and codes: [Errors](/docs/reference/errors). - Try requests with the CLI (`alyabooks --json`) or import the document into Postman, Insomnia or Bruno. --- # Alya Books public API v1 Base URL: `https://api.alyabooks.ai/api/v1` (until the custom domain is live: `/functions/v1/api-v1`). The full specification is in [openapi.json](openapi.json) and is also served at `GET /openapi.json`. A Postman collection generated from it is [api.postman_collection.json](api.postman_collection.json) (also at `/docs/api.postman_collection.json` on the site); set its `baseUrl` and `apiKey` variables and start with the "Start here: ERP integration" folder. ## Authentication Create a key under **API access** in the app (owners, admins, or members with the *Manage API keys* permission). Keys are `sk_live_…` or `sk_test_…`; only a SHA-256 hash is stored, so copy the key when it is shown. ``` X-API-KEY: sk_live_82eddee6f1db1d424824d4005fff80903125ac91dbbedb36 ``` - Test keys create test documents (`isTest: true`) that never reach the live ASP and are excluded from reports. They are the "demo environment". - Scopes limit a key to resources (`invoices:read`, `invoices:write`, `incoming:*`, `customers:*`, `products:*`, `metadata:*`, `settings:*`, `webhooks:manage`). Write implies read. - CA firms send `X-Organization-Id: ` to act for a managed client; `GET /organizations/managed` lists them. - Rate limit: 600 requests per minute per key (`429` with `Retry-After`). ## Conventions - JSON bodies, camelCase fields, dates as `YYYY-MM-DD` (`YYYYMMDD` accepted on input). - Lists return `{ data: [...], meta: { page, perPage, totalPages, totalItems } }` with `page`, `perPage`, `sort`, `order`, `keyword`. - Errors return `{ code, statusCode, message, extra?, field? }`. Validation failures use `code: "E_VALIDATION"` with `extra.errors[]`; each issue has the rule, `path` (the field as you sent it), `scope` (document or organization), the message, Alya Books's `explanation` and `fix` in English and Arabic, and a public `docsUrl`. - Async steps (`POST /invoices`, `/submit`, `/send`, `/import/incoming`) answer `202`; poll `/status` or subscribe to webhooks. ## Create and report an invoice ```bash curl -X POST "$BASE/invoices" \ -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \ -d '{ "invoices": [{ "buyer": { "name": "Buyer LLC", "trn": "100000000000011", "email": "ap@buyer.ae" }, "issueDate": "2026-09-23", "payment": { "termsDays": 30, "accountId": "AE070331234567890123456" }, "items": [{ "description": "Consulting, September", "quantity": 10, "unitPrice": 500 }], "metadata": { "po_number": "PO-4711" }, "submit": true, "send": { "to": ["ap@buyer.ae"] } }] }' ``` Response `202`: ```json [{ "index": 0, "success": true, "invoiceId": "…", "invoiceNumber": "INV-0042", "status": "submitted", "validation": { "validity": true, "errors": [], "warnings": [] }, "submission": { "status": "processing", "requestId": "…", "mode": "mock" }, "delivery": { "state": "sent", "deliveryId": "…" }, "detailsPage": "https://app.alyabooks.ai/invoices/…/edit" }] ``` When one element fails, the status mirrors the first failure (usually `400`) and `extra.results` lists every element; elements with `success: true` **were** created, so retry only the failed ones. Then: - `GET /invoices/{id}` — JSON; `?format=pdf`, `?format=xml` (PINT AE UBL), `?format=html`. - `GET /invoices/{id}/status` — `invoiceStatus` (draft → validated → submitted → delivered → accepted | rejected), `authority[]` (ASP/FTA timeline), `delivery[]`, `validation`. - `POST /invoices/{id}/payments` — `{ "amount": 5250, "method": "bank_transfer" }`. - `GET /invoices/next-number`, `GET /invoices/verify-number?value=INV-0043`, `GET|PUT /settings/invoice-number-template`. ## Incoming invoices ```bash curl -X POST "$BASE/import/incoming" -H "X-API-KEY: $KEY" -F file=@supplier-invoice.xml ``` `202` with `importId`, `incomingInvoiceId`, `status` and `validation`; `422` when the document failed validation (the import is still logged). Then `GET /import/incoming/{importId}/{status|validation|content|content/lineitems|content/pdf|source|attachments}` and `PATCH /import/incoming/{importId}` with `{ "status": "accepted" }`. ## ERP intake (CargoWise and pushed messages) Try it in the browser first: the site's test console (`/console`) sends freight samples and a dummy data feed through the dry run and the intake, with or without a test key. `POST /erp/cargowise/messages` takes a CargoWise UniversalTransaction XML as eAdaptor posts it (bare, in a UniversalInterchange or in a SOAP envelope); `POST /erp/json/messages` takes one InvoiceInput keyed by `externalId`. The message is stored, mapped through the organisation's charge map (`PUT /erp/connectors/cargowise`), then created and submitted like `POST /invoices`. A resend answers `200` with `status: duplicate`; a mapping or rule failure answers `400` (or `202` with `?ack=always` for push transports) and stays in the log: `GET /erp/messages?status=failed`, `GET /erp/messages/{id}` (with the raw body), `POST /erp/messages/{id}/replay`. The intake routes also accept HTTP Basic authentication with the API key as the password. `POST /invoices/validate` (JSON, or `?connector=cargowise` with XML) is a dry run: the validation report and a preview of the mapped invoice, nothing stored. ## Webhooks `POST /webhooks` with `{ "targetUrl": "https://…", "events": ["invoice.reported", "invoice.paid"] }` returns the signing secret once. Each delivery is a JSON POST: ``` X-Webhook-Event: invoice.paid X-Webhook-Delivery: X-Webhook-Signature: t=1700000000,v1=."> ``` Invoice payloads carry `validation` (validity and the blocking rule errors with links) and, on `invoice.rejected`, `rejection` (stage, provider error, submission id). Verify with the secret and reject timestamps older than five minutes. Deliveries retry after 1 min, 5 min, 30 min, 2 h and 12 h; `GET /webhooks/{id}/deliveries` shows the log, `POST /webhooks/{id}/test` sends a `webhook.test` event. Events: `GET /webhooks/events`. ## Custom fields Define fields (`POST /metadata/fields`) and group them in a template per document kind (`POST /metadata/templates`, `…/activate`). Values travel as `metadata: { key: value }` on invoices, lines and incoming invoices and are validated against the active template.