Invoices

The Invoices API returns your finalized invoices, so you can pull them into your own accounting or AP system, forward them, or archive them without signing in.

All 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.

Draft Invoices Are Excluded

This API returns only invoices that have been finalized. Drafts are never listed, and requesting one by ID returns 404 from every endpoint.

A draft is a bill Stripe is still assembling. Its amounts can still change, lines can still be added or removed, and it may be revised or discarded before it is ever issued — it has no invoice number and no PDF. Re-billing a tenant from one would charge for an amount that was never owed, and the correction would not reach the customer you had already invoiced.

Only draft is withheld — treat status as an open set. In practice you will see open, paid, uncollectible, and void, and note that void and uncollectible invoices were issued and are still returned. But this API filters on draft specifically rather than allowlisting the others, so if Stripe introduces a new status it will reach you rather than being silently dropped from your export. Accept an unfamiliar non-draft status instead of rejecting it.

Base Endpoint Group

The per-invoice endpoints live under:

/v1/payments/invoices

The monthly tenant breakdown is /v1/payments/tenant-breakdown, and the upcoming-invoice estimate is /v1/payments/upcoming-invoice.

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 finalized invoices, newest first. Drafts are omitted.

Query parameters:

  • start, end: whole UTC dates (YYYY-MM-DD). Both optional.
  • limit: 1–100, default 25
  • starting_after: invoice ID to page after
  • entity_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 /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.

A draft invoice returns 404, as it does everywhere else in this API. An invoice belonging to another organization also returns 404, and so does a finalized invoice whose PDF Stripe has not yet produced.

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",
  "status": "paid",
  "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
    }
  ],
  "lines_truncated": false,
  "lines": [
    {
      "description": "Hatz Business - 30 seats",
      "amount_cents": 46800,
      "quantity": 24,
      "period_start": "2026-06-21T22:57:58Z",
      "period_end": "2026-07-21T22:57:58Z",
      "tenant_entity_id": "3f7c1e28-5a94-4b60-9d31-8c2ea6f04b17",
      "unallocated_reason": null
    },
    {
      "description": "Remaining time on 6 x Hatz Business",
      "amount_cents": 11700,
      "quantity": 6,
      "period_start": "2026-07-05T00:00:00Z",
      "period_end": "2026-07-21T22:57:58Z",
      "tenant_entity_id": "3f7c1e28-5a94-4b60-9d31-8c2ea6f04b17",
      "unallocated_reason": null
    },
    {
      "description": "Credit pack",
      "amount_cents": 45000,
      "quantity": 1,
      "period_start": "2026-07-21T22:57:58Z",
      "period_end": "2026-07-21T22:57:58Z",
      "tenant_entity_id": null,
      "unallocated_reason": "no_subscription_item"
    }
  ]
}

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.

status is returned alongside the split. A void or uncollectible invoice splits across tenants exactly like a live one, so check it before re-billing: the split is real, but the underlying bill is not owed and should not be re-issued to your customer.

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.

When the Invoice Does Not Split by Tenant

tenants covers only what traces to a license. An invoice can be billed as a single consolidated charge, or as one-off items with no subscription behind them, and then nothing traces — tenants comes back empty and unallocated_cents equals total_cents.

lines is what you read in that case. It carries the invoice's lines in Stripe's own order, whether or not each reached a tenant — so the same field answers both the clean split and the one that does not split at all. It is complete only when lines_truncated is false; above the cap you get that same order, truncated to a prefix:

Field Meaning
description The line as it reads on the PDF
amount_cents This line's amount, in minor units. Negative for credits and discounts billed as their own line
quantity Units billed, typically seats. null when the line has none
period_start, period_end The period this line covers, which can differ from the invoice's own period on a proration
tenant_entity_id The tenant it was attributed to, or null
unallocated_reason Why it was not attributed. null when it was

Exactly one of those two is ever set: an attributed line has a tenant_entity_id and a null reason, an unattributed line has a reason and a null tenant. Neither both nor neither.

unallocated_reason has two values, and they mean different things:

  • no_subscription_item — the line is not tied to a subscription item. One-off charges, credits, manual lines and inline tax are the straightforward cases, and for those there genuinely is no tenant to find. Metered usage and overage lines also land here, and those often do name a tenant in their description even though this API cannot resolve it to a tenant_entity_id; read the description before concluding a usage charge is unattributable.
  • unmapped_subscription_item — the line is a subscription charge, but no license on your account holds that subscription item, so no tenant owns it. Usually a license moved or was removed after the invoice was issued. If you see this on a charge you can otherwise account for, tell us rather than reassigning it yourself.

