Skip to content

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 concurrencyStamp from 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"}}}
          }]
        }
      }
    }
  }'
Response
{
  "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" }'
Response
{
  "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"}
      }
    }
  }'
Response
{
  "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" }'
Response
{
  "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" }'
Response
{
  "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" }
    ]
  }'
Response
{
  "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"
Response
{
  "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"
Response
{
  "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
PDF /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