Next e-invoicing deadline: Businesses with revenue of AED 50 million or more must appoint an accredited service provider by 30 October 2026.

Read the guide

For developers

Connect your own system

Send invoices and credit notes from the system you already run, and receive the ones your suppliers send you. One API key, JSON over HTTPS, and the same accredited Access Point the product uses.

The same price as the screens

A document sent or received through the API counts exactly as one sent or received in the product: against the 200 included with a company's plan each year, then against its prepaid packs. There is no separate API fee, no per-call charge and no different rate. Drafts, documents that fail validation and documents rejected before transmission use none.

Before you start

Base URL

Every path below is relative to this host. The address shown is the test environment; confirm the production host with Tech Ventures before you go live, because it is a different one.

Test (UAT)
https://ca-einvoicing-api-uat.blackcoast-6e3be86c.uaenorth.azurecontainerapps.io

Authentication

One header on every request. There is no login step, no token to exchange and nothing to refresh.

X-Api-Key: tvk_live_a1b2c3d4_<secret>
  • The key is generated once by the company's own administrator, from Company Information in the portal, and shown in full only at that moment. It cannot be read back afterwards — only regenerated.
  • Regenerating a key invalidates the previous one immediately. If calls suddenly fail to authenticate, check which key is in use.
  • A key belongs to exactly one company, permanently. Integrating several companies means a key for each.
  • Send it whole, starting with tvk_live_, with no stray spaces. A truncated key is refused with the same message as a wrong one.

Requests and responses

  • JSON in and JSON out, as application/json.
  • Dates on the sales side are MM/DD/YYYY.
  • Field names are camelCase and case-sensitive; they must match exactly.
  • Failures are JSON too, with success: false and an explanation. There is no second error format to handle.

Attachments

Up to 512 KB per file, base64 inside the JSON body, and only pdf, xml, jpeg, png, csv, ods or xlsx. A file that is too large, of the wrong type, or that fails the malware scan fails the whole submission — the document is not created without it.

Sending an invoice or a credit note

One call creates the document, validates it and queues it for the network. documentType decides which kind it is: 380 a commercial invoice, 480 an invoice out of scope of tax, 381 a credit note, 81 a credit note for goods or services.

POST/api/public/v1/outbound

Sending the same thing twice is safe

invoiceNo is the idempotency key. Send the same number again and you get the document that already exists, with alreadyExisted: true, rather than a duplicate. This is what makes a retry after a timeout safe.

What happens, in order

  1. 1.The document is created and validated as one operation. If validation fails, nothing is created at all — a 422 comes back listing every reason.
  2. 2.Attachments are stored and scanned before validation completes.
  3. 3.The send to the Access Point then happens in the background, which is what submissionQueued: true is telling you.
Request
POST /api/public/v1/outbound
X-Api-Key: tvk_live_a1b2c3d4_<secret>
Content-Type: application/json

{
  "invoiceNo": "INV-2026-00125",
  "documentType": "380",
  "transactionType": "00000000",
  "invoiceDate": "03/14/2026",
  "dueDate": "04/13/2026",
  "currency": "AED",
  "buyer": {
    "name": "Gulf Trading LLC",
    "taxId": "100123456700003",
    "email": "ap@gulftrading.ae",
    "countryCode": "AE",
    "city": "Dubai",
    "addressLine1": "Office 1204, Business Bay"
  },
  "lines": [
    {
      "lineNo": 1,
      "description": "Consulting services, March 2026",
      "quantity": 10,
      "unitPrice": 450.00,
      "vatCategory": "S",
      "vatRate": 5
    }
  ],
  "attachments": [
    { "fileName": "timesheet.pdf", "contentType": "application/pdf", "content": "<base64>" }
  ]
}
Accepted — 202
HTTP/1.1 202 Accepted

{
  "success": true,
  "alreadyExisted": false,
  "submissionQueued": true,
  "header": {
    "id": "7c9f1a44-55e2-4a1d-9d0b-2f0e8c6b31aa",
    "invoiceNo": "INV-2026-00125",
    "status": "Submitted"
  }
}
Rejected — 422
HTTP/1.1 422 Unprocessable Entity

{
  "success": false,
  "message": "The document failed validation.",
  "validations": [
    { "field": "buyer.taxId", "message": "A 15-digit UAE TRN is required for a 380 document." },
    { "field": "lines[0].vatRate", "message": "vatRate must be 5 when vatCategory is S." }
  ]
}

Nothing was created. Correct the document and send it again under the same invoiceNo.

Checking what happened to it

The 202 means the portal has it, not that the network has accepted it. Poll this with the id from header.id — your own invoiceNo will not work here.

GET/api/public/v1/outbound/{invoiceId}

Response
GET /api/public/v1/outbound/7c9f1a44-55e2-4a1d-9d0b-2f0e8c6b31aa
X-Api-Key: tvk_live_a1b2c3d4_<secret>

{
  "success": true,
  "invoiceNo": "INV-2026-00125",
  "status": "Delivered",
  "cpAspStatus": "Accepted",
  "ftaStatus": null,
  "submittedAt": "2026-03-14T09:12:44Z"
}

null does not mean accepted

cpAspStatus is the buyer's Access Point's verdict and ftaStatus is the tax authority's. Until each arrives the field is null, which means not yet known. Treating null as a clean result is the single most common integration mistake.

