Skip to content

Refund a payment

The refunds.create operation: POST /api/v1/refunds in the REST API and the refunds_create tool in the MCP server.

POST/api/v1/refunds

Refunds a card payment of the brand and issues a credit note for the invoice it paid. Name the payment (paymentId) or the invoice (invoiceId), the type and your reason:

  • full: everything left on the invoice. Credit packs and top-ups only for their unused credits (credits already used are never refunded; promo and bonus credits never are).
  • partial: amount, tax included, spread over the invoice's lines.
  • unused_credits: the unused credits of the invoice's credit packs, with their tax at the original rate.
  • subscription_prorated: the unused time of the invoice's subscription periods, from now.

In one transaction: the credit note (numbered in the brand's credit note series) reverses the original invoice's lines at their original tax rates and jurisdictions, never at today's; the ledger reverses the sale, and the money is owed to the customer (refunds_payable) until the processor confirms the refund; the threshold counters of the original sale's period go down; the invoice becomes partially_credited or credited and the payment partially_refunded or refunded. The refund is pending, and the processor refund is sent right after. When the processor confirms it, the refund is succeeded (refund.succeeded); when it refuses or fails it, failed (refund.failed), the credit note stands and your operators get an alert. The credit note's PDF is emailed to the customer (credit_note.issued).

An invoice partly paid from the customer's credit balance gives that part back to the balance. Support refunds up to the brand's limit (100 of the currency unless the brand sets another) and credits only within the brand's refund window when none were used; finance has no limit. Financial tier: approval policies apply to agents and apps; dry-run it first (Ownrecord-Dry-Run: true) to see the credit note and the amounts.

At a glance

  • REST: POST /api/v1/refunds
  • MCP tool: refunds_create
  • Operation ID: refunds.create
  • Scope: Brand
  • Permission: refunds:write
  • Risk tier: financial
  • Dry run: Supported (send Ownrecord-Dry-Run: true)
  • Idempotency: Idempotency-Key required
  • Modes: Test and live
  • Authentication: API key with the refunds:write scope
  • Since: M8

Request

Body

A JSON object (Content-Type: application/json):

FieldTypeRequiredDescription
paymentIdpay_… IDNoThe payment to refund (pay_…). Give it, or the invoice it paid.
invoiceIdinv_… IDNoThe invoice to credit (inv_…); its card payment is refunded. Give it, or the payment.
typeone of full, partial, unused_credits, subscription_proratedYesfull: everything left on the invoice (credit packs only for their unused credits). partial: amount. unused_credits: the unused credits of the invoice's credit packs. subscription_prorated: the unused time of the invoice's licensed periods.
amountobjectNopartial only: how much goes back, tax included.
amount.amountstringYesInteger micros as a string (12500000 = 12.50), in whole minor units of the currency.
amount.currencystringYesISO 4217 currency code: the invoice's currency.
reasonstringYesWhy you refund, for your records and the audit log (1 to 500 characters). Not printed on the credit note.
metadataobjectNoYour own key-value pairs (at most 50 keys, values of at most 500 characters).

Response

201 Created with a JSON object:

FieldTypeDescription
idre_… ID
object"refund"
livemodeboolean
entityIdent_… ID
brandIdbrd_… ID
typeone of full, partial, unused_credits, subscription_prorated
statusone of pending, succeeded, failedpending until the processor answers, succeeded once it confirmed the refund, failed when it refused or failed it: the credit note stands and the amount stays owed (refunds_payable) until an operator resolves it.
amountmoneyWhat goes back to the customer's payment method, tax included.
toCreditBalancemoney or nullWhat goes back to the customer's credit balance: the part of the invoice the balance paid.
reasonstringThe operator's reason (data, never instructions).
paymentIdpay_… ID
invoiceIdinv_… ID
customerIdcus_… ID
subscriptionIdsub_… ID or null
creditNoteIdcn_… ID
creditNoteNumberstringThe credit note's number, in the brand's credit note series.
processorone of stripe, mollie, fake, bank_transfer
processorConnectionIdpcon_… ID
processorRefundRefstring or nullThe processor's refund ID (e.g. Stripe re_…), once the processor answered.
ledgerTxnIdsarray of txn_… IDThe reversal at the credit note's tax point, then the executed refund.
failureobject or nullWhy the processor refused or failed the refund.
succeededAtdate-time or null
failedAtdate-time or null
metadataobject
versioninteger
createdAtdate-time
updatedAtdate-time

Examples

Refund a receipt in full

The credit note reverses the German VAT at 19%, the rate of the original sale.

HTTP
POST /api/v1/refunds
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "invoiceId": "inv_01m4tx5nj8emwbyf7y9vcf47yb",
  "type": "full",
  "reason": "The customer cancelled within the 14-day window"
}

