An invoice represents the amount due for one subscription billing period. It can contain more than one payment attempt. Subscription state and invoice state are separate and must be stored independently.
Invoice states
See Subscription lifecycle and statuses for the canonical invoice states and their effect on the subscription.
Scheduled invoice IDs
The subscription response exposes upcoming entries in scheduled_invoices. Each entry contains a reserved invoice_id and billing date.
See Plans and billing cycles for the complete scheduled-invoice window behavior.
Invoice operations
Use the XPay API reference for filters, pagination, and complete response schemas.
Recurring payment flow
- The scheduled billing date associated with the reserved invoice ID arrives.
- XPay creates or updates the invoice and attempts payment with the subscription's
payment.token. - XPay records the payment attempt on the invoice.
- On success, the invoice becomes
paidand the subscription remains or returns topaid. - On failure, XPay follows the configured retry behavior and the subscription becomes
inactive. - XPay sends
subscription_paymentandinvoice_paymentwebhooks for the payment outcome.
Pay an invoice
POST {{base_url}}/public/v1/invoice/{{invoice_id}}/payAn already-paid invoice can't be paid again. When payment succeeds, XPay moves the invoice into the subscription's paid invoice history and returns the subscription to paid.
Mark an invoice paid
Use the mark-as-paid operation when the merchant has collected the amount outside the normal XPay payment attempt and wants to record the invoice as paid.
POST {{base_url}}/public/v1/invoice/{{invoice_id}}/mark/paidThe request has no body. XPay records the invoice as manually paid, while the public subscription state remains paid.
Create an invoice payment link
POST {{base_url}}/public/v1/invoice/payment/link
{
"invoice_id": "{{invoice_id}}",
"mode": "invoice_customer"
}Use mode: "invoice_customer" to use the customer already associated with the invoice. Use mode: "customer" and supply the required customer name, email, and phone when the merchant needs to override the recipient.
Refund a paid invoice
Refunding a subscription payment doesn't change the subscription state; it remains paid.
XPay supports full and partial refunds for eligible payment methods. BNPL payments support full refunds but don't support partial refunds.
First, retrieve the invoice and select the successful PaymentIntent ID from payment_attempts. Then create the refund through the XPay refund API:
POST {{base_url}}/public/v1/refundrefunded_amount uses major currency units and supports up to two decimal places. For example, 2000 with PKR represents PKR 2,000.00, and 2000.50 represents PKR 2,000.50.
The response returns the refund object in data:
{
"success": true,
"responseStatus": "OK",
"message": "Request processed successfully.",
"data": {
"_id": "xpay_ref_{{refund_id}}",
"object_name": "refund",
"payment_intent_id": "xpay_pi_{{payment_intent_id}}",
"refunded_amount": 2000,
"currency": "PKR",
"reason": "Customer requested refund",
"status": "partially_refunded"
}
}For another partial refund against the same payment, use PUT {{base_url}}/public/v1/refund/{refund_id} with the additional amount. XPay sends REFUND_NOTIFICATION after each successful refund.
A generic refund doesn't automatically change the original invoice state. Process REFUND_NOTIFICATION and store refund information separately from the subscription and invoice states.
Reconcile invoice data
Store:
- Invoice ID and state
- Subscription ID and plan ID
- Amount and currency
- Scheduled and payment dates
- Payment attempt identifiers and outcomes