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_id must 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 same 404 as one that does not exist.
  • amount_minor is optional. Without it, the refund is what is left of the donation. See Partial and multiple refunds.
  • reason is duplicate, requested_by_donor or other.
  • cancel_recurrence is required: true also cancels the donor's recurring gift before the money goes out, and the cancellation reaches you as enrollment events; false keeps the recurring gift.
  • metadata holds 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.

  1. Before the church is paid. Roster stops the donation, which reads in_review in donation.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 ends succeeded when the donor's bank confirms it.
  2. 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_action while Roster waits for the donor's bank account, pending with awaiting_approval while Roster checks it, processing once the money is sent to the donor's bank, and succeeded when that bank confirms it. Roster then deducts the refund amount from the church's next payouts (see Fees and later payouts).
  3. 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.created for them like for yours: reason is after_cancellation when the money arrived after the donor's recurring gift was cancelled, and other otherwise. They have no metadata.

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_review until 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 gets 409 refund-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_minor is what the donor's bank received from this donation's refunds, counting only refunds that succeeded. A refund still on its way is not counted. Each fund in funds carries its own refunded_minor, and the funds add up to the donation's.
  • When the whole donation is refunded, in one refund or several, status reads reversed and refunded_minor equals amount_minor. A partial refund does not change status.
  • 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.

  1. 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).
  2. Create a refund (POST /v1/refunds) and receive refund.created: it is requires_action, because the test donor has no account yet. Cancel it (POST /v1/refunds/{id}/cancel) and receive refund.updated with canceled.
  3. Create another refund: it is requires_action too.
  4. 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 the test_refund_destination login. The refund moves to pending, awaiting_approval.
  5. 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 processing and succeeded, and donation.updated carries refunded_minor. In production, people at Roster take these steps.
  6. Play the donor's bank sending the money back with POST /sandbox/simulate/refund-returned: the refund returns to requires_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.