Invoices
The Invoices API returns the same invoices shown in the Billing tab of the admin platform, so you can pull them into your own accounting or AP system, forward them, or archive them without signing in.
All three invoice endpoints are read-only.
Permissions
Invoice access requires the billing_report permission. An API key carries the permissions of the user it belongs to, so a key created by a user without billing access cannot read invoices.
Base Endpoint Group
All invoice endpoints live under:
/v1/payments/invoices
Amounts Are in Minor Units
Every monetary field ends in _cents and is an integer in the smallest unit of the invoice's currency — never a decimal amount. Always read it together with the currency field on the same invoice.
For two-decimal currencies, which is what Hatz bills in today, divide by 100:
currency |
total_cents |
Displays as |
|---|---|---|
usd |
103500 |
$1,035.00 |
eur |
103500 |
€1,035.00 |
Dividing by 100 is not universally correct. Zero-decimal currencies such as jpy and krw have no minor unit, so the integer is already the whole amount — 103500 in jpy is ¥103,500, not ¥1,035. Three-decimal currencies such as bhd divide by 1000. If you may receive currencies beyond USD and EUR, take the exponent from a currency table rather than hardcoding 100; Stripe publishes the zero-decimal list in its currency documentation.
The _cents suffix is a reminder that the value is not a decimal amount — it is not a promise that the divisor is always 100.
Endpoints
List Invoices
GET /v1/payments/invoices
Returns your invoices, newest first.
Query parameters:
start,end: whole UTC dates (YYYY-MM-DD). Both optional.limit: 1–100, default 25starting_after: invoice ID to page afterentity_id: which of your organizations to read — required only if you hold billing access to more than one
curl -H "X-API-Key: $HATZ_API_KEY" \
"https://ai.hatz.ai/v1/payments/invoices?start=2026-07-01&end=2026-09-30&limit=100"
{
"invoices": [
{
"id": "in_1TvmbzLB3VQuvRiYUrkb7C5H",
"number": "HSLNEQJJ-0005",
"status": "paid",
"created": "2026-07-21T22:58:07Z",
"period_start": "2026-06-21T22:57:58Z",
"period_end": "2026-07-21T22:57:58Z",
"currency": "usd",
"total_cents": 103500,
"amount_due_cents": 103500,
"amount_paid_cents": 103500,
"hosted_invoice_url": "https://invoice.stripe.com/i/...",
"invoice_pdf": "https://pay.stripe.com/invoice/.../pdf"
}
],
"has_more": false,
"next_cursor": null
}
Get Invoice PDF Link
GET /v1/payments/invoices/{invoice_id}/pdf
Returns the download link for one invoice's PDF.
curl -H "X-API-Key: $HATZ_API_KEY" \
"https://ai.hatz.ai/v1/payments/invoices/in_1TvmbzLB3VQuvRiYUrkb7C5H/pdf"
{
"id": "in_1TvmbzLB3VQuvRiYUrkb7C5H",
"invoice_pdf": "https://pay.stripe.com/invoice/.../pdf",
"hosted_invoice_url": "https://invoice.stripe.com/i/..."
}
The link points at Stripe and carries its own access token, so you download the file directly from Stripe rather than through the Hatz API:
curl -L -o invoice.pdf "$INVOICE_PDF_URL"
The URL is not a stable identifier. Requests for the same invoice at different times return different invoice_pdf values — the signed token embeds when it was issued. Two calls in quick succession usually match, but do not rely on it: never use this URL as a cache key, a deduplication key, or a way to tell two invoices apart. The invoice id is the durable reference; re-request the link when you need the file. The same applies to hosted_invoice_url, and to both fields on the list endpoint.
Stripe controls how long a given link stays valid, so if you store one, be ready for it to stop working and to re-request it.
Because the token travels in the URL, anyone holding it can fetch that invoice without a Hatz API key — so keep these links out of logs, analytics, and anywhere you would not paste the invoice itself.
Draft invoices have no PDF until they are finalized; requesting one returns 404. An invoice belonging to another organization also returns 404.
Get Invoice Tenant Breakdown
GET /v1/payments/invoices/{invoice_id}/breakdown
Splits one invoice's total across the tenants it charges for. Use this to re-bill: the list endpoint tells you what you owe, this tells you who incurred it.
curl -H "X-API-Key: $HATZ_API_KEY" \
"https://ai.hatz.ai/v1/payments/invoices/in_1TvmbzLB3VQuvRiYUrkb7C5H/breakdown"
{
"invoice_id": "in_1TvmbzLB3VQuvRiYUrkb7C5H",
"currency": "usd",
"total_cents": 103500,
"unallocated_cents": 45000,
"tenants": [
{
"tenant_entity_id": "3f7c1e28-5a94-4b60-9d31-8c2ea6f04b17",
"tenant_name": "Acme Corp",
"line_count": 2,
"amount_cents": 58500
}
]
}
Each invoice line is traced through its subscription item to the tenant holding that license, and amounts are summed per tenant. Tenants are ordered by amount, largest first.
The split always reconciles:
total_cents == sum(tenants[].amount_cents) + unallocated_cents
unallocated_cents is whatever the total leaves over after tenant-attributable amounts are claimed. It absorbs charges that belong to no single tenant:
- one-off items with no subscription behind them
- subscription items whose tenant could not be resolved
- invoice-level adjustments — overall discounts, applied credits, customer balance, tax
It is derived from the total rather than by adding up unattributed lines, so an invoice-level discount that never appears on any line still lands there instead of quietly disappearing. That also means it can be negative when a credit exceeds the unattributed charges.
An invoice whose lines are all one-off charges returns an empty tenants array with the full total unallocated — that is a correct answer, not a lookup failure.
Filtering by Period
start and end filter on the date an invoice was issued (its created date), not on the billing period it covers. An invoice issued on 1 October for September usage is matched by start=2026-10-01, not by end=2026-09-30.
Both bounds are whole UTC days, and end includes that entire day. end=2026-09-30 matches an invoice issued at 22:58 UTC on 30 September.
To select by the period covered instead, request a wider issue-date range and filter the results on period_start and period_end.
Pagination
When has_more is true, pass next_cursor as starting_after to fetch the next page. Repeat until has_more is false.
Paging is cursor-based rather than offset-based, so an invoice created while you are paging cannot shift rows onto a page you have already read. It is not a frozen snapshot, though: invoices are ordered newest first, so one created mid-pagination sorts ahead of your cursor and simply will not appear in any later page.
For an export that must be complete, pin the range before you start — set end to a day that has already finished — or re-run from the first page.
# first page
curl -H "X-API-Key: $HATZ_API_KEY" \
"https://ai.hatz.ai/v1/payments/invoices?limit=100"
# next page
curl -H "X-API-Key: $HATZ_API_KEY" \
"https://ai.hatz.ai/v1/payments/invoices?limit=100&starting_after=in_1TvmbzLB3VQuvRiYUrkb7C5H"
Multiple Organizations
If you hold billing access to more than one organization, the API will not guess which one you mean. The request returns 409 and names your options, each with the id to pass:
{
"error": {
"type": "HTTPException",
"message": "You have billing access to more than one account: Acme MSP (tenant_OrrJZ5pWAKQkDTzz1OBXEnjWM7); Globex MSP (tenant_9dKpQm2XvBnLtWzR7YsHcF4jTa). Pass `entity_id` with one of these ids."
}
}
Each entry is name (id), separated by a semicolon and a space, ordered by name. Retry with entity_id set to the id in parentheses for the organization you want — you do not need to look it up anywhere else. Most callers hold access to a single organization and never see this.
Errors
| Status | Meaning |
|---|---|
400 |
entity_id is not a recognizable id — not a UUID, tenant external id, or tenant_… HashID |
401 |
Missing or invalid API key |
403 |
Your user lacks the billing_report permission, or the account is billed by an external distributor |
404 |
No billing account for this organization, or no such invoice for it |
409 |
You have billing access to several organizations — pass entity_id |
422 |
Invalid parameters, for example start after end or a limit outside 1–100 |
503 |
Billing account or permissions could not be verified — safe to retry |
A 404 is returned both for an invoice that does not exist and for one belonging to another organization. The two are deliberately indistinguishable so the endpoint cannot be used to discover other organizations' invoice IDs.
400 and 404 say different things about entity_id: 400 means the value is not a well-formed id at all, so fix how you are producing it. 404 means the id is well-formed but is not an organization you have billing access to — and, like invoice ids, it does not distinguish "no such organization" from "not yours".