Refunds and Returns
Initiate provider refunds safely and follow immutable return operations.
Refunds and Returns
Refunds are financial commands against a verified payment receipt. The receipt balance, currency, and version returned by the server are the authority for each request. Transaction totals and earlier event lists remain useful history, but clients must not subtract those values to decide what can be refunded.
Available workflows
The transaction screen separates two workflows:
- Initiate payout asks the connected provider to return funds. Stripe is the only provider that currently supports initiation through this workflow.
- Record external return would record money already returned outside the gateway. It is unavailable because the gateway does not yet have a receipt or source verifier for that evidence.
Legacy manual refund, manual chargeback, and financial-status mutation controls are unavailable. Existing refund and dispute history remains readable.
Open a receipt-backed completed transaction and select Process Refund. The form shows the authoritative held capacity, sale capacity, currency, and receipt version. The server allocates held funds and sale funds; the browser does not choose or recompute that allocation. An invoice's Refund Payment action routes to its linked transaction so the same eligibility check and command are used.
Provider capabilities
| Provider | Initiate payout | Verified callback recording |
|---|---|---|
| Stripe | Supported for an eligible receipt-backed payment | Signed Stripe refund and dispute callbacks are reconciled to stored provider identities |
| Mollie | Not initiated from the Admin API | Verified provider lookups can record refund and chargeback callbacks |
| GoCardless | Not initiated from the Admin API | Signed refund callbacks can record provider-confirmed returns |
| PayPal | Not initiated from the Admin API | Verified transmission headers and stored account/payment identities protect callback recording |
| Wise and wire transfer | Not initiated from the Admin API | No verified external-return recording workflow is exposed |
Provider callbacks update immutable adjustment history only after authenticity and ownership checks. A delayed callback cannot make an older state overwrite a newer operation. A won dispute reverses that dispute's effect; it does not undo an independent refund.
API contract
All paths below are relative to the Admin API prefix /api/v1.
Read eligibility and bounded history
GET /transactions/{transactionId}/adjustments?limit=25&cursor={cursor}The response contains eligibility, capability, an adjustments page, and an optional nextCursor. eligibility.availableHeld, availableSale, availableAmount, currency, and receiptVersion are server-owned values. Use the returned cursor rather than increasing the limit without a bound.
Initiate a provider refund
POST /transactions/{transactionId}/refund/initiate
Authorization: Bearer …
Idempotency-Key: 8bd627d2-1fbb-43ad-8a20-9fd9f558c89b
Content-Type: application/json
{
"amount": 2000,
"currency": "EUR",
"reason": "customer_request",
"expectedVersion": 7
}The body accepts only amount, currency, reason, and expectedVersion. amount is in minor units. Send one canonical Idempotency-Key header containing a UUID. Do not put the operation ID in the body, add a second spelling of the header, request IPN delivery, or attach private notes or external references.
An accepted response contains the immutable operation, current capability and eligibility, and a direct status path:
{
"operation": {
"operationId": "8bd627d2-1fbb-43ad-8a20-9fd9f558c89b",
"kind": "refund",
"workflow": "initiate_provider",
"state": "reserved",
"requestedAmount": 2000,
"components": { "held": 500, "sale": 1500, "credit": 0 },
"currency": "EUR",
"reason": "customer_request",
"expectedVersion": 7
},
"statusPath": "/transactions/…/adjustments/8bd627d2-1fbb-43ad-8a20-9fd9f558c89b?kind=refund"
}reserved, pending, and unknown are non-terminal. succeeded, failed, and cancelled are terminal only when the readback matches the original organization, transaction, workflow, operation ID, payload digest, and expected version.
Read one operation directly
GET /transactions/{transactionId}/adjustments/{operationId}?kind=refundUse the exact statusPath returned by the server. If the first response is lost, derive the same canonical path from the transaction ID and already allocated idempotency key. A 404, timeout, proxy error, malformed response, or ownership mismatch is not proof that no mutation happened.
Retry and recovery rules
Allocate the idempotency key before the first network request and keep it with a digest of the exact intent, original expectedVersion, and canonical status path. Scope that identity to organization, transaction, and initiate_provider. Do not store the reason, amount, customer data, provider secrets, or other form content.
After 202, a timeout, a lost response, unknown, or a readback failure:
- Retry only the original amount, currency, reason, and expected version with the same key.
- Read the direct operation path with a bounded polling budget.
- Refresh eligibility and history for display, but do not replace the stored intent or version with newly displayed values.
- Keep the identity until a matching terminal operation is validated.
A changed payload with the same key is a conflict. A new key while the first intent may exist can reserve the same money twice, so the UI blocks concurrent submissions across mounts in the same browser session.
REFUND_VERSION_CONFLICT and REFUND_INITIATION_CONFLICT retain the existing identity. An exact first attempt may be corrected only when the response identifies the same operation and transaction, reports mutationApplied: false, and uses the explicit REFUND_INITIATION_REJECTED or REFUND_INITIATION_UNSUPPORTED code. An earlier uncertain attempt never qualifies for that reset.
Payment, tax, and document boundaries
A provider refund records a payment return. It does not by itself prove that tax records or a legal document were created. When a receipt-backed return becomes an immutable committed economic return and the original receipt has one exact qualified historical tax plan, the receipt-tax worker asynchronously materializes the signed historical correction and actual-return receipt. The correction preserves the actual return date, original tax point and reporting period, and parent receipt/document lineage; it does not use current tax settings.
Tax and document completion remain separate from provider completion. In Tax Settings → Tax record maintenance, preview receipt-tax recovery and confirm only the exact clean preview. Unsupported document capability, missing historical evidence, ambiguous materialized roots, changed evidence, or an unsealed/unpublished PDF remains fail-closed and needs review. A download appears in the Tax Record detail only after the stored seal, current ready rendition and publication proof all validate. Do not create a compensating sale, move the original receipt date, or treat a provider success as proof of tax or legal-document completion.
For invoices without linked payment records, a separate document-side credit-note workflow may be available. For linked payments, follow the transaction return and then reconcile any required tax or document work independently.
Controlled migration and rollback
Treat this as a coordinated frontend, Admin Backend, Main Backend, and database cutover:
- Back up MongoDB and test restoration before the maintenance window.
- Deploy code that can read existing historical refund and dispute events before enabling new initiation.
- Create and verify the shipped unique operation-identity and receipt-version indexes. Stop if duplicates or index build errors appear; do not weaken uniqueness.
- Run receipt backfill in dry-run, bounded pages first. Review unresolved and duplicate evidence, then apply bounded pages with cursor checkpoints. Do not synthesize immutable operations from aggregate refund totals.
- Enable the new frontend only after the adjustment read and initiation endpoints are healthy. Historical events stay read-only.
- Verify same-key replay, changed-payload conflict, direct operation readback, tenant isolation, and available held/sale capacity before ending the window.
Rollback may remove the new frontend entry point while leaving accepted operations and indexes in place. Do not roll back by deleting operations, clearing reservations, re-enabling legacy mutation routes, or restoring a database snapshot over newer accepted payment events. If an accepted intent remains non-terminal, keep its identity and recover it with the same backend version or complete a reviewed forward repair.
PayPal provider migration is also one-way for stored endpoint overrides. Back up and test restoration, then inventory affected providers by organization, provider ID, label, receipt account identity, enabled state, explicit mode, and apiBaseUrl presence only. Never record configuration values or credentials. In a controlled maintenance window, open each affected provider in Providers → Edit, explicitly select canonical sandbox or live mode, keep masked credentials and all other settings unchanged, and select Save Changes. The supported save removes only apiBaseUrl and is idempotent. Reopen the provider and confirm the migration notice is gone, the mode and receipt account identity match the inventory, credentials remain masked, and the matching PayPal app and webhook use the same environment before callbacks resume.
Do not roll back by restoring apiBaseUrl, deleting accepted refund or dispute events, clearing operations, or restoring an older database over newer accepted payment records. Roll back application code independently. Correct an incorrectly selected mode with another maintenance save and readback. Production has no custom API-base override or fallback.