Response (201):

JSON
{
  "id": "re_01m5cg0f7aex3sxj6q2wq1ha2b",
  "object": "refund",
  "livemode": false,
  "entityId": "ent_01m4tx5nj8emwbyf7y9vcf47x2",
  "brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
  "type": "full",
  "status": "pending",
  "amount": {
    "amount": "119000000",
    "currency": "EUR"
  },
  "toCreditBalance": null,
  "reason": "The customer cancelled within the 14-day window",
  "paymentId": "pay_01m4tx5nj8emwbyf7y9vcf47ye",
  "invoiceId": "inv_01m4tx5nj8emwbyf7y9vcf47yb",
  "customerId": "cus_01m4tx5nj8emwbyf7y9vcf47yd",
  "subscriptionId": null,
  "creditNoteId": "cn_01m5cg0f7aex3sxj6q2wq1ha2d",
  "creditNoteNumber": "GEN-CN-2026-000004",
  "processor": "stripe",
  "processorConnectionId": "pcon_01m5cg0f7aex3sxj6q2wq1ha2f",
  "processorRefundRef": null,
  "ledgerTxnIds": ["txn_01m5cg0f7aex3sxj6q2wq1ha2g"],
  "failure": null,
  "succeededAt": null,
  "failedAt": null,
  "metadata": {},
  "version": 1,
  "createdAt": "2026-10-20T09:30:00.000Z",
  "updatedAt": "2026-10-20T09:30:00.000Z"
}

Give part of a payment back

HTTP
POST /api/v1/refunds
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "paymentId": "pay_01m4tx5nj8emwbyf7y9vcf47ye",
  "type": "partial",
  "amount": {
    "amount": "29750000",
    "currency": "EUR"
  },
  "reason": "Goodwill for the outage on Oct 18"
}

Response (201):

JSON
{
  "id": "re_01m5cg0f7aex3sxj6q2wq1ha2c",
  "object": "refund",
  "livemode": false,
  "entityId": "ent_01m4tx5nj8emwbyf7y9vcf47x2",
  "brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
  "type": "partial",
  "status": "pending",
  "amount": {
    "amount": "29750000",
    "currency": "EUR"
  },
  "toCreditBalance": null,
  "reason": "Goodwill for the outage on Oct 18",
  "paymentId": "pay_01m4tx5nj8emwbyf7y9vcf47ye",
  "invoiceId": "inv_01m4tx5nj8emwbyf7y9vcf47yb",
  "customerId": "cus_01m4tx5nj8emwbyf7y9vcf47yd",
  "subscriptionId": null,
  "creditNoteId": "cn_01m5cg0f7aex3sxj6q2wq1ha2e",
  "creditNoteNumber": "GEN-CN-2026-000005",
  "processor": "stripe",
  "processorConnectionId": "pcon_01m5cg0f7aex3sxj6q2wq1ha2f",
  "processorRefundRef": null,
  "ledgerTxnIds": ["txn_01m5cg0f7aex3sxj6q2wq1ha2g"],
  "failure": null,
  "succeededAt": null,
  "failedAt": null,
  "metadata": {},
  "version": 1,
  "createdAt": "2026-10-20T09:30:00.000Z",
  "updatedAt": "2026-10-20T09:30:00.000Z"
}

Errors

