Invoices

An invoice is a billable document — line items, totals, discounts, payment terms — that produces a charge against a customer or account. Invoices are how subscriptions bill (one invoice per cycle), and they're also a first-class primitive for manual B2B billing: net-30 terms, ad-hoc invoices for consulting work, milestone billing.

Where invoices come from

Two creation paths:

  • Automatic from subscriptions — each billing cycle, the subscription bills the customer directly via the renewal chain (Subscription → ChargeIntent → Charge → Transfer). An invoice is not generated for the normal renewal — it's only created in the optional metered-billing trueup path (Billings::HandleSubscriptionRenewalService). If you need an invoice artifact per renewal for accounting or PDF delivery, generate one application-side off the subscription.renewed webhook.
  • ManualPOST /v1/invoices to create an invoice directly. Useful for one-off B2B billing where you're not running a recurring subscription, or for charge flows that need invoice-style itemization and customer-facing payment terms.

Both paths produce the same primitive. From here on, "invoice" means either.

Collection methods

The collection_method field controls how the invoice settles:

  • auto_charge — Frame charges the auto_charge_payment_method when the invoice is issued. For subscriptions, this is the default. For B2B manual invoices, use this when you've stored the customer's payment method and have authority to charge.
  • request_payment — Frame doesn't auto-charge. Instead, the invoice is delivered to the customer (email, dashboard, or via a payment link) and they pay it on their own. Use for net-terms billing.

auto_charge_payment_method is required if collection_method = auto_charge. Without it, the invoice has nothing to charge.

Lifecycle

Invoices carry a status state machine:

  • draft — created but not finalized. Line items can still be added/edited; not yet sent to the customer or charged.
  • outstanding — issued and awaiting payment. Set when you call issue on the invoice and the due date is in the future.
  • due — issued and the due date has arrived. Same operational state as outstanding for an outstanding invoice that's tipped over its due date — Frame transitions automatically.
  • overdue — due date passed without payment.
  • paid — fully collected.
  • written_off — manually marked uncollectible.
  • voided — canceled before collection.

Transitions:

  • draft → outstanding | due via issue (which-state depends on whether the due date is future or past).
  • outstanding → due via time passing (mark_as_due).
  • due → overdue via time passing (mark_as_overdue).
  • outstanding | due | overdue → paid via pay.
  • Terminal: paid, written_off, voided.

Fields

curl --request POST \
  --url https://api.framepayments.com/v1/invoices \
  --header "Authorization: Bearer $FRAME_SECRET_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "customer": "<customer_id>",
    "collection_method": "auto_charge",
    "payment_method": "<payment_method_id>",
    "net_terms": 0,
    "description": "Pro Plan — June 2026",
    "promotion_codes": ["LAUNCH10"],
    "line_items": [
      {
        "product": "<product_id>",
        "quantity": 1
      }
    ]
  }'

The interesting fields:

  • customer / account — owner. Mutually exclusive, optional only in unusual flows.
  • collection_method — see above.
  • payment_method — request-body parameter name. Frame maps it to the auto_charge_payment_method field on the invoice record. Required for auto_charge.
  • net_terms — days from issue to due date. 0 means due immediately on issue; 30 is net-30; etc.
  • number — invoice number for your internal/customer reference. Frame can auto-generate; supply if you want a specific format.
  • description / memo — descriptive text.
  • promotion_codes[] — array of code strings; Frame validates each and applies the discounts (via Discounts::ComputeService) to the invoice's gross amount.
  • line_items[] — array of line items. Each entry takes product (FK) and quantity only — pricing is inherited from the product. See invoice line items for the constraints.
  • metadata — k/v pairs for your internal tracking.

On the response, the invoice's amount_cents is the final post-discount total. The breakdown lives in gross_amount_cents, discount_amount_cents, and discount_count.

Issuing

A draft invoice doesn't bill anyone. Call POST /v1/invoices/<id>/issue to transition it to outstanding (or due, if the due date has already passed):

  • For auto_charge invoices, Frame attempts the charge against auto_charge_payment_method during the issue flow.
  • For request_payment invoices, Frame surfaces the payment link / sends notification per your platform's setup.

