Skip to main content
Owner-only, card top-ups only. Every control this guide walks is visible to the organization’s owner role only, and the flow refunds card top-ups to the original card. Cryptocurrency payments are final from the dashboard and are settled as manual payouts; see Cryptocurrency refunds instead.

Refunding unused card top-up credits: the owner’s self-serve walkthrough

When a card top-up still holds unused credits, you can send some or all of the unused portion back to the card that funded it, yourself, from the Billing page. No support ticket, no waiting on an operator. This guide walks the whole loop in the console: checking that a refund will succeed before you request one, making the request, reading a rejection when it does not succeed, and tracking the money afterwards. The model behind this page (lot provenance, the eligibility window, FIFO attribution, two-leg settlement) is documented on Credit refund lifecycle; the endpoint surface map is Billing overview, §8 Refunds. This guide is the task-level companion to both: what to click, in what order, and what each response means.

1. Which refund page applies to you

Two refund flows exist in the Billing area, and they never overlap: A third mechanism reverses money without ever touching your card: when a message fails terminally at the carrier, its usage charge is reversed inside the wallet automatically. Those are usage-side corrections, not top-up refunds, and there is nothing for you to request. See Channel charge trigger model. If your top-up was paid by card, continue here.

2. Where the controls live

Open Billing in the dashboard. Two surfaces carry this flow, and both render for owners only (admins and other roles see neither, matching the API, which answers them with 403):
  • Transaction history lists every ledger row on your wallet. A card top-up that is still refundable shows a Refund button on its row. If the button is absent, the row is not eligible; §3 says why.
  • Refund history lists every refund request you have submitted, with its live status. It is empty until your first request (“No refund requests on file”).
The same two surfaces exist over the API: GET /api/v1/billing/transactions for the lots, POST /api/v1/billing/refunds to request, and GET /api/v1/billing/refunds to track.

3. Eligibility pre-flight

Before you click Refund, confirm three things about the lot in transaction history. The dashboard applies the same checks when it decides whether to show the button, so this is also how you read a missing button.
  1. It is a card top-up. Only credit_purchase rows, card top-ups that settled through Stripe checkout, are refundable. Bonus credits, trial grants, and crypto top-ups carry no original card charge to refund against, so they never show the button.
  2. It is within 30 days. A lot is refundable for 30 days from its purchase timestamp, which the row shows in your timezone. The API applies a further 24-hour grace past that boundary, and the reason is clock agreement, not leniency: the dashboard computes the eligibility badge in your browser while the API checks its own clock when the request arrives, and the grace keeps the two from disagreeing at the boundary. Treat 30 days as your deadline; a request past the window fails with a 422 (§6).
  3. It still holds unused credits. The refundable amount is the unused portion (§4). A fully spent lot shows nothing to refund: the confirm dialog says “Nothing refundable on this top-up — the credits have already been spent” and disables the confirm button.
One more rule to plan around: a lot settles at most one refund. Once a refund on a lot processes, the lot is stamped as refunded and closes, even if you left part of it unrefunded. Decide the amount you want in a single request; a second request on the same lot returns 409.

4. The partial-refund math

A lot refunds only its unused portion:
  • original is the top-up amount.
  • used is the usage spend attributed to this lot: every debit posted between this top-up and your next card top-up counts against it, oldest lot first, capped at the lot’s own size.
  • already claimed counts this lot’s pending, approved, and processed refund requests, so an in-flight request holds those cents and a second request cannot double-refund them. Failed and rejected requests count as zero, because no money moved.
Worked example, one lot. A $100.00 top-up that sent $40.00 of usage before your next top-up refunds at most $60.00. The confirm dialog states this as **Refundable now:** $60.00, and requesting $80.00 over the API fails with details.reason: "refund_exceeds_refundable" and refundable_cents: 6000, the exact figure to retry with. Worked example, two lots, oldest first. A $100.00 top-up on Sep 1 and a $50.00 top-up on Sep 20, with $90.00 of usage between Sep 1 and Sep 20 and $20.00 after Sep 20: Spend is always attributed to the oldest open lot first, which is why an older lot’s refundable amount shrinks as you use the platform while a newer lot stays mostly intact. The dialog derives its preview from the rows visible in transaction history; the API re-validates against the complete ledger on submit, so if the two ever disagree you get a clear 422 carrying the authoritative refundable_cents, never an over-refund.

