Payment API refunds are not one universal synchronous call. Stripe, Adyen, PayPal and Square all support full or partial refunds, but they use different identifiers, status names and webhook events. A reliable integration stores the original payment reference, sends an idempotent refund request, tracks the refund object and waits for a final state before telling an order system that the money is settled.

The practical model has four parts: validate what remains refundable, create a provider-specific request, persist the returned refund ID and reconcile later status changes. The initial API response may confirm only that the provider accepted the request for processing.

Refund the captured payment, not an authorization

Refunds generally operate on money that has already been captured. If an authorization has not been captured, the correct action is usually a cancellation or void instead.

Adyen’s refund documentation says refunds apply after capture and points integrations to a reversal endpoint when the payment’s capture state is uncertain. PayPal’s refund guide similarly tells developers to void an authorized payment that has not been captured.

This distinction should be explicit in an order system. A single “cancel” button may need to choose among canceling an order, voiding an authorization and refunding a capture. Sending every case to a refund endpoint creates avoidable errors and can leave the payment state out of sync with the order.

Full and partial refunds need a remaining balance

All four providers can handle a full refund, and each supports partial refunds in at least its main card-payment flow. The application still needs its own remaining-refundable calculation.

Store the captured amount, currency and the sum of successful or still-pending refund attempts. Before creating another refund, calculate:

remaining refundable = captured amount - completed refunds - active pending refunds

The pending component matters. Two support agents can otherwise request the same remainder while the first refund is still processing.

Adyen allows multiple partial refunds when the total does not exceed the captured amount, while warning that support depends on the payment method. PayPal’s Payments API v2 uses the capture ID and an amount object for a partial refund. Square’s RefundPayment endpoint also accepts all or part of a completed payment. Provider and payment-method limits must be checked at request time rather than assumed from a card-only test.

Make the request retry-safe

A network timeout does not tell a caller whether the provider received a refund request. Retrying with a new request identity can produce a duplicate attempt.

Use the provider’s idempotency mechanism where available and keep the key with the internal refund record. Square’s RefundPayment reference requires an idempotency_key. PayPal recommends idempotency keys in its refund guide. The same internal operation ID should also appear in logs and metadata so a response can be reconciled after a timeout.

Idempotency is one layer. An application should also enforce a database constraint or transaction around the order’s refundable balance. Provider-side protection does not prevent two distinct internal operations from using two distinct keys for the same return.

Treat pending as a real state

A successful HTTP response can still describe a refund that is pending. Square documents PENDING, COMPLETED, FAILED and REJECTED outcomes. Its refund guide says a caller can retrieve the refund status and can subscribe to refund.updated notifications.

Adyen describes its refund process as asynchronous. The request returns a reference, and the integration waits for a REFUND webhook. Even a positive validation result can later be followed by REFUND_FAILED or REFUNDED_REVERSED in exceptional cases.

PayPal exposes refund and capture status values and sends PAYMENT.CAPTURE.REFUNDED for completed capture refunds. Stripe publishes refund.created, refund.updated and refund.failed events; some payment methods complete asynchronously.

An internal state machine can normalize these differences:

  • requested: the application accepted the request;
  • pending: the provider accepted it but has not reached a final result;
  • completed: the provider confirms the refund;
  • failed: the provider says it did not complete;
  • reversed: a previously positive result was later reversed, when the provider exposes that case.

Keep the provider’s raw status alongside the normalized state. That makes audits and support investigations possible without forcing every provider into a lossy label.

Webhooks and polling work together

Webhooks reduce delay, but a refund system should be able to recover when a webhook arrives late, is delivered more than once or never reaches the endpoint.

Verify webhook signatures, store the event ID and process each event idempotently. Then run a reconciliation job that polls refunds left pending beyond an expected interval. Our guide to verifying payment webhook signatures covers the signature layer; the payment webhook retry guide explains replay-safe event processing.

The provider refund ID is the link between request, webhook and poll. Do not rely only on an order number in a free-text reference.

Customer messaging should reflect the state

“Refund submitted” and “refund completed” are different claims. The first can be shown after a provider accepts a request. The second should wait for the provider’s final status.

Even after a provider marks a refund completed, the customer’s bank or payment method can take additional time to display the credit. Keep that customer-facing arrival estimate separate from the API state, and use the payment-method guidance supplied by the provider rather than one universal promise.

For a failed or rejected refund, preserve the reason, release or recalculate the internal refundable balance and route the case to a documented recovery path. Never silently mark the order as refunded because an API request returned 200.