Invoice validation
Every invoice you push is validated against the UAE PINT-AE rules, converted to UBL XML and delivered over Peppol. POST /oauth/invoices/validate runs that same validation without storing or sending anything, so you can fix your data before an invoice exists. This page documents what is checked, which request field ends up where in the XML, and how to read a failure.
How it fits the send flow
POST /oauth/invoices stores your invoice and queues it; the validate → XML → deliver steps then run asynchronously. A rejection therefore surfaces as a status on the invoice, not as an error on your original request. Validating first turns that asynchronous failure into a synchronous answer.
The dry run uses the identical rule engine and the identical XML builder as the send, so a payload that passes here passes there. Nothing is persisted and no invoice number is reserved — validate the same payload as often as you like.
The response is always HTTP 200, whether or not the data is valid. data.is_valid carries the verdict.
Request and response
The body is exactly the body of Push invoice — the same schema, the same conditional rules.
POST /oauth/invoices/validate
Authorization: Bearer <access_token>
Content-Type: application/json
{
"entity_id": "64a1b2c3d4e5f6a7b8c9d0e1",
"invoice_number": "INV-1001",
"date": "2026-08-12",
"due_date": "2026-08-14",
"currency": "AED",
"payment_means_code": "30",
"payment_account_id": "AE070331234567890123456",
"seller": {
"registration_type": "EID",
"issuing_authority": "ICP"
},
"buyer": {
"name": "Buyer LLC",
"trn": "104239483100003",
"country": "AE",
"address_line1": "Sheikh Zayed Road 12",
"city": "Dubai",
"region": "Dubai",
"legal_registration_id": "CN-1234567",
"legal_registration_type": "TL",
"authority_name": "Department of Economic Development - Dubai"
},
"line_items": [
{
"description": "Mercedes Benz",
"item_name": "Mercedes",
"quantity": 1,
"unit_amount": 300000,
"tax_amount": 15000,
"line_amount": 300000,
"tax_rate": 5,
"tax_category": "S"
}
]
}A payload with nothing wrong with it:
{
"message": "Invoice data is valid and ready to send",
"data": {
"is_valid": true,
"entity_id": "64a1b2c3d4e5f6a7b8c9d0e1",
"invoice_number": "INV-1001",
"total_errors": 0,
"errors": [],
"warnings": []
}
}The same payload with a 3-digit buyer TRN and a zero-VAT line. Each entry names the section it came from, so you can map it back to the part of your payload that needs fixing:
{
"message": "Invoice data has validation errors",
"data": {
"is_valid": false,
"entity_id": "64a1b2c3d4e5f6a7b8c9d0e1",
"invoice_number": "INV-1001",
"total_errors": 3,
"errors": [
{ "message": "Contact TaxNumber (TRN) must be 15 characters long.", "section": "contact_trn" },
{ "message": "Total tax amount cannot be zero.", "section": "tax" },
{ "message": "Tax amount cannot be zero.", "section": "invoice_items" }
],
"warnings": []
}
}data.warnings is separate on purpose: it lists things that are not rule failures but would still make the send fail. Today that means a duplicate invoice number for the entity, which Push invoice rejects with 409. A payload can be is_valid: true and still carry a warning.
Error sections
Every error carries one of these section names.
| section | What it covers |
|---|---|
endpoint_ids | The entity has no Peppol registry code — it is not registered for sending yet. |
contact_trn | Buyer identity: TRN missing or not 15 characters, buyer name missing, address country missing or not a supported country. |
invoice_header | Document level: invoice number, issue date format, currency code, due date format, document identifier. |
seller_party | Seller identity, address, TRN and legal registration — including ibr-173-ae. |
buyer_party | Buyer identity, address and legal registration — including the BTUAE-16 code check. |
tax | Document VAT: total tax or taxable amount is zero, currency mismatch, percentage out of range. |
legal_monetary | Document totals: line extension, tax exclusive/inclusive and payable amounts. |
invoice_items | Per line: zero tax, zero line amount, zero price, missing account code, missing description. |
Seller and buyer registration type (BTUAE-15 / BTUAE-16)
A UAE seller identified by the "0235" scheme must qualify its legal registration identifier with a registration type. ibr-173-ae is fatal: the gateway rejects the whole document when the type is missing or outside this list, so the validator reports it as an error rather than letting the send fail later. The buyer's BTUAE-16 uses the same four codes.
| Code | Meaning | Companion field | Notes |
|---|---|---|---|
TL | Commercial / Trade license | issuing_authority — required | The default when you send no seller block and the entity has a trade licence number. |
EID | Emirates ID | issuing_authority — optional | Emitted as @schemeAgencyName when supplied. |
PAS | Passport | passport_country — required | The passport's issuing country becomes @schemeAgencyName. |
CD | Cabinet decision | issuing_authority — optional | Emitted as @schemeAgencyName when supplied. |
The code is emitted as @schemeAgencyID on PartyLegalEntity/cbc:CompanyID, and the companion field as @schemeAgencyName. Lower case is accepted and normalised; anything outside the list is rejected with 400 before validation even runs.
You can set this once instead of per invoice. Register legal_registration_type (and issuing_authority or passport_country) on the entity via POST/PATCH /oauth/entities and every invoice for that entity carries it — the per-invoice seller block then only needs sending when one invoice must differ, and it overrides the entity for that invoice alone.
With neither the entity nor the payload supplying a type, an entity that has a registration number on file falls back to TL; an entity with no number at all emits its registry code and no @schemeAgencyID, which fails ibr-173-ae for an AE seller — so register one of the two.
VAT category and zero-tax lines (IBT-151)
Set line_items[].tax_category per line. It is the line's VAT treatment, and a zero tax amount is valid on every category except S — a zero-rated, exempt, out-of-scope, reverse-charge or margin line charges no VAT, and the rules make a non-zero amount on those categories fatal. Categories may be mixed on one invoice: each one is delivered as its own cac:TaxSubtotal.
| Code | Meaning | Rules |
|---|---|---|
S | Standard rated | Must charge VAT at 5% (ibr-190-ae). A zero tax_amount on an “S” line is rejected — state the line’s real category instead. |
Z | Zero rated | tax_amount must be 0. No exemption reason. |
E | Exempt | tax_amount must be 0 and tax_exemption_reason is required (ibr-167-ae) — the request is refused without it. |
O | Out of scope of tax | tax_amount must be 0. An invoice whose every line is E or O is delivered as invoice type 480 (ibr-151-ae). |
AE | Reverse charge | tax_amount must be 0 — the buyer accounts for the VAT. Requires buyer.trn, item_type (goods code) and item_standard_id. |
N | Margin scheme | tax_amount must be 0; the rate stays 5% and the VAT is not disclosed. |
Full labels are accepted too ("Zero Rated", "Exempt", "Reverse Charge"), as are the field names tax_category_code, vat_category and category. Anything outside the list is reported against the line rather than silently treated as standard rated.
An exempt line needs tax_exemption_reason (IBT-121); the request is refused without it. These four are the UAE codelist reasons; the list is not closed, so any other code you are required to use is carried through as sent.
| Code | Reason |
|---|---|
DL8.46.1 | Certain financial services |
DL8.46.2 | Residential units — lease or sale |
DL8.46.3 | Bare land |
DL8.46.4 | Local passenger transport |
A reverse-charge line additionally needs item_type — the type of goods, one of DL8.48.8.1 (gold and diamonds), DL8.48.8.2 (electronic devices), DL8.48.3.1 (crude or refined oil), DL8.48.3.2 (natural gas) or DL8.48.3.3 (pure hydrocarbons) — plus item_standard_id(your item code) and the buyer's TRN.
Payment means (IBT-081)
A standard invoice must carry a payment means code (ibr-191-ae), so one is always emitted: 30 credit transfer, 10 cash, 20 cheque, 48 bank card, 49 direct debit. Sending 30 requires payment_account_id (the IBAN), which is emitted as PayeeFinancialAccount/cbc:ID.
Send no payment fields at all and the invoice carries code 1 ("Instrument Not Defined") rather than claiming a credit transfer to an account you never supplied.
Field-to-XML reference
Where each part of your request ends up in the delivered UBL, with the PINT-AE business term it satisfies. The seller party is built from your entity record, not the invoice body — keep its address, TRN and trade licence up to date.
| Field | UBL element | Term / rule | |
|---|---|---|---|
| Document | |||
invoice_number | cbc:ID / cbc:UUID * | IBT-001 | required |
date | cbc:IssueDate | IBT-002 | required |
due_date | cbc:DueDate | IBT-009 | optional |
currency | cbc:DocumentCurrencyCode | IBT-005 | optional |
| Seller party — from your entity record | |||
entity.area / street | cac:PostalAddress/cbc:StreetName | IBT-035 | entity |
entity.city | cac:PostalAddress/cbc:CityName | IBT-037 | entity |
entity.postal_code | cac:PostalAddress/cbc:PostalZone | IBT-038 | entity |
entity.state | cac:PostalAddress/cbc:CountrySubentity | IBT-039 | entity |
entity.trn | cac:PartyTaxScheme/cbc:CompanyID | IBT-031 | entity |
entity.legal_registration_id | cac:PartyLegalEntity/cbc:CompanyID | IBT-030 | entity |
entity.legal_registration_type | CompanyID/@schemeAgencyID | BTUAE-15 | entity |
entity.issuing_authority | CompanyID/@schemeAgencyName | BTUAE-12 | entity |
entity.passport_country | CompanyID/@schemeAgencyName | BTUAE-18 | entity |
| Seller legal registration — per invoice | |||
seller.legal_registration_id | cac:PartyLegalEntity/cbc:CompanyID | IBT-030 | conditional |
seller.registration_type | CompanyID/@schemeAgencyID | BTUAE-15 · ibr-173-ae | conditional |
seller.issuing_authority | CompanyID/@schemeAgencyName | BTUAE-12 | conditional |
seller.passport_country | CompanyID/@schemeAgencyName | BTUAE-18 | conditional |
| Buyer party — from your request | |||
buyer.name | cac:PartyLegalEntity/cbc:RegistrationName | IBT-044 | required |
buyer.trn | cbc:EndpointID + cac:PartyTaxScheme/cbc:CompanyID | IBT-049 / IBT-048 | required |
buyer.address_line1 | cac:PostalAddress/cbc:StreetName | IBT-050 | optional |
buyer.city | cac:PostalAddress/cbc:CityName | IBT-052 | optional |
buyer.region | cac:PostalAddress/cbc:CountrySubentity | IBT-054 | optional |
buyer.country | cac:PostalAddress/cac:Country/cbc:IdentificationCode | IBT-055 | required |
buyer.legal_registration_id | cac:PartyLegalEntity/cbc:CompanyID | IBT-047 | optional |
buyer.legal_registration_type | CompanyID/@schemeAgencyID | BTUAE-16 | optional |
buyer.authority_name | CompanyID/@schemeAgencyName | BTUAE-11 | conditional |
buyer.passport_country_code | CompanyID/@schemeAgencyName | BTUAE-19 | conditional |
| Payment | |||
payment_means_code | cac:PaymentMeans/cbc:PaymentMeansCode | IBT-081 · ibr-191-ae | optional |
payment_account_id | cac:PayeeFinancialAccount/cbc:ID | IBT-084 | conditional |
| Totals and lines | |||
sub_total / total_tax | cac:TaxTotal + cac:LegalMonetaryTotal | IBG-22 / IBG-23 | optional |
line_items[].description | cac:Item/cbc:Description + cbc:Name | IBT-154 / IBT-153 | required |
line_items[].quantity | cbc:InvoicedQuantity | IBT-129 | required |
line_items[].unit_amount | cac:Price/cbc:PriceAmount | IBT-146 | required |
line_items[].tax_amount | cac:ItemPriceExtension/cac:TaxTotal | IBT-117 | required |
line_items[].tax_category | cac:ClassifiedTaxCategory/cbc:ID | IBT-151 | optional |
line_items[].tax_exemption_reason | cbc:TaxExemptionReasonCode | IBT-121 · ibr-167-ae | conditional |
line_items[].item_type | cac:CommodityClassification/cbc:NatureCode | BTUAE-09 · ibr-006-ae | conditional |
line_items[].item_standard_id | cac:StandardItemIdentification/cbc:ID | IBT-157 · ibr-174-ae | conditional |
* The document identifier is a system-generated UUID, stamped once per invoice record and emitted as both cbc:ID and cbc:UUID. Your invoice_number identifies the invoice in this API and must be unique per entity.
What the delivered XML looks like
The parties and payment means produced by the request above:
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="0235">1042394831</cbc:EndpointID>
<cac:PostalAddress>
<cbc:StreetName>Business Bay</cbc:StreetName>
<cbc:CityName>Dubai</cbc:CityName>
<cbc:PostalZone>PO Box 77</cbc:PostalZone>
<cbc:CountrySubentity>DXB</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode>AE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>104239483100003</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>E-Invoice Dev</cbc:RegistrationName>
<cbc:CompanyID schemeAgencyID="EID" schemeAgencyName="ICP">2309778</cbc:CompanyID>
</cac:PartyLegalEntity>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="0235">1042394831</cbc:EndpointID>
<cac:PostalAddress>
<cbc:StreetName>Sheikh Zayed Road 12</cbc:StreetName>
<cbc:CityName>Dubai</cbc:CityName>
<cbc:CountrySubentity>DXB</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode>AE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>104239483100003</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Buyer LLC</cbc:RegistrationName>
<cbc:CompanyID schemeAgencyID="TL"
schemeAgencyName="Department of Economic Development - Dubai">CN-1234567</cbc:CompanyID>
</cac:PartyLegalEntity>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:PaymentMeans>
<cbc:PaymentMeansCode name="Credit transfer">30</cbc:PaymentMeansCode>
<cac:PayeeFinancialAccount>
<cbc:ID>AE070331234567890123456</cbc:ID>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>Known limitations
These fields are accepted by the schema but do not yet reach the delivered XML on this flow. Do not rely on them until they are announced as supported:
invoice_type— every document is emitted asInvoiceTypeCode 380(tax invoice). Credit notes are not supported yet.transaction_type_code— the free-zone / export / deemed supply flags are not emitted;ProfileExecutionIDis always00000000.reference(document note),buyer.phone,buyer.electronic_address.- Line level:
unit(alwaysEA),item_name(the description is used),discount_percent,hs_code,service_code.