Reconciling by hand: sum amount_cents across lines and you get the invoice's pre-adjustment subtotal, not total_cents. The difference is invoice-level discounts, credits, and tax, which Stripe applies to the invoice rather than to any line. total_cents remains the authoritative figure — always split from it, never from the line sum. (An applied customer balance sits between total_cents and what is actually charged, so it will not close this gap either.)

A credit note does not reduce total_cents. Stripe tracks credit notes separately from the invoice total, so a credit-noted invoice owes less than total_cents and no entry in lines records the difference. Do not treat total_cents as the final amount owed on an invoice you know has been credited.

lines is capped. Check lines_truncated before reconciling line by line: when it is true the invoice carried more lines than the response returned. The tenant amounts and unallocated_cents are computed from every line either way and stay exact — only the listing is shortened.

Get Monthly Tenant Breakdown

GET /v1/payments/tenant-breakdown

Returns what each of your tenants was charged for one calendar month: the subscription invoices issued that month and the usage invoices issued early the next month. Use it to re-bill a month at a time instead of invoice by invoice. Re-bill from a past (final) month: by then both its subscriptions and its usage are in. The current month, which is the default, has no usage in its rows; a month's usage arrives early the next month, so the current month's rows understate what you will be charged.

Query parameters:

  • month: the UTC month as YYYY-MM. Optional; defaults to the current month. A month after the current one returns 422.
  • entity_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/tenant-breakdown?month=2026-09"
{
  "month": "2026-09",
  "status": "final",
  "as_of": "2026-10-27T15:00:00Z",
  "tenant_split_available": true,
  "totals": [
    { "currency": "usd", "total_cents": 64200, "advance_cents": 58500, "arrears_cents": 5700 }
  ],
  "tenants": [
    {
      "tenant_entity_id": "3f7c1e28-5a94-4b60-9d31-8c2ea6f04b17",
      "tenant_name": "Acme Corp",
      "role": "tenant",
      "trial": null,
      "usage_status": "billed",
      "totals": [
        { "currency": "usd", "total_cents": 64200, "advance_cents": 58500, "arrears_cents": 5700 }
      ],
      "previous_month_totals": [
        { "currency": "usd", "total_cents": 58500, "advance_cents": 58500, "arrears_cents": 0 }
      ],
      "lines": [
        {
          "kind": "package",
          "timing": "advance",
          "description": "Hatz Business - 30 seats",
          "package_name": "Hatz Business",
          "quantity": 30,
          "proration": false,
          "amount_cents": 58500,
          "tax_cents": 0,
          "discount_cents": 0,
          "currency": "usd",
          "period_start": "2026-09-01T00:00:00Z",
          "period_end": "2026-10-01T00:00:00Z",
          "invoice_id": "in_1TvmbzLB3VQuvRiYUrkb7C5H"
        },
        {
          "kind": "credit_overage",
          "timing": "arrears",
          "description": "Credit overage",
          "package_name": null,
          "quantity": null,
          "proration": false,
          "amount_cents": 5700,
          "tax_cents": 0,
          "discount_cents": 0,
          "currency": "usd",
          "period_start": "2026-09-01T00:00:00Z",
          "period_end": "2026-10-01T00:00:00Z",
          "invoice_id": "in_1U2kQmLB3VQuvRiY4kWn2DpA"
        }
      ],
      "changes": []
    }
  ],
  "invoices": [
    {
      "id": "in_1TvmbzLB3VQuvRiYUrkb7C5H",
      "number": "ABCD1234-0009",
      "created": "2026-09-01T00:04:11Z",
      "status": "paid",
      "currency": "usd",
      "total_cents": 58500
    },
    {
      "id": "in_1U2kQmLB3VQuvRiY4kWn2DpA",
      "number": "ABCD1234-0010",
      "created": "2026-10-02T03:52:40Z",
      "status": "paid",
      "currency": "usd",
      "total_cents": 5700
    }
  ]
}

