invoices
Stable. Describes behaviour that is built and covered by tests in this repository.
GET /v1/invoices #
List invoices (ordered by id)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
customer | query | string | Customer external id |
period | query | string | |
status | query | draft | open | paid | uncollectible | void | |
page_size | query | integer | |
page_token | query | string |
Responses
| Status | Meaning | Body |
|---|---|---|
200 | OK | ListInvoicesResponse |
400 | Error envelope | Error |
404 | Error envelope | Error |
429 | Error envelope | Error |
Example response
{
"invoices": [
{
"id": "string",
"customer_id": "string",
"customer_external_id": "string",
"period_id": "string",
"kind": "REGULAR",
"number": "INV-2026-000123",
"currency": "string",
"subtotal_minor": "string",
"total_minor": "string",
"status": "draft",
"saga_state": "string",
"create_time": "2026-11-03T10:15:00Z",
"finalize_time": "2026-11-03T10:15:00Z",
"lines": [
{
"line_no": 1,
"kind": "BASE_FEE",
"meter_code": "string",
"origin_period_id": "string",
"quantity": "1.250",
"amount_minor": "string",
"description": "string",
"service_from": "2026-11-03T10:15:00Z",
"service_to": "2026-11-03T10:15:00Z"
}
],
"payments": [
{
"id": "string",
"invoice_id": "string",
"method": "bank_transfer",
"amount_minor": "string",
"currency": "string",
"received_at": "2026-11-03T10:15:00Z",
"reference": "string",
"note": "string",
"recorded_by": "string",
"recorded_at": "2026-11-03T10:15:00Z"
}
],
"plan_version_id": "string",
"pdf_object_key": "string",
"credit_surplus_minor": "string"
}
],
"next_page_token": "string"
}curl
curl -X GET https://YOUR-API-ADDRESS/v1/invoices \
-H "Authorization: Bearer $PAYRUDDER_API_KEY"GET /v1/invoices/{id} #
One invoice with its lines
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string · uuid |
Responses
| Status | Meaning | Body |
|---|---|---|
200 | OK | Invoice |
404 | Error envelope | Error |
429 | Error envelope | Error |
Example response
{
"id": "string",
"customer_id": "string",
"customer_external_id": "string",
"period_id": "string",
"kind": "REGULAR",
"number": "INV-2026-000123",
"currency": "string",
"subtotal_minor": "string",
"total_minor": "string",
"status": "draft",
"saga_state": "string",
"create_time": "2026-11-03T10:15:00Z",
"finalize_time": "2026-11-03T10:15:00Z",
"lines": [
{
"line_no": 1,
"kind": "BASE_FEE",
"meter_code": "string",
"origin_period_id": "string",
"quantity": "1.250",
"amount_minor": "string",
"description": "string",
"service_from": "2026-11-03T10:15:00Z",
"service_to": "2026-11-03T10:15:00Z"
}
],
"payments": [
{
"id": "string",
"invoice_id": "string",
"method": "bank_transfer",
"amount_minor": "string",
"currency": "string",
"received_at": "2026-11-03T10:15:00Z",
"reference": "string",
"note": "string",
"recorded_by": "string",
"recorded_at": "2026-11-03T10:15:00Z"
}
],
"plan_version_id": "string",
"pdf_object_key": "string",
"credit_surplus_minor": "string"
}curl
curl -X GET https://YOUR-API-ADDRESS/v1/invoices/<id> \
-H "Authorization: Bearer $PAYRUDDER_API_KEY"POST /v1/invoices/{id}/links #
Make a link to the invoice that its customer can open without signing in
The link shows the finalized invoice as a page, and its PDF once it is rendered. It is unguessable (256 random bits; only a hash is kept), works until expires_in_days (default 90, at most 365) or until revoked, and the token is returned once. The page is not indexed or cached, loads nothing from elsewhere, and is available in English, Turkish, German, French, Spanish and Arabic (?lang= or the browser's language). The url is absolute when the deployment has a public address set.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string · uuid |
Request body
| Field | Type | Description |
|---|---|---|
expires_in_days | integer |
Example
{}Responses
| Status | Meaning | Body |
|---|---|---|
201 | Created | CreateInvoiceLinkResponse |
400 | Error envelope | Error |
404 | Error envelope | Error |
409 | Error envelope | Error |
Example response
{
"token": "string",
"url": "string",
"expires_at": "2026-11-03T10:15:00Z"
}curl
curl -X POST https://YOUR-API-ADDRESS/v1/invoices/<id>/links \
-H "Authorization: Bearer $PAYRUDDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'DELETE /v1/invoices/{id}/links #
Make every link to the invoice stop working
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string · uuid |
Responses
| Status | Meaning | Body |
|---|---|---|
200 | OK | RevokeInvoiceLinksResponse |
404 | Error envelope | Error |
Example response
{
"revoked": 1
}curl
curl -X DELETE https://YOUR-API-ADDRESS/v1/invoices/<id>/links \
-H "Authorization: Bearer $PAYRUDDER_API_KEY"GET /v1/invoices/{id}/pdf #
A presigned link to the invoice PDF (valid for 5 minutes)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string · uuid |
Responses
| Status | Meaning | Body |
|---|---|---|
200 | OK | SignedUrl |
404 | Error envelope | Error |
409 | Error envelope | Error |
Example response
{
"url": "string",
"expires_at": "2026-11-03T10:15:00Z"
}curl
curl -X GET https://YOUR-API-ADDRESS/v1/invoices/<id>/pdf \
-H "Authorization: Bearer $PAYRUDDER_API_KEY"GET /v1/invoices/{id}/payment-wait #
What an invoice's payment is waiting for (a first charge that is held, a customer who has to confirm it)
Only state is always set. PAYMENT_HELD: the first charge was not sent because the payment gate said no (see hold_reason); since_time is when it began to be held and next_check_time when Payrudder looks again (it charges the invoice then if nothing holds it any more). PAYMENT_ACTION_REQUIRED: the card needs the customer to confirm the payment with their bank (3-D Secure); the customer was mailed a link to the invoice page at since_time (the mail log says whether it arrived), Payrudder asks the provider again at next_check_time, and if the customer has not confirmed by window_ends_time the payment counts as declined and dunning applies. Any other state answers the state alone.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string · uuid |
Responses
| Status | Meaning | Body |
|---|---|---|
200 | OK | PaymentWait |
404 | Error envelope | Error |
Example response
{
"state": "PAYMENT_HELD"
}curl
curl -X GET https://YOUR-API-ADDRESS/v1/invoices/<id>/payment-wait \
-H "Authorization: Bearer $PAYRUDDER_API_KEY"POST /v1/invoices/{id}/release #
Ask again for an invoice whose first charge is held, and charge it if nothing holds it any more
Payrudder asks the payment gate again, as it does every hour. When the gate lets the invoice through the charge is sent now (answer PAYMENT_REQUESTED); when the customer is still blocked for fraud or a chargeback, or the environment is on hold, the invoice stays held (answer PAYMENT_HELD, see hold_reason of the payment-wait read): to charge a blocked customer, lift the block first. 409 billing.invoice.not_held when the invoice's first charge is not held. Recorded in the audit log.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string · uuid |
Responses
| Status | Meaning | Body |
|---|---|---|
200 | OK | InvoiceSaga |
404 | Error envelope | Error |
409 | Error envelope | Error |
Example response
{
"id": "string",
"invoice_id": "string",
"state": "string",
"attempt": 1,
"dunning_attempt": 1,
"last_error": "string",
"version": 1
}curl
curl -X POST https://YOUR-API-ADDRESS/v1/invoices/<id>/release \
-H "Authorization: Bearer $PAYRUDDER_API_KEY"POST /v1/invoices/{id}/credit-notes #
Correct one of your paid invoices with a numbered credit note, added to the customer's credit balance or refunded to the card
An issued invoice is never edited. The note's amount goes to the customer's credit balance in the invoice's currency and pays their next invoice, or, with refund_to_card, back to the card through the payment provider that took the payment (the note's refund_state says whether it went through; a refund nobody answers is sent again, and a failed one can be retried with POST /v1/credit-notes/{id}/retry-refund). The notes of an invoice never add up to more than the invoice. Only a paid invoice can be credited: 409 billing.credit_note.not_paid for any other, 409 billing.credit_note.fully_credited when nothing is left of it, 409 billing.credit_note.nothing_to_refund when refund_to_card is asked of a payment no provider can return. Works on the invoices of the key's (or the signed-in person's) own environment only. Needs invoices:write. Recorded in the audit log (credit_note.issued).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string · uuid |
Request body
| Field | Type | Description |
|---|---|---|
amount_minor | string · int64 | int64 minor units; 0 or absent = what is left of the invoice |
reason | string | |
refund_to_card | boolean | Refund the amount to the card instead of adding it to the credit balance |
Example
{}Responses
| Status | Meaning | Body |
|---|---|---|
201 | Created | CreditNote |
400 | Error envelope | Error |
403 | Error envelope | Error |
404 | Error envelope | Error |
409 | Error envelope | Error |
Example response
{
"id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
"number": "string",
"invoice_id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
"invoice_number": "string",
"customer_id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
"customer_external_id": "string",
"currency": "string",
"total_minor": "string",
"tax_minor": "string",
"reason": "string",
"create_time": "2026-11-03T10:15:00Z",
"disposition": "CREDIT_NOTE_DISPOSITION_CREDIT",
"refund_state": "REFUND_STATE_REQUESTED",
"refund_error": "string"
}curl
curl -X POST https://YOUR-API-ADDRESS/v1/invoices/<id>/credit-notes \
-H "Authorization: Bearer $PAYRUDDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'POST /v1/invoices/{id}/void #
Void an invoice whose first charge is held
Nothing was charged, so nothing is refunded; the invoice becomes void (answer VOIDED) and the invoice.voided webhook event is sent. Only an invoice whose first charge is held can be voided here: 409 billing.invoice.not_held for any other, a paid one included. Recorded in the audit log.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string · uuid |
Responses
| Status | Meaning | Body |
|---|---|---|
200 | OK | InvoiceSaga |
404 | Error envelope | Error |
409 | Error envelope | Error |
Example response
{
"id": "string",
"invoice_id": "string",
"state": "string",
"attempt": 1,
"dunning_attempt": 1,
"last_error": "string",
"version": 1
}curl
curl -X POST https://YOUR-API-ADDRESS/v1/invoices/<id>/void \
-H "Authorization: Bearer $PAYRUDDER_API_KEY"Models
CreateCreditNoteRequest
| Field | Type | Description |
|---|---|---|
amount_minor | string · int64 | int64 minor units; 0 or absent = what is left of the invoice |
reason | string | |
refund_to_card | boolean | Refund the amount to the card instead of adding it to the credit balance |
CreateInvoiceLinkRequest
| Field | Type | Description |
|---|---|---|
expires_in_days | integer |
CreateInvoiceLinkResponse
| Field | Type | Description |
|---|---|---|
token required | string | |
url required | string | |
expires_at required | string · date-time |
CreditNote
| Field | Type | Description |
|---|---|---|
id required | string · uuid | |
number required | string | CN-2026-000007 (live) or SBX-CN-2026-000007 (sandbox) |
invoice_id required | string · uuid | |
invoice_number required | string | |
customer_id required | string · uuid | |
customer_external_id required | string | |
currency required | string | |
total_minor required | string · int64 | |
tax_minor required | string · int64 | The part of total_minor that is the invoice's tax given back: the note's share of the invoice's total taken from the invoice's tax, so the notes of an invoice give back its tax exactly. 0 for an untaxed invoice and for a note made before this field existed |
reason required | string | |
create_time required | string · date-time | |
disposition required | CREDIT_NOTE_DISPOSITION_CREDIT | CREDIT_NOTE_DISPOSITION_REFUND | Where the amount went: the credit balance, or back to the card |
refund_state required | REFUND_STATE_REQUESTED | REFUND_STATE_REFUNDED | REFUND_STATE_FAILED | Only for a refund to the card |
refund_error required | string |
Error
| Field | Type | Description |
|---|---|---|
error required | object |
Invoice
| Field | Type | Description |
|---|---|---|
id required | string | |
customer_id required | string | |
customer_external_id required | string | |
period_id required | string | |
kind required | REGULAR | CORRECTION | FEE | LINK | FEE: a fixed fee invoiced in advance for one period, or what a plan change moved. LINK: made from an ad-hoc payment link |
number required | string | |
currency required | string | |
subtotal_minor required | string · int64 | |
total_minor required | string · int64 | |
status required | draft | open | paid | uncollectible | void | |
saga_state required | string | |
create_time required | string · date-time | |
finalize_time required | string · date-time | |
lines required | array of InvoiceLine | |
payments required | array of InvoicePayment | Payments recorded by hand by the operator (they do not come through a payment provider), oldest first. Only the single-invoice reads fill it. |
plan_version_id required | string | |
pdf_object_key required | string | |
credit_surplus_minor required | string · int64 | What the lines came to below zero (late-usage corrections larger than everything else on the invoice). The total is never negative, so this was added to the customer's credit balance instead, once per invoice, and their next invoices use it. 0 for every other invoice |
InvoiceLine
| Field | Type | Description |
|---|---|---|
line_no required | integer | |
kind required | BASE_FEE | USAGE | LATE_USAGE | CREDIT | CORRECTION | DISCOUNT | TAX | |
meter_code required | string | |
origin_period_id required | string | |
quantity required | string | |
amount_minor required | string · int64 | |
description required | string | |
service_from required | string · date-time | The service period the line covers, nullable: true |
service_to required | string · date-time |
InvoicePayment
| Field | Type | Description |
|---|---|---|
id required | string | |
invoice_id required | string | |
method required | bank_transfer | cash | other | |
amount_minor required | string · int64 | |
currency required | string | |
received_at required | string · date-time | |
reference required | string | |
note required | string | |
recorded_by required | string | |
recorded_at required | string · date-time |
InvoiceSaga
| Field | Type | Description |
|---|---|---|
id required | string | |
invoice_id required | string | |
state required | string | |
attempt required | integer | |
dunning_attempt required | integer | |
last_error required | string | |
version required | integer |
ListInvoicesResponse
| Field | Type | Description |
|---|---|---|
invoices required | array of Invoice | |
next_page_token required | string |
PaymentWait
| Field | Type | Description |
|---|---|---|
state required | string | The invoice's saga state (`Invoice.saga_state`). Everything below is set only for PAYMENT_HELD and PAYMENT_ACTION_REQUIRED |
since_time | string · date-time | When the payment began to wait. For PAYMENT_ACTION_REQUIRED, the moment the mail asking the customer to confirm it was asked for (not proof that it was delivered) |
next_check_time | string · date-time | When Payrudder looks again (the next pass over held charges, or the next question to the provider) |
hold_reason | customer_blocked | tenant_on_hold | none | PAYMENT_HELD only, asked of the payment gate now. `customer_blocked`: the customer is blocked for fraud or a chargeback. `tenant_on_hold`: the operator has suspended, frozen or banned the environment. `none`: nothing holds it now, the next pass charges it. Absent when the gate could not be asked |
window_ends_time | string · date-time | PAYMENT_ACTION_REQUIRED only: when the payment counts as declined if the customer has not confirmed it. Absent when the payment service did not answer |
RevokeInvoiceLinksResponse
| Field | Type | Description |
|---|---|---|
revoked required | integer |
SignedUrl
| Field | Type | Description |
|---|---|---|
url required | string · uri | |
expires_at required | string · date-time | RFC 3339, UTC |