Easy Labs
SDKsPythonExamples

Refunds

Refunds pattern for Python.

An end-to-end recipe for issuing refunds — full and partial — including idempotency, webhook follow-up, and retry handling.

Goal

Refunds are reversals on transfers (see the Refunds resource page). The SDK exposes them as client.transfers.create_refund(...), not a separate client.refunds namespace. This recipe covers:

  • Full refund of a single transfer.
  • Partial refund with metadata for ops tooling.
  • Retry-safe webhook flow that reacts to refund.updated.

Implementation

1. Full refund

import os
from easylabs import Client

client = Client(api_key=os.environ["EASY_API_KEY"])


def full_refund(transfer_id):
    transfer = client.transfers.retrieve(transfer_id)
    return client.transfers.create_refund(
        transfer.id,
        refund_amount=transfer.amount,
        tags={"reason": "customer_request", "kind": "full"},
        idempotency_key=f"refund-{transfer.id}-full",
    )

2. Partial refund with reason tracking

def partial_refund(*, transfer_id, amount_cents, reason, ticket_id):
    return client.transfers.create_refund(
        transfer_id,
        refund_amount=amount_cents,
        tags={
            "reason": reason,
            "ticket_id": ticket_id,
            "kind": "partial",
        },
        idempotency_key=f"refund-{transfer_id}-{ticket_id}",
    )

3. Retry-safe call wrapper

import time
from easylabs import RateLimitError, ServerError, EasyError


def with_retries(fn, attempts=5):
    for i in range(attempts):
        try:
            return fn()
        except RateLimitError as e:
            time.sleep(e.retry_after_seconds or 1)
        except ServerError:
            time.sleep(min(2 ** i, 30))
    return fn()  # last attempt; let it raise

Wrap any of the refund calls in with_retries(...) to survive transient issues without losing idempotency.

4. React to refund webhooks

from easylabs import Webhooks, InvalidRequestError

SECRET = os.environ["EASY_WEBHOOK_SECRET"]


def receive(request):
    try:
        event = Webhooks.construct_event(
            payload=request.body,
            signature=request.headers.get("x-easy-webhook-signature", ""),
            secret=SECRET,
        )
    except InvalidRequestError:
        return ("invalid", 400)

    if event.type == "refund.updated":
        refund = event.data
        if refund.get("state") == "SUCCEEDED":
            mark_refund_completed(refund["id"], refund["amount"])
        elif refund.get("state") == "FAILED":
            alert_ops(refund["id"], refund.get("failure_message"))

    return ("", 204)

Tradeoffs

  • Always pass idempotency_key. A network blip during refund creation that retries without an idempotency key will refund the customer twice — and chargebacks for double refunds are the worst.
  • Don't poll for completion. Refunds may take seconds (cards) to several days (ACH). Wait for the refund.updated webhook to flip to SUCCEEDED.
  • Reverse-then-resell is rarely what you want. If the customer bought the wrong item, refund + create a new order rather than trying to mutate the original.
  • Disputes ≠ refunds. If the cardholder has already opened a chargeback, manage it via client.disputes — issuing a refund on a disputed transfer can leave you double-paying.

On this page