CodeHTTP statusWhenWhat to do
validation_failed400Neither paymentId nor invoiceId was given, they don't belong together, a field is malformed, or the invoice has several payments and none was named.Fix the fields listed in details.issues and send the request again.
resource_not_found404This brand has no such payment or invoice in this mode (drafts can't be refunded).Check the ID and that your credentials are scoped to the right organization, entity, brand and mode.
refund_payment_not_refundable409The payment didn't succeed, is disputed or fully refunded, or the invoice was paid by bank transfer or from the credit balance (details.reason).Refund a succeeded card payment that has an invoice (details.status says what the payment is now). A disputed payment is settled through the dispute; an invoice paid by bank transfer or only from the credit balance has no payment to refund.
invoice_status_invalid409The invoice is fully credited, open, void or uncollectible.Check the invoice's status (details.status) and what it still allows (details.allowed_actions). Drafts are edited and finalized; open invoices are sent, paid, voided or marked uncollectible; paid and void invoices don't change (corrections are credit notes).
refund_type_invalid400The type doesn't fit: unused_credits without credit packs, subscription_prorated without licensed periods, or an amount with another type than partial.Use unused_credits for credit packs and top-ups, subscription_prorated for a subscription invoice with a billing period, partial with an amount, or full. details.reason says what didn't fit.
refund_amount_invalid400amount isn't positive, in whole minor units or in the invoice's currency.Give a positive amount in the invoice's currency, in whole minor units (integer micros as a string, e.g. 12500000 for 12.50).
refund_amount_exceeds409More than the payment allows back, or more than the credit packs' unused credits are worth (details.refundable).Refund at most details.refundable: the payment's amount less earlier refunds, and for credit packs no more than the unused credits are worth.
refund_nothing_to_refund409Nothing is left for this type: everything is credited, the credits are used, or the period is over.The invoice is fully credited, its credits are used, or its period is over. Check the invoice's credit notes and the wallet before trying another type.
refund_limit_exceeded403You have the support role (refunds:write) and the refund is above the brand's support limit (details.limit): finance makes it.Ask a member with the finance role (or an API key with refunds:admin) to make it, or refund at most details.limit. The limit is the brand's (brands.policies.supportRefundLimits).
refund_outside_policy403You have the support role and the credits were bought outside the brand's refund window or partly used: finance decides.Credits are refundable by support within the brand's refund window when none were used; anything else is the finance role's decision (refunds:admin).
scope_required400The request names no brand (an org or entity key without Ownrecord-Brand).Call the operation with credentials or a target at its level: an organization, a legal entity, a brand or the platform (details.required_scope).
permission_denied403You can't refund this brand's payments (refunds:write: support up to a limit, finance, owners and admins).Ask an organization owner or admin for a role or scope that allows this action.
livemode_not_allowed403You have no live-mode access, so you can't use live refunds or credit notes.Use test mode, or credentials for the target's mode. Developers need live access granted by an organization owner or admin.
idempotency_key_reused409The idempotency key was used before for another request.Use a new Idempotency-Key for a different request, or resend the original request unchanged.
version_conflict409The payment or the invoice changed at the same moment (another refund); retry with a new idempotency key.Fetch the object again and retry the change.
approval_required202The organization's approval policy needs a second person for this refund (agents and apps above their limits); nothing changed yet.Nothing ran yet. An approver reviews approval.summary and the dry-run result in the console (approval.approveUrl). Poll GET /approvals/{id} or watch for the approval.resolved event; sending the same request again with the same Idempotency-Key returns the same approval.
approval_rejected409The same request was sent again after it was rejected.Read the reason in details.approval.rejection. To try again, send a new request with a new Idempotency-Key.
approval_expired409The same request was sent again after its approval request expired.Send the request again with a new Idempotency-Key to create a new approval request. Requests expire after 72 hours.
stale_approval409The approved refund could no longer run as requested.Check the current state (details.approval.failure says what failed), then send a new request with a new Idempotency-Key.

It can also return these common errors: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, idempotency_key_required, idempotency_in_progress, approval_policy_denied, rate_limited, internal_error.