A month is what was billed for it. Subscriptions are billed in advance, so a month holds every open or paid subscription invoice whose created date falls in that UTC month. Usage is billed early the next month, so a month also holds every usage invoice issued in the month after it, whatever its lines' periods say: September's usage, invoiced on 2 October, appears in September, even when a line on that invoice starts on 1 October. Each usage invoice belongs, whole, to one month only. void and uncollectible invoices are left out, and so are lines for non-revenue licenses that aren't packages (such as platform access and phone numbers), which are never billed. A non-revenue package's line stays.

Changed in November 2026. Usage invoices used to be filed under the month they were issued in, so each month carried the previous month's usage. Each usage invoice now belongs to the month before it was issued, so from June 2026 on, every month's usage, totals and invoices move one month earlier: the usage invoice issued on 2 October used to appear in October and now appears in September. If you re-billed months under the old rule, compare each month again before re-billing it. A just-ended month now stays so_far until the nightly sync has covered the 2nd of the next month, when its usage is in. If you re-billed by cursor, the usage invoice issued in the first days of the month this change reaches you moves into a month you have already billed.

Each line can be split. One invoice line that covers several tenants is shared between them, so the same line can appear under more than one row, and the shares add back up to the line. Each row is one of:

role Row
tenant A tenant you manage that had a charge or an active trial in the month
internal Charges for your own organization, named "Your organization". At most one
unallocated Charges that could not be attributed to anyone. At most one

A tenant on a trial appears with its trial dates even when it has no charges. trial.status is active while the trial runs, expired when it ended without converting, converted once the tenant became a paying tenant, and canceled when it was canceled before converting.

A seat change nets to whoever changed. When a quantity changes, Stripe credits the unused time on every seat held before and charges the remaining time on every seat held after. The pair's net goes only to the rows whose seats changed. A row with the same seats on both sides gets a credit and a charge that cancel exactly, so a tenant that did not change pays nothing for another tenant's change. When Stripe billed fewer seats than are known on either side, the pair cannot be split this way: its credit is split by seat and its charge goes to unallocated.

status is final once the nightly invoice sync has covered the 2nd of the next month, when the month's subscription and usage invoices are all in, and so_far before that (the current month always is). usage_status says where usage stands on each row:

  • billed: the month billed usage for this row, in its arrears lines. Only in a final month.
  • none: no usage was found for this row. The sync has covered the 2nd of the next month and the month's usage invoices have no arrears line for it, or no usage invoice for it had arrived by then. none is not proof that usage was billed.
  • not_calculated: the row's usage may not be billed yet, in every row of a so_far month. Usage is billed early the next month, so the current month has no arrears lines. A month that just ended can take several usage invoices, issued hours apart through the 2nd of the next month (UTC), so every row stays not_calculated, even one with an arrears line already, until the nightly sync has covered all of the 2nd, usually by about 07:30 UTC on the 3rd. If the sync runs late, rows stay not_calculated longer rather than reading none.

Months before June 2026 predate per-tenant attribution. tenant_split_available is false and every charge is on the unallocated row. Lines for periods before 12 June 2026 stay unallocated in June too.

Totals are per currency. Every totals array holds one entry per currency, and amounts in different currencies are never added together. A row's total_cents is its line amounts after discount_cents, plus tax_cents, and always equals advance_cents + arrears_cents. previous_month_totals is the same row's totals for the month before, or null when it had none.

invoices[].total_cents is Stripe's own total. It includes invoice-level discounts and tax that are not on any line, so it can differ from the sum of the rows. Pay and reconcile against the invoice total; split by the rows.

Credit notes are listed, not subtracted. Each invoice lists its issued credit notes in credit_notes, oldest first, and their sum in credited_cents; the invoice net of credits is total_cents - credited_cents. Each note's total splits into reduced_amount_due_cents (issued before the invoice was paid), refunded_cents to the payment method, credited_to_balance_cents, and outside_stripe_cents. type is pre_payment, post_payment or mixed (issued on a partly paid invoice), or null for a type Stripe adds later. On the rows, a line's credits lists what credited it, before tax and discounts. A credit stays on the row the line went to only when the whole line went to that one row. A credit on a line split across rows, on an unallocated line or on no line, and a note's tax and discounts, appear on the internal row as a line of their own with amount_cents 0; the tax-and-discount entry is negative when the discounts exceed the tax. Rows' totals never subtract credits, so the internal row can appear in a month only to carry these $0 credit lines. Voided credit notes are left out.

