api

Admin invoice refunds

POST /admin/@refund_invoice refunds a Stripe or PayPal invoice in full. Authenticate with Authorization: Bearer <admin-key>; the existing ?key=<admin-key> authentication also works. Unauthorized requests return 404.

Send JSON with the numeric primary key from accounts_invoices.id, not the public invoice UUID:

{ "invoiceId": 123 }

The API selects the gateway and payment reference from the invoice. It does not accept amount, reason, or payment-reference overrides. Only paid invoices can initiate a refund. Already-refunded invoices can reconcile a refund confirmed by the provider. Pending/failed invoices, unsupported gateways, missing payment references, and partial refunds cannot initiate a refund.

Results

A completed refund returns HTTP 200:

{
  "invoiceId": 123,
  "gateway": "stripe",
  "status": "refunded",
  "refundId": "re_example"
}

If inspection finds a completed full refund, status is already_refunded and no new refund is submitted. refundId can be null when completion is known from the original transaction or several refunds together cover the payment.

A pending refund returns HTTP 202 with the same fields and status: "pending". This includes Stripe refunds awaiting action and PayPal delayed refunds. The invoice remains paid until completion is confirmed by a later request, Stripe webhook, or PayPal IPN.

Confirmed refunds use the existing invoice bookkeeping: cancel the account’s payment method/renewal, mark the invoice refunded, and recalculate paid access using the remaining pending/paid invoices. Refunding the latest invoice can end access immediately. If cancellation fails, the invoice remains paid so a later request can retry. Once the invoice is refunded, later requests and provider events do not cancel a replacement payment method.

Errors

Errors contain an error code:

HTTP Error Meaning
400 invalid_invoice_id Missing or invalid positive integer ID.
404 unknown_invoice No matching invoice.
409 invoice_not_paid Invoice is neither paid nor refunded.
409 unsupported_gateway Gateway is not Stripe or PayPal.
409 missing_payment_reference No stored transaction reference.
409 payment_not_refundable Provider state does not permit this refund.
409 payment_partially_refunded A partial refund exists or is pending.
409 invoice_state_changed Invoice says refunded but the provider says refundable.
409 refund_failed Provider returned a failed/canceled refund object. Includes result fields.
429 daily_refund_limit_reached The daily submission allowance is exhausted.
502 refund_lookup_failed Provider inspection failed; nothing was submitted by this request.
502 refund_outcome_unknown Submission threw an error or returned an unrecognized result. Completion is not confirmed.
500 refund_bookkeeping_failed The provider confirmed the refund, but local bookkeeping failed. Includes result fields.

Limits and retries

The separate admin_refund_invoice Redis bucket allows 10 submission attempts per UTC day. Validation, inspections, reconciliation, and recognized pending refunds consume no allowance. Every submission attempt consumes allowance even if it fails; exhausted requests return 429 until UTC midnight. The existing cancellation allowance is independent.

Each payment has a stable provider idempotency identifier: Stripe uses admin-refund:<payment-intent>, and PayPal uses a 38-character MSGSUBID derived from the transaction reference. Stripe automatic network retries are disabled for this operation. Neither provider submission is automatically retried by this route.

After refund_outcome_unknown, a subsequent explicit request first inspects the provider. A confirmed refund is reconciled without another submission. If the payment is still refundable, submission uses the same identifier and consumes another attempt. Provider errors during submission are conservatively reported as an unknown outcome, including explicit API rejections without a refund object.

After refund_bookkeeping_failed, retry the same invoice to complete bookkeeping without issuing another refund. Do not create a new payment reference or idempotency identifier to recover an uncertain outcome.

Stripe’s existing fraud automation keeps its fraud-specific reason and idempotency identifier. No customer email, schema migration, or deployment is part of this endpoint.