5. Requesting the refund in the console

  1. On Billing, find the original top-up in transaction history (§3) and click Refund.
  2. The confirm dialog shows the lot’s original amount and Refundable now: , the most you can request for this lot right now. Confirming refunds exactly that amount. To refund less than the maximum, keep the credits in the wallet or issue the partial amount over the API with amount_cents at or below refundable_cents:
  3. Confirm with Refund (or back out with Keep credits). The button shows Refunding… while the request is submitted.
  4. The row appears in Refund history immediately with status Pending; your wallet balance updates as soon as the refund processes.
Two behaviors make the request safe to retry: repeating the POST for a lot that already has an active request returns the existing request with "duplicate": true instead of creating a second one, and the card leg is keyed per request, so a retried settlement cannot issue two card refunds. After a network error, resubmitting is the correct move. The dialog also states what to expect on the card side: the refund processes through Stripe and can take 5-10 business days to appear on your card statement, while the wallet side updates immediately.

6. When a request is refused: reading 422 and 409

Every refusal names its reason in error.details.reason, and each has a fix: Refusals that happen after acceptance, during settlement (missing purchase provenance on a legacy lot, a stored-currency conversion the API will not guess, Stripe declining), flip the request to Failed instead and record a plain-language failure_reason; §7 covers reading it.

7. Tracking: Refund history and the API

Refund history on the Billing page lists each request as a row: the amount, a status badge, the lot it refunds (truncated, full id on hover), the request time in your timezone, and, once terminal, the processed time. A processed row also links its Stripe refund id so you can cross-check the disposition and statement timing in your Stripe Dashboard. Most requests that pass validation settle automatically against Stripe without human review, typically well inside a day; the card side then follows Stripe’s 5-10 business-day statement timing. If a request shows Failed with a reason you cannot act on, or stays in flight past a day, contact support with the row at hand; the failure text tells them which leg stopped. Over the API, poll the list or a single request:
GET /api/v1/billing/refunds/:id returns the same row plus failure_reason on failed requests.

8. What settlement does to your account

Settlement runs as two legs, and each lands on a different surface:
  • The card leg. The refund is issued against the original charge, in the charge’s currency. The wallet is USD; if your card was charged in another currency, the refund converts back at the exchange rate stored on the lot at purchase time, so the card receives the original-currency amount that corresponds to the refunded cents. See Wallet currency conversion for the forward conversion at top-up.
  • The wallet leg. When Stripe confirms, the request flips to Processed and the refunded cents are removed from your wallet: the balance is debited by exactly the refunded amount and the ledger receives a credit_refunded adjustment row referencing the request. In transaction history that row renders as Refund, and on the monthly statement it appears as an adjustment line offsetting the original top-up. See Billing documents model for how ledger rows become statements.
If you spent the credits down after requesting, the card refund still completes and the wallet debit removes whatever remains, never taking the balance below zero; the ledger keeps the full adjustment row either way. Plan for the balance drop. A refund reduces your available balance exactly like spend does, without being spend, and it can drop the balance below your auto-top-up threshold, which then fires a recharge if auto-top-up is enabled, or leaves sends closer to the pause threshold if it is not. Check the threshold on the Billing page before refunding a large lot ahead of a campaign.

9. What a refund does not do

  • It does not re-price usage you already ran. Usage stays billed and on your statements; the refund only returns the unused portion of a top-up to the card. There is no retroactive credit against invoiced usage.
  • It does not reverse cryptocurrency payments. Crypto is final from the dashboard and follows the operator-run flow in Cryptocurrency refunds.
  • It does not reopen a settled lot. One refund per lot; whatever you leave unrefunded stays in the wallet as usable credit.
  • It is not silent. The request and its outcome are written to your organization’s audit trail, so a refund is always attributable to a person and a time.