changes lists each tenant's subscription changes that this month's invoice bills, which are the changes made in the month before. Each has a type, occurred_at, from_package and to_package. The types are upgrade, downgrade, cancel, trial_started, trial_converted, package_added, package_removed, add_on_added and add_on_removed. On /upcoming-invoice, changes lists the changes made this month.

Get Upcoming Invoice Estimate

GET /v1/payments/upcoming-invoice

Returns your next bill as it stands today, split across your tenants in the same shape as the monthly breakdown. Stripe previews the next invoice of each of your active subscriptions; nothing is created or charged.

Query parameters:

  • entity_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/upcoming-invoice"
{
  "month": "2026-11",
  "status": "estimate",
  "as_of": "2026-10-27T15:00:00Z",
  "tenant_split_available": true,
  "bill_date": "2026-11-01",
  "distributor_billed": false,
  "coupons": [
    {
      "name": "Partner 20%",
      "percent_off": 20.0,
      "amount_off_cents": null,
      "currency": "usd",
      "promotion_code": null,
      "amount_cents": 11700
    }
  ],
  "totals": [
    { "currency": "usd", "total_cents": 46800, "advance_cents": 46800, "arrears_cents": 0 }
  ],
  "tenants": [
    {
      "tenant_entity_id": "3f7c1e28-5a94-4b60-9d31-8c2ea6f04b17",
      "tenant_name": "Acme Corp",
      "role": "tenant",
      "trial": null,
      "usage_status": "not_calculated",
      "totals": [
        { "currency": "usd", "total_cents": 46800, "advance_cents": 46800, "arrears_cents": 0 }
      ],
      "previous_month_totals": null,
      "lines": [
        {
          "kind": "package",
          "timing": "advance",
          "description": "30 × Hatz Business (at $19.50 / month)",
          "package_name": "Hatz Business",
          "quantity": 30,
          "proration": false,
          "amount_cents": 58500,
          "tax_cents": 0,
          "discount_cents": 11700,
          "currency": "usd",
          "period_start": "2026-11-01T00:00:00Z",
          "period_end": "2026-12-01T00:00:00Z",
          "invoice_id": null
        }
      ],
      "changes": []
    }
  ],
  "invoices": []
}

It is an estimate. status is always estimate, invoices is empty, and every line's invoice_id is null. Seat changes, usage and tax can still change the bill before bill_date, the UTC date Stripe issues the next invoice (the earliest one when you have several subscriptions). bill_date is null when nothing is due to bill. month is the calendar month after today.

Lines are split like a billed month. Each line is attributed to the seats held when its period starts, with the same rules and rows as the monthly breakdown, so seats scheduled to leave or arrive by then are already counted. Pending prorations from changes made this cycle appear as their own proration lines.

No usage yet. Usage is billed after the month it covers, so every row's usage_status is not_calculated and there are no arrears lines. previous_month_totals is always null.

coupons lists each discount Stripe will apply, with what it takes off this invoice in amount_cents. A coupon is either a percentage (percent_off) or a fixed amount (amount_off_cents); the other is null.

Accounts billed by a distributor get distributor_billed: true, no tenants and no totals, rather than a 403: your distributor bills you, so there is no Stripe estimate to show.

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 — not until a page comes back empty.

Drafts are removed from a page after it has already been assembled, so a page can hold fewer invoices than limit, occasionally none at all, while has_more is still true. Treating an empty page as the end of the list would cut an export short. next_cursor is present whenever has_more is true, including on a page that returned no invoices.

next_cursor is an opaque token — pass it back verbatim and nothing else. It is Stripe's position in the unfiltered list, so it may name an invoice that never appeared in your invoices array, including a draft that this API will 404 if you request it by ID. Do not store it as "the last invoice I processed", and do not derive your own cursor from the last invoice you received: on a page whose trailing rows were drafts, that cursor would rewind and re-read them forever.

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 (except /upcoming-invoice, which returns distributor_billed: true)
404 No billing account for this organization, or no such invoice for it — including an invoice that is still a draft
409 You have billing access to several organizations — pass entity_id
422 Invalid parameters, for example start after end, a limit outside 1–100, or a month after the current month
503 Billing account or permissions could not be verified — safe to retry

A 404 is returned for an invoice that does not exist, one belonging to another organization, and one that is still a draft. These 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".