Once issued, the line-item composition is frozen. Edits after issue land on the next invoice (for subscriptions) or require voiding and re-creating.

Discounts

Promotion codes apply at the invoice level via promotion_codes[] on create. Frame's Discounts::ComputeService evaluates each code against the validation rules and computes the resulting discount.

The fields on the invoice after discount application:

  • gross_amount_cents — total before discounts.
  • discount_amount_cents — total discount applied.
  • discount_count — number of codes that successfully resolved (0 if all rejected).
  • amount_cents — final amount to charge (gross_amount_cents - discount_amount_cents).

If discount_count is 0 after you sent codes, none of them resolved — surface this to the customer ("your code couldn't be applied — checkout proceeded at full price") rather than silently dropping the discount.

Failed collections

When the auto_charge_payment_method charge fails on issue (for auto_charge invoices):

  • The invoice transitions to outstanding / due as if the issue succeeded — the invoice exists, it just hasn't been collected.
  • The parent subscription (if any) transitions its renewal_status to failed.

Whether Frame auto-retries depends on which path the failure happened on:

  • Subscription renewals do auto-retry. When a renewal charge fails, Frame schedules a retry 24 hours later (Subscriptions::RenewSubscriptionJob), up to a maximum of 3 attempts. After the third failure, the subscription transitions to past_due / unpaid and stops retrying.
  • Manual invoice charges do not auto-retry. A failed POST /v1/invoices/<id>/pay on a standalone invoice — i.e. one not created by the subscription renewal flow — leaves the invoice in its outstanding state. There's no dunning engine for these. Re-issue manually, prompt the customer to update their payment method, or whatever your platform's recovery flow does.

Failed-collection recovery for manual invoices is a key area where your platform owns the workflow. See build subscriptions for the subscription side; for manual invoices, listen for invoice.overdue and trigger your own retry.

Webhooks

Invoice events fire on state transitions and line-item mutations:

  • invoice.created — fired on initial creation (even drafts).
  • invoice.updated — any mutation to the invoice itself.
  • invoice.issued — draft → outstanding / due.
  • invoice.paid — collected successfully.
  • invoice.overdue — due → overdue.
  • invoice.voided — voided before collection.
  • invoice.deleted — soft-deleted (draft only).
  • invoice.line_item.created — line item added.
  • invoice.line_item.updated — line item edited.
  • invoice.line_item.deleted — line item removed.

Subscribe to invoice.issued and invoice.paid for the typical "did this bill go out / did it collect" tracking.

Gotchas

Symptom: you created an invoice but no charge happened. Why: drafts don't charge. You need to call issue to transition the invoice out of draft. Fix: POST /v1/invoices/<id>/issue.

Symptom: you applied a promotion_codes array but discount_amount_cents came back zero. Why: none of the codes resolved — one of the validation checks failed for each. Fix: inspect each code's status via GET /v1/promotion_codes/<id>; surface the issue to the customer at checkout.

Symptom: you edited a line item on an issued invoice and it didn't change the charge amount. Why: line items freeze on issue. Edits after that don't retroactively re-bill. Fix: void the invoice and create a new one if the change matters, or absorb it into the next billing cycle.

Symptom: request_payment collection method but the customer says they never got an email. Why: notification delivery depends on the customer record having a deliverable email and on your Frame settings being configured for outbound mail. Fix: check the customer's email field; check Frame's delivery logs in the dashboard.

Symptom: a manually-created invoice failed collection and now sits in overdue indefinitely. Why: manual invoice charges don't auto-retry (only subscription renewals do, on the 24h × 3 schedule). Without application-side intervention, manual invoices stay overdue. Fix: build a recovery flow — listen for invoice.overdue, prompt the customer, re-issue once the payment method is updated.

Reference

For the full API surface, see POST/v1/invoices and the rest of the Invoices resource.