Receiving what suppliers send you

Documents that arrive for you over the network are pushed to a URL you provide. You do not poll for them.

The webhook is a pointer, not the document

The call tells you which document changed and how. To get the document itself, call the detail endpoint with the purchaseDocumentId from the event, and store that response as the document.

What to set up

  1. 1.An HTTPS endpoint of your own that accepts a POST and answers 2xx. Plain http is refused.
  2. 2.In the portal: Company Information, then API credentials, then the Purchase invoice webhook card. Enter the delivery URL and enable delivery.
  3. 3.For any ERP other than Business Central, choose Other ERP and generate a webhook secret. It is shown once.
  4. 4.Press Send test event to check it, and read the Delivery log for the outcome of every call, real or test.

The events

FieldNotes
purchase_document.receivedA document has arrived and been stored.
purchase_document.status_changedA receipt, Access Point or tax-authority verdict changed. The event carries all the statuses as they stand, so overwrite yours with it rather than merging.
webhook.testSomeone pressed Send test event. Acknowledge it and ignore it.
Delivery
POST /your/webhook/endpoint
X-TV-Event: purchase_document.received
X-TV-Timestamp: 1773480764
X-TV-Signature: v1=4f1c0b7e…

{
  "event": "purchase_document.received",
  "purchaseDocumentId": "b17d3e90-6a21-4c88-9f3e-77aa1c2d5e04",
  "invoiceNo": "SUP-9921",
  "supplierName": "Emirates Stationery LLC",
  "receivedAt": "2026-03-14T09:14:02Z"
}

Checking a call really came from the portal

Signed calls carry X-TV-Timestamp and X-TV-Signature. Build the string {timestamp}.{raw body}, take HMAC-SHA256 of it with your webhook secret, write it as lowercase hex, and compare it in constant time with the value after v1=. Reject anything more than 300 seconds old, which stops a captured call being replayed.

verify.ts
import crypto from "node:crypto";

export function verify(rawBody: string, headers: Headers, secret: string) {
  const timestamp = headers.get("X-TV-Timestamp") ?? "";
  const signature = (headers.get("X-TV-Signature") ?? "").replace(/^v1=/, "");

  // Outside five minutes, treat it as a replay of a captured call.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signature, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Verify the bytes you received

Sign the raw body, not a re-serialised copy of it. Re-serialising changes key order and spacing, and makes a genuine call look forged.

Answering, and retries

  • Answer 2xx as soon as you have the event and do the work afterwards. A slow endpoint holds up delivery.
  • On 5xx, 429 or no connection the portal retries: three attempts in all, roughly five then ten seconds apart.
  • Any other 4xx is read as a refusal and is not retried.
  • After the last attempt nothing is sent again. The failure stays in the Delivery log, and the document is stored in the portal either way.
  • The same event can reach you more than once. Key on purchaseDocumentId and make your handling safe to repeat.

Fetching the document

One call returns everything: the header, every line, allowances and charges, attachments and the original UBL exactly as it was delivered. Pass includeXml=false or includeAttachmentContent=false to leave the heavy parts out.

GET/api/public/v1/purchase-documents/{id}

Request
GET /api/public/v1/purchase-documents/b17d3e90-6a21-4c88-9f3e-77aa1c2d5e04?includeXml=false
X-Api-Key: tvk_live_a1b2c3d4_<secret>

A 404 means the id does not exist — or belongs to a company that is not the one your key represents.

Field reference

The fields every document carries. The full line-item and attachment tables, and the complete code lists, are in the integration PDF — ask Tech Ventures for the current version.

FieldTypeRequiredNotes
invoiceNostringYesYour own number. Also the idempotency key — send it twice and you get the first document back.
documentTypestringYes380 invoice · 480 out of scope · 381 credit note · 81 credit note for goods or services.
transactionTypestringYesEight characters, one per flag. 00000000 for a standard taxable sale.
invoiceDatestringYesMM/DD/YYYY.
dueDatestring—MM/DD/YYYY.
currencystringYesISO 4217. AED for a domestic sale.
buyerobjectYesname, taxId, countryCode, city and addressLine1 at minimum; taxId is required for a taxable sale.
linesarrayYesAt least one. lineNo, description, quantity, unitPrice, vatCategory, vatRate.
allowanceChargesarray—Discounts and charges at document or line level.
attachmentsarray—base64, 512 KB each, pdf · xml · jpeg · png · csv · ods · xlsx only.
referenceobjectConditionalThe original invoice a credit note refers to. Required for 381 and 81.

Transaction type

Eight characters, one position per flag. Standard is all zeros; set the position that applies.

FieldNotes
Position 1Reserved — 0.
Position 21 for a self-billed invoice.
Position 31 for a summary invoice.
Position 41 for a disclosed agent sale.
Position 51 for an export.
Positions 6–8Reserved — 0.

Getting a key, and getting help

API keys are generated by a company's own administrator from Company Information in the portal. For the production host, the full field reference, or anything the guide above does not answer, talk to us.

Get ready for UAE e-invoicing today

Accounting, VAT and PINT AE e-invoicing in one place, from a provider accredited by the UAE. Start free — no card needed.

UAE accredited ASPPeppol certified Access PointISO/IEC 27001 certifiedISO 22301 certified