Skip to content

Quickstart: Your First Invoice in 5 Minutes

This is a guided, copy-paste first run. You will get an access token, create a draft invoice from a couple of line items, freeze it (the platform computes the totals, renders the compliant document, and submits it to the tax authority), and finally read it back to see the QR code and signed artifact.

Every request below runs against Staging — a safe sandbox — so you can follow along without touching production data. Each step shows the request and a realistic response so you always know what success looks like and where the next step's input comes from.

We use the Simple invoicing API (/api/v1/simple/...): you send only the business essentials — buyer, line items, tax rates — and the platform computes every total for you. For the full field-by-field UBL surface, see the Documents API instead.

Prerequisites

  • An Invora account — register at stg-dashboard.invora.app.
  • A machine-to-machine client_id and client_secret from the dashboard.

Set them as shell variables so the snippets below are copy-pasteable:

export CLIENT_ID="your-client-id"
export CLIENT_SECRET="your-client-secret"

1. Get an access token

Exchange your credentials for a token with the OAuth 2.0 client_credentials grant:

TOKEN=$(curl -s -X POST https://stg-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:372376692817133647:aud urn:zitadel:iam:user:resourceowner" \
  | jq -r '.access_token')
Response
{
  "access_token": "iIDF3v0p9X8q2nReferenceTokenOpaqueValue",
  "token_type": "Bearer",
  "expires_in": 43200
}

The access_token is an opaque reference token, not a JWT — pass it through verbatim; do not try to decode it. Every request from here carries Authorization: Bearer $TOKEN. Your token is automatically scoped to your own tenant, so no tenancy header is needed. (Full details in Authentication.)

2. Create a draft invoice

Create an invoice with two standard-rate line items. You send quantities, unit prices, and tax rates — not totals; the platform computes those. Omitting freezeImmediately leaves the invoice as an editable draft.

Monetary values and rates are exact decimals shaped { "units": …, "nanos": … } (both numbers, value = units + nanos / 1,000,000,000); a 15% VAT rate is { "units": 15, "nanos": 0 }. See decimal values.

curl -s -X POST https://stg-gateway.invora.app/api/v1/simple/invoices \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "changes": {
      "issueAt": "2026-06-11T10:30:00Z",
      "supplyDate": { "year": 2026, "month": 6, "day": 11 },
      "currencyCode": "SAR",
      "buyer": {
        "inline": {
          "legalName": "Beta Corp",
          "vatRegistrationNumber": "310122393500003",
          "address": {
            "streetName": "King Fahd Road",
            "buildingNumber": "1234",
            "citySubdivisionName": "Al Olaya",
            "cityName": "Riyadh",
            "postalZone": "12345",
            "countryCode": "SA"
          }
        }
      },
      "lines": [
        {
          "description": "Consulting services",
          "quantity": { "value": { "units": 10, "nanos": 0 }, "unitCode": "HUR" },
          "unitPrice": { "units": 100, "nanos": 0 },
          "tax": { "taxRate": { "units": 15, "nanos": 0 } }
        },
        {
          "description": "Onboarding setup fee",
          "quantity": { "value": { "units": 1, "nanos": 0 }, "unitCode": "EA" },
          "unitPrice": { "units": 500, "nanos": 0 },
          "tax": { "taxRate": { "units": 15, "nanos": 0 } }
        }
      ]
    }
  }'
Response
{
  "details": {
    "key": "siv_01J7ZK8QF3EXAMPLE",
    "frozen": false,
    "concurrencyStamp": "8f2c1a44",
    "calculations": {
      "lineExtensionTotal": { "units": 1500, "nanos": 0 },
      "taxTotal": { "units": 225, "nanos": 0 },
      "payableAmount": { "units": 1725, "nanos": 0 }
    }
  }
}

Two things to keep from this response drive the rest of the tutorial — the server-generated key and the concurrencyStamp. Save them:

export KEY="siv_01J7ZK8QF3EXAMPLE"
export STAMP="8f2c1a44"

The invoice is a draft (frozen: false), so calculations are already computed but nothing has been submitted yet. Because the buyer has a vatRegistrationNumber, this is a B2B invoice that ZATCA will clear when you freeze it.

3. (Optional) Preview the totals with Calculate

Before committing, you can preview the fully computed document — per-line and document totals — without persisting anything. Send the same changes you sent on Create:

