Refunds
A refund returns a donation, in full or in part, to the donor's bank account. You create it with POST /v1/refunds, follow it through refund.created and refund.updated, and read the refunded total on donation.updated. Roster sends the money and asks the donor for the bank account that receives it: you never collect the donor's bank details.
Refunds are available where financial integration is enabled. Today that is the sandbox; in production the refund operations answer 503.
Create a refund
Send the donation's id, as received in donation.updated (the snapshot's id, which is also the event's entity_id):
curl --fail-with-body -X POST "$ROSTER_BASE_URL/v1/refunds" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: refund-occ_8f3a2b1c-ticket-4411" \
-H "Content-Type: application/json" \
--data '{
"donation_id": "occ_8f3a2b1c",
"amount_minor": 3000,
"reason": "requested_by_donor",
"cancel_recurrence": false,
"metadata": { "ticket": "4411" }
}'
donation_idmust be a donation of an enrollment made through your platform. A donation made through another partner, even at a church you share, or on the church's own giving page, gets the same404as one that does not exist.amount_minoris optional. Without it, the refund is what is left of the donation. See Partial and multiple refunds.reasonisduplicate,requested_by_donororother.cancel_recurrenceis required:truealso cancels the donor's recurring gift before the money goes out, and the cancellation reaches you as enrollment events;falsekeeps the recurring gift.metadataholds your own key-value pairs, such as a support ticket number: up to 50 keys, a key up to 40 characters, a value up to 500. Roster returns them as sent. Do not put donor personal data there.
The answer is 201 with the refund. A repeat with the same Idempotency-Key and body returns the same refund with 200 and creates nothing; the key stays with its body for as long as the refund exists (see Retries).
{
"id": "rf_2c9e41a07b5d8f13e6a90c24",
"object": "refund",
"donation_id": "occ_8f3a2b1c",
"church_id": "ch_8f3a2b1c9d4e",
"amount_minor": 3000,
"currency": "USD",
"funds": null,
"status": "pending",
"pending_reason": "awaiting_approval",
"reason": "requested_by_donor",
"failure_reason": null,
"cancel_recurrence": false,
"metadata": { "ticket": "4411" },
"created_at": "2026-10-09T18:02:11.000Z",
"updated_at": "2026-10-09T18:02:11.000Z"
}
Read a refund with GET /v1/refunds/{id}, and every refund of a donation, oldest first, with GET /v1/refunds?donation_id=…. A refund of a donation that is not yours gets 404.
Three moments, seen from outside
Where the money is when you ask decides the path the refund takes.
- Before the church is paid. Roster stops the donation, which reads
in_reviewindonation.updated, and reviews the refund (pending,awaiting_approval). If the donation was already in review at Roster when you asked, the refund waits for that review first (pending,in_review). Once Roster approves, it sends the money to the donor's bank account (processing), and the refund endssucceededwhen the donor's bank confirms it. - After the church is paid. Roster does not wait for the church. The refund goes through the same statuses as one made before the church is paid:
requires_actionwhile Roster waits for the donor's bank account,pendingwithawaiting_approvalwhile Roster checks it,processingonce the money is sent to the donor's bank, andsucceededwhen that bank confirms it. Roster then deducts the refund amount from the church's next payouts (see Fees and later payouts). - Money Roster did not pass on to the church. Roster also creates refunds on its own, for money that reached the donor's account and does not go to the church. You receive
refund.createdfor them like for yours:reasonisafter_cancellationwhen the money arrived after the donor's recurring gift was cancelled, andotherotherwise. They have nometadata.
While the donation is being sent to the church's bank, POST /v1/refunds answers 409 donation-in-flight and creates nothing: repeat it in a few minutes, and the refund takes path 1 or 2.
The donor's bank account
To send the money, Roster needs a bank account from the donor. When the refund is ready to go and the donor has not added one, the refund is requires_action. Roster emails the donor a link to add it on the manage page, and reminds them while the refund waits. You do not need to act: once the donor adds the account, the refund goes back to pending with awaiting_approval and you receive refund.updated. You can tell the donor to look for Roster's email.
If the donor's bank sends the money back, for example because the account cannot receive it, the refund returns to requires_action, even after succeeded, and Roster asks the donor for another account.
Status and pending reason
status |
pending_reason |
What it means |
|---|---|---|
pending |
in_review |
The donation is in review at Roster; the refund waits for that review to end. |
pending |
awaiting_approval |
Roster is checking the refund before sending the money. |
pending |
processing |
The money was sent to the donor's bank; waiting for its confirmation. |
requires_action |
null |
Roster is waiting for the donor's bank account. |
succeeded |
null |
The donor's bank received the money. |
failed |
null |
The refund ended without sending the money; see failure_reason. |
canceled |
null |
Canceled by you or by Roster before the money was sent. |
failure_reason is not_refundable when Roster cannot return the donation through a refund and handles it directly, and bank_error when the bank could not send the money. A failed or canceled refund frees its amount, which you can refund again. Treat a status or reason you do not recognize as "something changed; read the refund again".
Partial and multiple refunds
What is left to refund is the donation's amount minus its refunds that have not failed or been canceled: a refund holds its amount from the moment it is created, not only once it succeeds. amount_minor can be anything up to what is left; above it, the request gets 422 amount-exceeds-refundable and no refund is created. A donation can have several refunds, created one after another.
When the donation is split among funds, each refund records in funds, once, the part taken from each fund. Each part is in proportion to what is left in that fund, rounded so the parts add up to the refund: the cents left by rounding go to the funds with the largest remainders, and a tie goes to the fund listed first. No fund returns more than it has, and a refund of what is left empties every fund. For a USD 100.00 donation split USD 66.67 and USD 33.33, a USD 30.00 refund takes USD 20.00 and USD 10.00, and a refund of the remaining USD 70.00 takes USD 46.67 and USD 23.33.
Cancel a refund
POST /v1/refunds/{id}/cancel, with an Idempotency-Key and no body, cancels a refund that Roster has never approved for sending: one that is pending (except processing) or requires_action, before its first approval. Once Roster approved it, even if the donor's bank later sent the money back, or once the refund ended, the request gets 409 refund-not-cancelable and nothing changes.
A refund of a donation the church was not paid yet waits for Roster's review first, and that review decides what can still be canceled:
- Canceled before the review, the donation stays
in_reviewuntil Roster releases it, and then goes on its way to the church. - After the review, a refund of part of the donation can no longer be canceled, even while it is
requires_action: the rest of the donation is on its way to the church, and the church's part was set at the review. The request gets409refund-not-cancelable. - After the review, when the refunds cover the whole donation, one can still be canceled until Roster approves it for sending. Its money is not passed on to the church, and the donation reads
failed.
Webhooks and the financial feed
Subscribe your webhook endpoint to refund.created and refund.updated. Both carry the whole refund in payload, the same object GET /v1/refunds/{id} returns; entity_id is the refund ID, and, like every financial event, they carry sequence and are also in GET /v1/financial_events. refund.created arrives once per refund; refund.updated arrives on every later change, a failure included (status: failed). They go to the partner the donation came through.
Deliveries can arrive out of order. Store each event by event_id, and read GET /v1/refunds/{id} when you need the refund's current state rather than the state in the event.
What the donation shows
donation.updated carries the refunded total:
refunded_minoris what the donor's bank received from this donation's refunds, counting only refunds thatsucceeded. A refund still on its way is not counted. Each fund infundscarries its ownrefunded_minor, and the funds add up to the donation's.- When the whole donation is refunded, in one refund or several,
statusreadsreversedandrefunded_minorequalsamount_minor. A partial refund does not changestatus. - While a refund of a donation the church was not paid yet waits for Roster's review, the donation reads
in_review.
| Moment | Refund | Donation |
|---|---|---|
| Before the church is paid, until the bank confirms | pending or requires_action |
in_review |
| After the church is paid, until the bank confirms | pending or requires_action |
Unchanged |
| Refunded in full (one refund or the sum of several) | succeeded |
reversed, with refunded_minor |
| Refunded in part | succeeded |
Unchanged, with refunded_minor |
| Canceled before Roster's review | canceled |
in_review until Roster releases it, then on its way to the church |
| Canceled after Roster's review (refunds covering the whole donation only) | canceled |
failed |
Fees and later payouts
When the church was already paid, Roster refunds the donor without waiting for the church and deducts the whole refund amount from the church's next payouts. Roster's fee and your commission on the refunded donation are not returned. Before the church is paid, fees and your commission apply only to what still goes to the church.
The deduction is between Roster and the church, and the church sees it in its own panel. On the later donations whose payouts cover it, nothing changes for you: the church entry in transfers is still the church's share, because the part applied to the refund counts as paid to the church, and fee and church still add up to the donation's amount_minor. Roster's fee and your commission on those later donations are the usual ones, on the whole donation. The refunded donation itself does not change.
Test in the sandbox
The test donor adds the account a refund goes to by typing test bank numbers (choose Manual, then routing number 110000000 and account number 1111222233331111) or by signing in to a test bank. GET /sandbox/fixtures returns both in test_refund_destination: the numbers in its rule, the test bank login in username and passcode.
- Have a donation the church was already paid: run the deposit step, wait for
donation.updated, and let the weekday cutoff pay the church (the sandbox pays it at 16:00 ET). - Create a refund (
POST /v1/refunds) and receiverefund.created: it isrequires_action, because the test donor has no account yet. Cancel it (POST /v1/refunds/{id}/cancel) and receiverefund.updatedwithcanceled. - Create another refund: it is
requires_actiontoo. - As the test donor, open the manage page (from Roster's email to the donor, or with a manage session you create,
POST /v1/manage_sessions), add a bank account: choose Manual and type the test numbers, or choose any bank in the test list and sign in with thetest_refund_destinationlogin. The refund moves topending,awaiting_approval. - The sandbox has no Roster team, so Roster's steps are automatic there: a refund with the donor's account is approved for sending at the next weekday cutoff (16:00 ET), the same cutoff that pays churches. It then moves to
processingandsucceeded, anddonation.updatedcarriesrefunded_minor. In production, people at Roster take these steps. - Play the donor's bank sending the money back with
POST /sandbox/simulate/refund-returned: the refund returns torequires_action.
A refund on a donation the church was not paid yet is also reviewed automatically in the sandbox: it moves straight to requires_action, or to awaiting_approval when the test donor already has an account. Because that review comes right after the refund is created, a refund of part of such a donation cannot be canceled in the sandbox: try canceling on a donation the church was already paid, as in step 2.
The refund simulator, called again after it changed the refund, answers 400 naming the refund's status: read the refund before retrying.