Documents API (v2 / full UBL)¶
The Documents API is Invora's full-power invoicing surface. A single DocumentsService at /api/v2/documents creates, validates, freezes, and submits all 65 UBL 2.1 document types — invoices, credit/debit notes, orders, despatch advices, statements, and more — with field-by-field control over the UBL content.
Documents vs. the Simple surface¶
| Documents (this guide) | Simple (v1) | |
|---|---|---|
| Path | /api/v2/documents |
/api/v1/simple/... |
| Input | Raw UBL 2.1, every field under your control | Minimal business fields; the platform computes UBL |
| Coverage | All 65 UBL document types | Invoice, credit note, debit note |
| Lifecycle | editState: Draft → Validated → Frozen |
2 states (draft → frozen) |
| Best for | Power users, full UBL, the order-to-cash chain | Most integrations |
If you only need to bill customers, start with Simple invoicing. Use the Documents API when you need raw UBL or document types beyond invoices and notes.
Base URLs & auth¶
| Environment | Base URL |
|---|---|
| Production | https://gateway.invora.app |
| Staging | https://stg-gateway.invora.app |
Every call carries a bearer token. Obtain one with the client-credentials grant and export it as $TOKEN:
TOKEN=$(curl -s -X POST https://auth.invora.app/oauth/v2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET" \
--data-urlencode "scope=openid urn:zitadel:iam:org:project:id:372376660185448530:aud urn:zitadel:iam:user:resourceowner" \
| jq -r '.access_token')
All bodies are camelCase JSON. See gRPC & JSON transcoding for field, date, decimal, and enum conventions, and Authentication for tokens and scopes.
Document types¶
Every type is created and managed through the one DocumentsService. The categories are a conceptual grouping, not separate services:
| Category | Types |
|---|---|
| Financial | Invoice, credit note, debit note, self-billed invoice, self-billed credit note, freight invoice |
| Commercial | Order (purchase order), quotation, order response |
| Operational | Despatch advice (delivery note), receipt advice, reminder, statement |
All content is UBL 2.1, so you stay interoperable with any system that speaks UBL XML. Create/update accept either typed UBL content or raw UBL XML; responses always carry both.
Lifecycle¶
A document's editability is tracked by a single editState. Regulatory submission is tracked separately, per regulation, inside the document's regulation metadata.
editState |
Editable? | Meaning |
|---|---|---|
EDIT_STATE_DRAFT |
Yes | Work in progress. Editable and deletable. |
EDIT_STATE_VALIDATED |
Yes | Passed validation; still editable. |
EDIT_STATE_FROZEN |
No | Locked permanently. The regulation pipeline runs (signing, QR, submission). |
flowchart LR
D["Create<br/>EDIT_STATE_DRAFT"] --> U["Update<br/>(draft only)"]
U --> V["Validate<br/>EDIT_STATE_VALIDATED"]
V --> F["Freeze<br/>EDIT_STATE_FROZEN<br/>(regulation pipeline)"]
D -- "freezeImmediately: true" --> F
F --> S["Send / Share"]
Key rules:
- Only drafts can be edited or deleted. Once frozen, content is locked permanently.
- Freezing is the critical transition — that is when signing, QR generation, and authority submission happen.
- There is no edit or cancel of a frozen document. To correct one, issue a credit note (lower the amount) or debit note (raise it), linked via the UBL
BillingReference. - Every mutation needs the current
concurrencyStampfrom your last read (optimistic concurrency — see Entity versioning).
Core operations¶
| RPC | Method & path | Scope |
|---|---|---|
| Create | POST /api/v2/documents |
Invora.Documents.v2.Modify.Create |
| Get | GET /api/v2/documents/{key} |
Invora.Documents.v2.Get |
| Update | PUT /api/v2/documents/{key} |
Invora.Documents.v2.Modify.Update |
| Delete | POST /api/v2/documents/delete |
Invora.Documents.v2.Modify.Delete |
| Validate | POST /api/v2/documents/{key}/validate |
Invora.Documents.v2.Validate |
| Freeze | POST /api/v2/documents/{key}/freeze |
Invora.Documents.v2.Modify.Freeze |
| Send | POST /api/v2/documents/{key}/send |
Invora.Documents.v2.Lifecycle.Send |
| Clone | POST /api/v2/documents/{sourceKey}/clone |
Invora.Documents.v2.Modify.Clone |
| List | POST /api/v2/documents/list |
Invora.Documents.v2.List |
| Calculate | POST /api/v2/documents/calculate |
Invora.Documents.v2.Calculate |
Bulk (/api/v2/documents/bulk-create, bulk-freeze, bulk-update, bulk-delete) and bidirectional gRPC streaming (StreamCreate, StreamFreeze, StreamUpdate, ListStream, GetStream) variants exist for high-volume pipelines.
Create a document¶
Provide the UBL content under changes. Set freezeImmediately: true to create, validate, freeze, and submit in one call; set autoCalculate: true to have the platform fill computed totals from a partial body. Optionally pin a branchId.
curl -X POST https://gateway.invora.app/api/v2/documents \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"freezeImmediately": false,
"autoCalculate": true,
"changes": {
"content": {
"invoice": {
"id": {"value": "INV-001"},
"issueDate": {"value": "2026-04-29"},
"invoiceTypeCode": {"value": "388"},
"documentCurrencyCode": {"value": "SAR"},
"invoiceLine": [{
"id": {"value": "1"},
"invoicedQuantity": {"value": "10.00", "unitCode": {"value": "EA"}},
"lineExtensionAmount": {"value": "1000.00", "currencyId": {"value": "SAR"}},
"item": {"name": {"value": "Widget"}},
"price": {"priceAmount": {"value": "100.00", "currencyId": {"value": "SAR"}}}
}]
}
}
}
}'
{
"details": {
"key": "doc_01J7...",
"editState": "EDIT_STATE_DRAFT",
"concurrencyStamp": "a1b2c3d4",
"content": { "invoice": { "id": { "value": "INV-001" } } }
}
}
Get a document¶
curl -X GET https://gateway.invora.app/api/v2/documents/doc_01J7... \
-H "Authorization: Bearer $TOKEN"
Pass a mask query parameter to return only the fields you need — see Field masks.
Update a draft¶
PUT with the concurrencyStamp from your last read and a mask naming the fields you are changing:
curl -X PUT https://gateway.invora.app/api/v2/documents/doc_01J7... \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"concurrencyStamp": "a1b2c3d4",
"mask": "content.invoice.documentCurrencyCode",
"changes": { "content": { "invoice": { "documentCurrencyCode": {"value": "SAR"} } } }
}'
Validate¶
Check a persisted document against business rules and, optionally, a regulation validation profile (profileId). The response reports isValid plus any errors.
curl -X POST https://gateway.invora.app/api/v2/documents/doc_01J7.../validate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "profileId": "zatca:fatoorah:2.0" }'
{
"isValid": false,
"errors": [
{ "field": "accountingSupplierParty.party.partyTaxScheme", "message": "VAT registration number is required" }
]
}
To validate raw content without saving it first, use POST /api/v2/documents/validate (ValidateContent):
curl -X POST https://gateway.invora.app/api/v2/documents/validate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"profileId": "zatca:fatoorah:2.0",
"content": {
"invoice": {
"id": {"value": "INV-001"},
"invoiceTypeCode": {"value": "388"},
"documentCurrencyCode": {"value": "SAR"}
}
}
}'
{
"isValid": false,
"errors": [
{ "field": "accountingSupplierParty.party.partyTaxScheme", "message": "VAT registration number is required" }
]
}
POST /api/v2/documents/{key}/fix auto-corrects common issues and returns the applied fixes:
curl -X POST https://gateway.invora.app/api/v2/documents/doc_01J7.../fix \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "concurrencyStamp": "a1b2c3d4" }'
{
"fixedContent": { "invoice": { "id": { "value": "INV-001" } } },
"appliedFixes": [
{ "field": "invoiceLine[0].lineExtensionAmount.currencyId", "fixDescription": "Set to match documentCurrencyCode" }
]
}
Freeze¶
Locks the content, sets editState to EDIT_STATE_FROZEN, and runs the regulation pipeline (signing, QR, submission):
curl -X POST https://gateway.invora.app/api/v2/documents/doc_01J7.../freeze \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "concurrencyStamp": "a1b2c3d4" }'
{
"details": {
"key": "doc_01J7...",
"editState": "EDIT_STATE_FROZEN",
"concurrencyStamp": "e5f6a7b8"
}
}
Send¶
Records that the frozen document was delivered to the recipient. POST /api/v2/documents/{key}/share additionally emails the recipient with the rendered PDF attached:
curl -X POST https://gateway.invora.app/api/v2/documents/doc_01J7.../share \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"recipients": [
{ "channel": "SHARE_CHANNEL_EMAIL", "email": "buyer@example.com" }
]
}'
{
"results": [
{ "recipient": { "channel": "SHARE_CHANNEL_EMAIL", "email": "buyer@example.com" }, "success": true }
]
}
Regulation support¶
Invora uses a per-country plugin architecture. Each regulation has a stable string id and operates independently; one document can have several active at once.
Regulation id |
Country | Submission model |
|---|---|---|
zatca |
Saudi Arabia | Pre-clearance (B2B, blocking) / post-clearance reporting (B2C) |
egy-eta |
Egypt | Real-time reporting |
peppol-be, peppol-de, peppol-sg, … |
Per country | Network delivery |
Each regulation is per country — there is no single "Peppol", but peppol-be, peppol-de, peppol-sg, and so on. Discover what is available with ListRegulations.
Submission follows one of six globally distinct models (SubmissionModel): PRE_CLEARANCE, POST_CLEARANCE, REAL_TIME_REPORTING, NETWORK_DELIVERY, DELEGATED_CLEARANCE, POST_AUDIT.
Enabling and configuring regulations per tenant/branch is an admin operation handled by the Invora team. The endpoints below are read/operational and available to tenant integrators.
Regulation & artifact operations¶
| RPC | Method & path |
|---|---|
| ListRegulations | POST /api/v2/regulations/list |
| GetRegulation | GET /api/v2/regulations/{regulationId} |
| GetRegulationStatus | GET /api/v2/regulations/{regulationId}/status |
| ListDocumentRegulations | GET /api/v2/regulations/documents/{documentKey} |
| GetArtifact | GET /api/v2/regulations/{regulationId}/documents/{documentKey}/artifact |
| ListDocumentArtifacts | GET /api/v2/regulations/documents/{documentKey}/artifacts |
| RetrySubmission | POST /api/v2/regulations/{regulationId}/submissions/{documentKey}/retry |
Artifact bytes are never inline on the document — only a hash and status. Download the signed XML, QR code, or clearance response with GetArtifact (optionally passing artifactId, e.g. signed-xml or qr-code):
curl -X GET "https://gateway.invora.app/api/v2/regulations/zatca/documents/doc_01J7.../artifact?artifactId=signed-xml" \
-H "Authorization: Bearer $TOKEN"
{
"artifact": "PD94bWwgdmVyc2lvbj0i...",
"contentType": "application/xml",
"artifactHash": "9f2c1a44e5f6a7b8..."
}
artifact is base64-encoded bytes. See ZATCA integration for the Saudi onboarding and submission flow.
Branches¶
A tenant can issue documents from multiple branches (physical locations or "doing business as" trade names). Each branch has its own party identity, regulation credentials (e.g. a separate ZATCA CSID per branch, an Egypt ETA branch ID, a Peppol GLN), and document numbering. Documents reference a branchId; those that omit it use the primary branch.
| RPC | Method & path | Scope |
|---|---|---|
| List | POST /api/v2/branches/list |
Invora.Branches.v2.List |
| Get | GET /api/v2/branches/{key} |
Invora.Branches.v2.Get |
| Create | POST /api/v2/branches |
Invora.Branches.v2.Modify.Create |
| Update | PUT /api/v2/branches/{key} |
Invora.Branches.v2.Modify.Update |
| Delete | POST /api/v2/branches/delete |
Invora.Branches.v2.Modify.Delete |
| SetPrimary | POST /api/v2/branches/{key}/set-primary |
Invora.Branches.v2.Modify.SetPrimary |
curl -X POST https://gateway.invora.app/api/v2/branches \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"changes": {
"name": "Riyadh Head Office",
"documentPrefix": "RUH-"
}
}'
The first branch created becomes the primary; the primary branch cannot be deleted.
The order-to-cash chain¶
The document types together support the full business flow. Documents link through billing references and order references; call GetRelatedDocuments (GET /api/v2/documents/{key}/related) to navigate them:
curl -X GET https://gateway.invora.app/api/v2/documents/doc_01J7.../related \
-H "Authorization: Bearer $TOKEN"
{
"items": [
{ "key": "doc_01J8...", "documentType": "DOCUMENT_TYPE_CREDIT_NOTE", "relationship": "CORRECTION_OF" },
{ "key": "doc_01J6...", "documentType": "DOCUMENT_TYPE_ORDER", "relationship": "REFERENCED_BY" }
]
}
flowchart TD
Q[Quotation] --> O[Order]
O --> OR[Order response]
OR --> DA[Despatch advice]
DA --> RA[Receipt advice]
RA --> INV[Invoice]
INV --> CN[Credit / debit note]
CN --> ST[Statement]
Supporting tenant services¶
| Service | Base path | Purpose |
|---|---|---|
| Parties | /api/v2/parties |
Customer/supplier address book referenced by partyKey |
| Settings | /api/v2/settings |
Self-party identity, currency, prefixes, branding |
/api/v2/pdf |
Branded PDF generation (/generate, /preview, /bulk-generate) |
|
| Code lists / items | /api/v2/code-lists, /api/v2/code-items |
Reference data (unit codes, tax categories) |
| Exchange rates | /api/v2/exchange-rates |
Date-based FX lookup |
Related¶
- Simple invoicing — the ergonomic invoice/credit/debit surface.
- Quickstart — registration to a frozen, submitted invoice.
- gRPC & JSON transcoding — UBL field, date, decimal, enum conventions.
- List & filtering — the list filter/sort/pagination model.
- Field masks — partial reads and updates.
- Entity versioning — optimistic concurrency.
- ZATCA integration — onboarding and submission.
- Error handling — status codes and error details.