curl -s -X POST https://stg-gateway.invora.app/api/v1/simple/invoices:calculate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "changes": {
      "currencyCode": "SAR",
      "lines": [
        {
          "description": "Consulting services",
          "quantity": { "value": { "units": 10, "nanos": 0 }, "unitCode": "HUR" },
          "unitPrice": { "units": 100, "nanos": 0 },
          "tax": { "taxRate": { "units": 15, "nanos": 0 } }
        },
        {
          "description": "Onboarding setup fee",
          "quantity": { "value": { "units": 1, "nanos": 0 }, "unitCode": "EA" },
          "unitPrice": { "units": 500, "nanos": 0 },
          "tax": { "taxRate": { "units": 15, "nanos": 0 } }
        }
      ]
    }
  }'
Response
{
  "details": {
    "calculations": {
      "lineExtensionTotal": { "units": 1500, "nanos": 0 },
      "taxTotal": { "units": 225, "nanos": 0 },
      "payableAmount": { "units": 1725, "nanos": 0 }
    }
  }
}

Calculate never stores anything and needs no key — it is a pure preview. The main flow continues with the draft you created in step 2.

4. Freeze the invoice

Freezing finalizes the draft and runs the regulation pipeline: the platform signs the document, generates the QR code, and submits it to ZATCA. Pass the concurrencyStamp from step 2 so the platform can reject a conflicting edit (concurrencyStamp is the optimistic-concurrency guard — see Entity versioning).

curl -s -X POST https://stg-gateway.invora.app/api/v1/simple/invoices/$KEY/freeze \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{ \"concurrencyStamp\": \"$STAMP\" }"
Response
{
  "details": {
    "key": "siv_01J7ZK8QF3EXAMPLE",
    "frozen": true,
    "concurrencyStamp": "d4e9b077",
    "calculations": {
      "lineExtensionTotal": { "units": 1500, "nanos": 0 },
      "taxTotal": { "units": 225, "nanos": 0 },
      "payableAmount": { "units": 1725, "nanos": 0 }
    },
    "regulation": {
      "regulationId": "zatca",
      "status": "SIMPLE_REGULATION_STATUS_ACCEPTED",
      "qrCode": "AR5T...base64-TLV...==",
      "authorityReferenceIds": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
      "artifacts": [
        { "id": "qr-code", "contentType": "image/png", "sha256": "3a7bd3e2...42dd4f1b" },
        { "id": "signed-xml", "contentType": "application/xml", "sha256": "9f1c04a7...b30e5521" }
      ]
    }
  }
}

The invoice is now frozen: true and immutable. regulation.status SIMPLE_REGULATION_STATUS_ACCEPTED means ZATCA cleared it, and regulation.qrCode plus the two artifacts are ready. To adjust a frozen invoice you never edit it — you issue a credit or debit note (see Simple invoicing).

5. Read the invoice back

Fetch the finalized invoice to confirm the terminal state and inspect the regulation metadata:

curl -s -X GET https://stg-gateway.invora.app/api/v1/simple/invoices/$KEY \
  -H "Authorization: Bearer $TOKEN"
Response
{
  "details": {
    "key": "siv_01J7ZK8QF3EXAMPLE",
    "frozen": true,
    "concurrencyStamp": "d4e9b077",
    "currencyCode": "SAR",
    "calculations": {
      "lineExtensionTotal": { "units": 1500, "nanos": 0 },
      "taxTotal": { "units": 225, "nanos": 0 },
      "payableAmount": { "units": 1725, "nanos": 0 }
    },
    "regulation": {
      "regulationId": "zatca",
      "status": "SIMPLE_REGULATION_STATUS_ACCEPTED",
      "qrCode": "AR5T...base64-TLV...==",
      "authorityReferenceIds": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
      "artifacts": [
        { "id": "qr-code", "contentType": "image/png", "sha256": "3a7bd3e2...42dd4f1b" },
        { "id": "signed-xml", "contentType": "application/xml", "sha256": "9f1c04a7...b30e5521" }
      ]
    }
  }
}

That is a complete, ZATCA-cleared invoice: computed totals, a QR code, and a signed XML artifact — the guaranteed terminal state for this walkthrough. The artifacts list carries identity and integrity metadata only; download the actual bytes (the signed XML or the QR image) by artifact id with the GetArtifact operation, covered in Simple invoicing → Artifacts.

Flow at a glance

flowchart LR
  A[Get access token] --> B["Create invoice<br/>(draft)"]
  B -. optional .-> C["Calculate<br/>(preview totals)"]
  B --> D["Freeze<br/>(ZATCA clearance)"]
  D --> E["Get / verify<br/>(QR + signed XML)"]

What's next

  • All Simple operations — update, delete, list, credit and debit notes, artifact download: Simple invoicing.
  • Environments and base URLs — moving from Staging to Production: Getting started.
  • ZATCA compliance — onboarding, clearance vs. reporting, and simplified (B2C) invoices: ZATCA integration.