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 raiseWrap 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.updatedwebhook to flip toSUCCEEDED. - 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.