Factuarea API

Reconciling with system metadata

The metadata keys Factuarea writes on invoices auto-issued from a Stripe subscription cycle, and how to use the metadata filter to pull every invoice of a subscription or of a billing period.

Every document in Factuarea carries a free-form metadata object you can write whatever you need into. On invoices that Factuarea issues automatically from a Stripe subscription cycle, the platform also writes a handful of system keys that tie the invoice back to the charge it came from: which Stripe invoice, which subscription, which billing period.

Those keys are what makes reconciliation possible without keeping your own mapping table. They have been written for a while; this page is where they get documented.

Scope: subscription cycles. These keys are written by the flow that auto-issues an invoice for a billed subscription cycle (see subscription cycles). One-shot charges auto-invoiced from charge.succeeded do not carry them today — for those, correlate through the auto-invoiced charges listing, which exposes the charge-side identifiers.

The system keys

KeyWhat it identifiesFormatPresence
stripe_invoice_idThe Stripe invoice of the billed cycleStripe id, in_…Always
billing_reasonWhy Stripe billed that invoiceStripe's raw billing_reason — in practice subscription_create (first cycle) or subscription_cycle (each renewal), the only two that are auto-invoicedAlways
stripe_subscription_idThe subscription the cycle belongs toStripe id, sub_…Optional — omitted when Stripe sends no subscription id
period_startFirst day of the billed periodYYYY-MM-DD, UTCOptional — omitted when the period timestamp is absent
period_endEnd of the billed period, verbatim from Stripe's period_end — the exclusive boundary, so for a monthly cycle it is the first day of the next period, not the last day of this oneYYYY-MM-DD, UTCOptional — omitted when the period timestamp is absent

Optional keys are not materialised as null or empty: when the value does not apply, the key is not written at all. That is deliberate — a key present with an empty value would look like a correlation that exists but is blank, and any code reading it would have to distinguish "no subscription" from "subscription unknown". Check for the key's presence, not for its value.

These are system keys. Do not write them by hand. They are the correlation between a Factuarea invoice and a Stripe object, and the reconciliation recipes below trust them. Writing stripe_invoice_id yourself on an unrelated invoice makes that invoice show up in a reconciliation it does not belong to, and nothing will flag it — metadata is free-form by design. Use your own keys (erp_ref, project_code, …) for your own correlations.

The keys are readable wherever the invoice is: metadata is part of the invoice resource, and it comes back as a JSON object ({} when empty).

Filtering by metadata

Eight v1 listings accept a metadata filter:

ResourceEndpoint
InvoicesGET /v1/invoices
QuotesGET /v1/quotes
Pro-forma invoicesGET /v1/proformas
Delivery notesGET /v1/delivery_notes
Purchase invoicesGET /v1/purchase_invoices
Recurring invoicesGET /v1/recurring_invoices
ProductsGET /v1/products
SuppliersGET /v1/suppliers

The syntax is deepObject: metadata[key]=value, one query parameter per pair.

  • Pairs combine with AND. Two pairs return the documents that match both.
  • Exact match on the value; there is no partial or prefix matching.
  • Up to 50 pairs per request; more returns parameter_invalid_range.
  • Keys must match [A-Za-z0-9_.-] and be 1 to 64 characters long; anything else returns parameter_invalid_enum.
  • The filter sits outside the {operator, value} contract of the column filters, so there is no metadata[key][eq] form. metadata[key]=value is the whole syntax.

Let curl encode the brackets. [ and ] are glob characters for curl and reserved characters in a URL. Pass the pairs with -G --data-urlencode, as in the recipes below, and curl encodes them correctly. Pasting a raw ?metadata[key]=value into a shell is where "the filter is being ignored" usually comes from.

Recipe: every invoice of one subscription

The reconciliation you need when a customer asks for all the invoices of their plan, or when you close the year for one subscriber:

curl -G https://api.factuarea.com/v1/invoices \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  --data-urlencode "metadata[stripe_subscription_id]=sub_1QRstuVWXYZabcde" \
  --data-urlencode "limit=100"
{
  "data": [
    {
      "id": "0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f",
      "object": "invoice",
      "number": "2026/0184",
      "total": "49.90",
      "currency": "EUR",
      "metadata": {
        "stripe_invoice_id": "in_1QRstuVWXYZabcde",
        "billing_reason": "subscription_cycle",
        "stripe_subscription_id": "sub_1QRstuVWXYZabcde",
        "period_start": "2026-07-01",
        "period_end": "2026-08-01"
      }
    }
  ],
  "has_more": false,
  "next_cursor": null
}

The listing is cursor-paginated like every other one: keep reading while has_more is true, feeding next_cursor back into starting_after. See pagination.

Recipe: the invoices of one billing period

Two pairs, combined with AND: the subscription and the first day of the period. This is the query that answers "did July's cycle get invoiced?".

curl -G https://api.factuarea.com/v1/invoices \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  --data-urlencode "metadata[stripe_subscription_id]=sub_1QRstuVWXYZabcde" \
  --data-urlencode "metadata[period_start]=2026-07-01"

Because period_start and period_end are exact dates in UTC, filter on the period boundary itself rather than on a range — the value in metadata is the day Stripe reports for the cycle, not a local calendar month. To sweep a whole month of cycles across every subscription, drop the subscription pair and query metadata[period_start] alone.

Filter on period_start, not on period_end. period_end is Stripe's exclusive upper bound: the July cycle of a monthly subscription carries period_start: 2026-07-01 and period_end: 2026-08-01. Querying metadata[period_end]=2026-07-31 returns nothing, and the empty result looks exactly like a cycle that was never invoiced.

An empty data array for a period you expected to be invoiced is a real signal, not a filter mistake. That is exactly the case the integration event inbox explains: open it filtered by provider=stripe and status=skipped and the typed discard reason will tell you whether the cycle was skipped because auto-invoicing was off, the cycle carried no amount, or something else — and whether you can replay it.

On this page