Refunds
A refund returns money to a buyer for a previous successful payment — a completed checkout session or a paid invoice. The Python SDK wraps the three refund endpoints behind plugipay.refunds. For wire format and the full lifecycle (which depends heavily on the upstream payment method), see API → Refunds.
Namespace
plug.refunds # _Refunds
Methods
create
plug.refunds.create(
*,
source_type: str,
source_id: str,
amount: int | None = None,
reason: str | None = None,
) -> Refund
Creates a refund against a payment source. source_type is "checkout_session" or "invoice". source_id is the underlying cs_ or inv_ id. If amount is omitted, refunds the full remaining refundable amount; pass an integer minor-unit value for partial refunds. reason is a free-form string surfaced to the buyer (where supported by the provider) and recorded for audit. The SDK auto-generates an idempotency key.
# Full refund
refund = plug.refunds.create(
source_type="checkout_session",
source_id="cs_01HX...",
reason="customer_request",
)
print(refund["id"]) # "rfd_01HX..."
print(refund["status"]) # "pending" — async on most providers
# Partial refund — IDR 50,000.00 off a 149,000.00 charge
partial = plug.refunds.create(
source_type="invoice",
source_id="inv_01HX...",
amount=50_000_00,
reason="adjustment",
)
get
plug.refunds.get(refund_id: str) -> Refund
Polls a refund's current state. Refunds move pending → succeeded (or failed) — the time to settlement is provider-dependent (cards: minutes; bank transfers: days). Subscribe to plugipay.refund.succeeded.v1 / plugipay.refund.failed.v1 instead of polling.
refund = plug.refunds.get("rfd_01HX...")
if refund["status"] == "failed":
print(refund.get("failureReason"))
list
plug.refunds.list(
*,
limit: int | None = None,
cursor: str | None = None,
status: str | None = None,
source_id: str | None = None,
) -> PageResult[Refund]
Lists refunds. Filter by status ("pending" | "succeeded" | "failed") or source_id to see all refunds against one payment.
# Every refund tied to a session
page = plug.refunds.list(source_id="cs_01HX...")
for r in page.data:
print(r["id"], r["amount"], r["status"])
# Failed refunds — usually need a manual nudge
cursor = None
failed = []
while True:
page = plug.refunds.list(limit=100, status="failed", cursor=cursor)
failed.extend(page.data)
if not page.has_more:
break
cursor = page.cursor
Refunds don't carry an
updateorcancel. Once submitted, a refund is final — it either settles or fails. There is no API to "undo" a refund mid-flight. If a refund fails, you can callcreateagain with the same parameters; the SDK's auto idempotency key means an accidental double-create on the same call is safe, but a deliberate retry across calls is not deduped (you'd issue a second refund).
Types
from plugipay import Refund, PageResult
Refund is a Resource subclass. Likely-read fields:
refund["id"]—rfd_+ ULID.refund["status"]—"pending" | "succeeded" | "failed".refund["sourceType"],refund["sourceId"].refund["amount"],refund["currency"].refund.get("reason")— your free-form string.refund.get("failureReason")— provider's reason ifstatus == "failed".refund["createdAt"],refund.get("settledAt").
Full reference at API → Refunds.
Common patterns
Idempotent partial refunds. Use plug.request with your own key so duplicate refund attempts (e.g. from a retried webhook handler) only execute once:
plug.request(
method="POST",
path="/api/v1/refunds",
body={
"sourceType": "invoice",
"sourceId": invoice_id,
"amount": 50_000_00,
"reason": "adjustment",
},
idempotency_key=f"adjust:{invoice_id}:50000",
)
Wait-for-terminal helper. Poll-with-backoff for synchronous-ish flows where you can't wait for the webhook:
import time
from plugipay import PlugipayError
def wait_for_refund(plug, refund_id, *, timeout_sec=60):
deadline = time.time() + timeout_sec
delay = 1.0
while time.time() < deadline:
refund = plug.refunds.get(refund_id)
if refund["status"] in ("succeeded", "failed"):
return refund
time.sleep(delay)
delay = min(delay * 1.5, 8.0)
raise TimeoutError(f"refund {refund_id} did not settle in {timeout_sec}s")
Refund-then-recreate flow. Wrong amount charged? Refund full, then create a new checkout session for the correct amount:
plug.refunds.create(source_type="checkout_session", source_id=bad_session_id)
new = plug.checkout_sessions.create(
amount=corrected_amount,
currency="IDR",
methods=["card"],
success_url="...",
cancel_url="...",
customer_id=customer_id,
metadata={"corrects": bad_session_id},
)
Reconcile failed refunds nightly. A failed refund didn't return the money — your customer is still waiting. Cron a sweep:
page = plug.refunds.list(limit=100, status="failed")
for r in page.data:
notify_support(
refund_id=r["id"],
reason=r.get("failureReason", "unknown"),
amount=r["amount"],
)
Errors
err.status |
err.code |
Cause |
|---|---|---|
400 |
validation_error |
Bad source_type, missing source_id, negative amount. |
404 |
not_found |
source_id doesn't exist or refund id missing. |
409 |
not_refundable |
Source isn't in a refundable state (open invoice, expired session, etc.). |
409 |
amount_exceeds_refundable |
Requested amount > remaining refundable balance. |
409 |
idempotency_key_reused |
Same Idempotency-Key against a different body. |
422 |
provider_unsupported |
Underlying payment method doesn't support refunds (some bank transfer types). |
All raise PlugipayError. See Errors.
Next
- API → Refunds — full lifecycle by provider, including settlement times.
- Checkout sessions — the most common refund source.
- Invoices — the other common refund source.
- Webhooks — subscribe to
refund.*events instead of polling.