Skip to main content
Case: you need to refund MXN 116 from a succeeded MXN 1,160 payment. The API can record a refund in gigstack, or also request an external refund for an eligible Stripe payment. Choose the effect before submitting.
The record-only flow below was exercised on staging on 2026-10-08 with a test key: a fresh MXN 1,160 payment received an MXN 116 refund record, and a readback confirmed 11,600 cumulative minor units. The Stripe path remains source-reviewed and was not executed with a processor. The handler multiplies the submitted amount by 100; the worked example uses MXN. Confirm processor currency support before adapting it to another currency. A credit note or cancellation is a separate fiscal operation, not proof that money was returned.

Before testing: check refund automations

Recording a refund can trigger published refund Journeys and the team’s automatic refund handling, including actions on associated fiscal documents. A payment’s automation_type: "none" does not disable these separate settings. Test payments match published test Journeys; check those with the account owner before running a test. Stripe test events can also reach configured webhook listeners and send team refund notifications. ignore_emails on the original payment does not suppress every refund notification. Use an approved test account with known webhook destinations and recipients, and a fresh synthetic payment without invoice or receipt associations.

1. Read the original payment and choose the amount

Use Bash with curl and jq; use the quickstart commands to set the base URL and key, and check their environment. PAYMENT_ID must refer to the intended succeeded payment in that team and mode. A payment merely requested or canceled is not eligible.
Review existing refunds and confirm that the selected refund is not already recorded. For this endpoint’s two-decimal convention, the remaining amount in currency units is data.total - (data.total_refunded // 0) / 100. The maximum considers all previous refunds.

Keep these units separate

Do not send 11600 to mean a refund of MXN 116. That would request MXN 11,600. Use decimal-safe arithmetic in your application and serialize refund attempts for the same payment; a client-side remaining-balance check is not a concurrency lock.

2. Record the authorized refund

This example records money you returned through another process. It does not initiate a bank transfer or a Stripe refund. Confirm the payment, amount and reason before running it.
The observed record-only success was HTTP 200. Save data.refund.id. The handler sets its refund record status to succeeded; this is not independent confirmation of a bank or processor settlement. The response’s data.payment.refund_status is fully_refunded when the cumulative amount equals the payment amount; otherwise it is requires_action. That label alone does not mean this partial refund failed.

3. Read back the refund record

Compare the refund ID, amount and reason, and reconcile the cumulative refunded amount with the pre-request snapshot. The refund POST returns a gigstack refund ID, not the Stripe refund ID. Later webhook processing may add Stripe-ID entries to the payment’s refund list; the list does not guarantee a one-to-one mapping to money movements.

If you intend to return money through Stripe

Only after confirming the payment has the correct Stripe PaymentIntent and account, use the same endpoint with external_processor_refund: true. This is a separate choice; do not first run the record-only example and then repeat it to send money. The public payment response does not prove PaymentIntent eligibility. Check the original processor record or ask the account owner. A payment without a PaymentIntent is rejected with 400 and External processor refund is not allowed for this payment. After an authorized processor refund, verify both the gigstack refund and the corresponding refund in the connected Stripe account. The refund POST does not expose the Stripe refund ID or use its final settlement status as a completion check. Do not report money received by the customer solely because gigstack’s record says succeeded. A later Stripe webhook can change the payment status to partial_refunded or refunded and append a processor refund entry alongside the API-created entry. Do not count list entries as separate money movements or sum them to infer the refunded balance. Reconcile the cumulative amount against the corresponding Stripe refund and preserve both identifiers when asking support to investigate a mismatch.

Duplicate attempts, failures, and timeouts

This endpoint accepts reason, amount and external_processor_refund; it does not document an idempotency key. Never apply the payment-registration replay recipe here. A processor request can succeed before the local update fails. After a timeout or 500, compare GET /payments/{id} and the processor history with your pre-request snapshot. Preserve the payment ID, amount, reason, time and any refund ID. Ask the account owner or support to reconcile an ambiguous result before another POST. For 400, inspect the body: the payment must be succeeded, the amount must be at least 0.01, and cumulative refunds cannot exceed the original payment. Correct the identified problem; do not invent a new reference to bypass a duplicate or over-refund concern. See the refund API contract for all response shapes.