Sandbox certification
This script proves four enrollment lifecycle scenarios in the sandbox. The portal indicators record events in your feed; they do not prove that your receiver handled them, financial settlement, or production readiness.
The examples use the two variables from the quickstart: ROSTER_KEY holds your
sk_test_ key, read from your secret manager, and ROSTER_BASE_URL is
https://api.sandbox.rostergiving.com. Each step says which IDs to save for the steps after it.
The recommended API checks in your portal track four scenarios, and they light up from your event feed — never from a checkbox. They help you test; they are not a gate: Roster releases production separately. Sections 0–4 below set up and prove the first one; sections 5, 6 and 7 are the other three.
| # | Scenario | Lights up on |
|---|---|---|
| 1 | An enrollment that activates | enrollment.activated |
| 2 | A donation that stops on a job change | enrollment.paused with reason: "payroll_connection_broken" |
| 3 | A value change | enrollment.updated |
| 4 | A cancellation | enrollment.cancelled |
Sandbox simulation catalog
Run simulations directly through the API or discover the same operations through
the API MCP. There is no separate Simulators tab. All nine
operations below are sandbox-only POST requests under /sandbox/simulate/.
Use the API reference for every accepted field and option.
| Operation | Prerequisites | Expected result and verification |
|---|---|---|
payroll-connected |
A pending enrollment at the paycheck step, or a paused one reconnecting | Activation; verify enrollment.activated and your receiver. A paused enrollment reconnects instead: expected_event is null. Example. |
connection-broken |
An active test enrollment | Pause with the payroll-connection reason; verify enrollment.paused. Example. |
payroll-disconnected |
A cancellation with its payroll removal open | Vendor confirms removal; verify enrollment.cancelled. Example. |
account-closure |
A cancelled enrollment (enrollment.cancelled received) |
Simulates the bank closing the donor's sub-account; no event is sent and the enrollment keeps reading cancelled. Example. |
deposit |
An enrollment whose donor finished the hosted flow, in an eligible church | Observe the donation and later transfer updates via donation.updated and the financial feed. The transfer waits for the first weekday 4:00 p.m. America/New_York cutoff at least 120 minutes after bank_ts. Example. |
late-return |
An executed transfer for the specified donation | Observe the returned leg via donation.updated and the financial feed. Example. |
kyc |
A test donor held in review (number 0190–0199) |
Simulates identity-check outcome; inspect the hosted journey. Passing this check alone does not activate an enrollment or approve a bank account. Example. |
refund-returned |
A refund sent to the donor's bank (processing or succeeded) |
The donor's bank sends it back; the refund returns to requires_action and refund.updated follows. Refunds. |
Every simulator except kyc checks its prerequisites before sending anything and refuses
with 400 naming what is missing. The kyc simulator checks that the enrollment exists and
is yours, and refuses with 400 a donor whose identity check already has a final outcome;
beyond that, its 202 does not prove the donor was waiting on identity: check the hosted
journey afterwards. The three payroll simulators also return
expected_event: the event to wait for on your webhook and feed, or null when the
simulated outcome deliberately emits none (a reconnection, the degraded and active
statuses, a removal that fails).
An accepted request is not proof that asynchronous processing finished. Register
and verify your webhook first, inspect the event or financial feed, and check the
result in your own software. Use a new idempotency key for a new intended test;
reuse the same key and body when retrying an uncertain request — the same key with
a different body gets 409. A deposit retried with the same key and body finds the
same deposit (same trace) instead of crediting again. The other simulators run again
on a retry while what they check still holds. A payroll simulator that returned an expected_event moves the enrollment on,
and once it has, a retry answers 400 naming the state it reached; one that returned
expected_event: null left the enrollment's status as it was, so a retry runs it again
and answers 202. The kyc simulator answers 400 once the donor's identity check has a
final outcome, even before the first outcome reaches the hosted flow; account-closure, once
the account is closed; late-return, once the leg was returned; and the refund
simulator, once the refund moved on, naming its status. Read the enrollment before retrying. Do not send real
donor details. Changing an amount and requesting cancellation remain actions in
the authenticated sandbox donor flow, not arbitrary event-injection operations.
Prerequisites
Have your sandbox key, your sites registered with PUT /v1/allowed_origins, a public HTTPS webhook
receiver and a synthetic church that is ready for donors (see 1b).
Test churches and test donors are self-serve: the values below are all you need, and nothing has
to come from Roster for any scenario in this guide. GET /sandbox/fixtures returns the same values
in one read, so a script or an AI assistant using the API MCP can look them up instead of guessing.
If an AI assistant drives this script through the API MCP, run steps 0 and 2 from
your backend or a terminal instead: their responses carry a webhook signing secret and a one-time
donor link, which must not stay in a chat transcript (see
What to run through the MCP). The other steps can run
through the MCP.
Scenarios 2 and 4 leave their enrollment paused or cancelled, a paused enrollment refuses a
value change, and a deposit belongs on an active one. So each step below names the
enrollment it uses: one for scenarios 1, 3 and 4, one for scenario 2, one for the deposit and
one for the identity check. Plan on four test donors.
Test donors
In the sandbox, anyone on your team can be a donor in the hosted flow:
| Field | Use |
|---|---|
| Mobile phone | A number from +1 202-555-0100 to +1 202-555-0199. The last four digits set the identity outcome |
| Date of birth | Any date for an adult. The same number with another date is another donor. Test numbers are shared by every sandbox account, so pick a date of your own: the same number and date is the same test donor for every partner |
| Code | 000000. No text message is sent |
| Identity | Passes from 0100 to 0189. From 0190 to 0199 it is held in review: see 11 |
An address on a domain that receives mail. A domain that does not exist, such as a typo (gmial.com) or a reserved test domain (.test), takes the donor back to review to check the email, as in production; nothing goes to identity verification |
The flow shows a fictional name and address for the donor to confirm, such as Jordan Sampleton or
Avery Testwell. Each test donor keeps the same one. That confirmed name is the one Roster stores,
so it is the name your API reads and your portal shows. The given_name and family_name you send
in donor are used only when verification cannot fill in the name, which never happens with test
numbers (see Pre-fill what you already know).
Test numbers are limited per number, not per network, so a QA team can run many test donors
from one office: five codes an hour and ten a day for each number. The hundred test numbers are
shared by every sandbox workspace, so other teams' tests count toward the same limits. Past a
limit no code is accepted for that number until the hour (or the day) turns, and the flow asks
the donor to wait; switch to another test number. Any other number follows the production rules: from the
fifth new donor on one network in 24 hours, donors are held for review, so do not use your own
phones for bulk testing. Send the number in donor_phone when you open the session and the
flow starts with it filled in.
The hosted sandbox flow shows the same screens as production: the same
agreements and the same recurring authorization, which stay unchecked until the
test donor acts, and the photo of an ID when identity verification asks for
one. Only the data is fictional. Test numbers keep the fictional identity and
the code 000000 described above.
The payroll connection is the payroll provider's own window, opened inside the
flow in the provider's test mode: it accepts only the provider's test users,
never a real payroll account. Search for a payroll and sign in with a test user
that the window indicates. When the window
closes, save the gift on Almost done (payroll-confirm); the enrollment
activates when the payroll's confirmation reaches Roster, as in production. For
automated tests that do not drive the payroll window, the sandbox simulators
below play the payroll provider instead (see
4 Activate the enrollment).
Test churches
A church becomes ready for donors once Roster verifies its legal details and its receiving account. In the sandbox, these values decide the outcome:
| You send | What happens |
|---|---|
EIN 00-0000001 to 00-0000093 |
Verified within seconds. The same EIN can be used for every test church's verification request |
EIN 00-0000094 to 00-0000099 |
Held for a manual check, so payout_ready stays false. Use it to test your waiting state |
| Any other EIN | Looked up in the IRS public list as in production. One that is not there waits for a manual check |
incorporation_date less than 6 months ago |
Held for a manual check, whatever the EIN |
| Routing number | Any valid ABA routing number, for example 021000021 |
Account number ending in 00 |
rejected: the account is invalid. Send a corrected account |
Account number ending in 11 or 22 |
in_review: ownership could not be confirmed, so payout_ready stays false |
| Any other account number (4–17 digits) | verified |
Nobody runs manual checks in the sandbox, so a church held for one stays not ready until you
correct what held it. Send its legal details again with a new Idempotency-Key, an EIN from
00-0000001 to 00-0000093 and an incorporation_date at least 6 months old; for an account in
review, register an account number that does not end in 00, 11 or 22 through
POST /v1/churches/{id}/bank-account, also with a new key. The church becomes ready within seconds. A church that is already verified
answers 409 (church-already-verified): treat it as done.
Send a test EIN only in the verification request, never in ein
when you create or import churches: there an EIN identifies the church across platforms, so one
shared test EIN would turn every church that uses it into a single church. POST /v1/churches
refuses a test EIN with 400. POST /v1/churches/import answers 200, returns that row as
rejected with the reason in detail, and imports the other rows.
0 Register a webhook endpoint
curl -X POST "$ROSTER_BASE_URL/v1/webhook_endpoints" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: setup-webhook-1" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/roster/webhooks",
"event_types": [
"church.onboarded",
"enrollment.created",
"enrollment.activated",
"enrollment.paused",
"enrollment.resumed",
"enrollment.updated",
"enrollment.cancel_requested",
"enrollment.cancelled",
"donation.updated",
"donor.updated",
"refund.created",
"refund.updated"
]
}'
The six enrollment types other than enrollment.resumed cover the four scenarios.
Subscribe to fewer and the bar still lights up — it reads the feed, not your
endpoint — but you will be certifying a receiver that never saw them. The other
six are what the rest of this guide asks your receiver to handle:
church.onboarded tells you a church is ready where it is sent (1b),
enrollment.resumed ends scenario 2 when the donor chooses Resume now
(reconnection alone does not emit it), and donation.updated and donor.updated
carry the deposit, the returned transfer and the donor details for the
financial acceptance, and refund.created
and refund.updated follow each refund.
The API cannot change an endpoint's event types after it is created (the portal's Webhooks page can, and keeps the secret), and a partner has at most three active endpoints, so subscribe to all of them now. The full list is in the event catalog.
Send this request from your backend or a terminal, never through an AI assistant or the API
MCP: the response carries the endpoint's signing secret, once, and a chat transcript would
keep it (see Registering an endpoint). The test delivery
below carries no secret and can run anywhere.
Store the secret from the response — it is shown only once — and save its id
as ENDPOINT_ID. Then prove your receiver works (signature check included) with a
test delivery:
curl -X POST "$ROSTER_BASE_URL/v1/webhook_endpoints/$ENDPOINT_ID/test" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: test-webhook-1"
{"delivered": true} means your endpoint answered 2xx to a signed envelope.
1 Import your churches
curl -X POST "$ROSTER_BASE_URL/v1/churches/import" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: import-batch-1" \
-H "Content-Type: application/json" \
-d '{"churches": [{"name": "First Baptist"}, {"name": "Grace Chapel"}]}'
The response has a result per item — an invalid row never fails the
batch. Keep the returned church ids, and save the one you test with
(results[0].church.id) as CHURCH_ID.
1b Verify the test church
Donors can start paycheck giving only after Roster verifies the church. Until then, the hosted flow tells the donor that paycheck giving isn't enabled for this church yet. The sandbox works the same way, so verify the church before opening a donor session. Submit its legal details with a test EIN:
curl -X POST "$ROSTER_BASE_URL/v1/churches/$CHURCH_ID/verification" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: verify-church-1" \
-H "Content-Type: application/json" \
-d '{
"ein": "00-0000001",
"account_holder_type": "organization",
"address_type": "street",
"incorporation_date": "2010-01-01",
"representative_name": "Test Representative"
}'
Then register its receiving account with a test account:
curl -X POST "$ROSTER_BASE_URL/v1/churches/$CHURCH_ID/bank-account" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: church-account-1" \
-H "Content-Type: application/json" \
-d '{"routing_number": "021000021", "account_number": "000123456789"}'
The verification returns 202 and is processed asynchronously. Before opening the
donor session, read GET /v1/churches/{id} until payout_ready is true: on a new
church, that happens once Roster has verified the church and its receiving account.
Where the API Reference says church.onboarded is sent, it arrives on your webhook at
that moment, once per church. That is the moment to turn on paycheck giving for that
church in your product. A church that later stops being ready sends no event.
Leave the giving button on when payout_ready later turns false: the hosted flow itself
tells new donors when the church cannot take new gifts. Show the church as in review, with no reason; see
Church status after verification.
The test values above pass within seconds; if readiness stays false with them, write
to support@rostergiving.com with the church ID and the request_id from the 202
(chvr_…).
2 Open a donor session
curl -X POST "$ROSTER_BASE_URL/v1/donor_sessions" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: donor-jane-1" \
-H "Content-Type: application/json" \
-d '{
"church_id": "'$CHURCH_ID'",
"donor_phone": "+12025550100",
"mode": "pct",
"value": 10
}'
The 201 response carries a url — the hosted flow. It is returned once
and expires in 30 minutes. Save the session id as SESSION_ID. Create the session from your
backend, like step 0: the url is a one-time link and stays out of AI conversations. Put the
attempt in the Idempotency-Key (donor-jane-1, then donor-jane-2 for a new link): repeating
a key answers 200 with the same session and no url, and that link cannot be recovered (see
Treat the URL as a one-time handoff).
3 Complete the flow as a test donor
Open the url with Roster.open on one of your registered sites
(GET /v1/allowed_origins lists them; see
Open the flow on your page) and
walk through it as a test donor: the test number, any adult date of birth,
code 000000, confirm the fictional identity, then accept the agreements and the
recurring authorization. On the authorization screen the checkbox and the button stay disabled
while Roster prepares the test donor's account (Preparing your account…): wait for them to
be enabled. It takes about a minute. A script finds every screen and control, and what to wait
for, in Automated browser tests. When the enrollment is
created you will receive enrollment.created on your webhook.
Then read the session; once it is completed it names the enrollment it created:
curl "$ROSTER_BASE_URL/v1/donor_sessions/$SESSION_ID" \
-H "Authorization: Bearer $ROSTER_KEY"
Save its enrollment_id as ENROLLMENT_ID. The entity_id of enrollment.created
is the same ID, but the event does not name the session, so with several test donors
at once read the session instead. Never guess an enrollment ID.
4 Activate the enrollment
In production, activation happens when the payroll provider confirms the direct-deposit switch. In the sandbox there are two ways to get there:
-
By hand, in the flow. After the recurring authorization, the flow opens
the payroll provider's window in its test mode. Connect the payroll as
described in Test donors and save the gift. The flow shows
You're all set, and your page's
onCompleteruns once the gift is active. -
From your script or CI, without a browser at the payroll step. Once the
donor reaches the payroll screen (
payroll-signin), trigger the payroll provider's confirmation yourself — through the same pipeline production uses:
curl -X POST "$ROSTER_BASE_URL/sandbox/simulate/payroll-connected" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: activate-1" \
-H "Content-Type: application/json" \
-d '{"enrollment_id": "'$ENROLLMENT_ID'", "church_id": "'$CHURCH_ID'"}'
The response carries "expected_event": "enrollment.activated", and the flow
moves on to its last screen by itself. If the donor has not
reached the paycheck step yet — still in identity verification or the agreements, or held
with the account in review — the call is refused with 400 and nothing is sent, so
finish the flow first. The payroll screen can appear a moment before the payroll
session behind it is ready, so the same 400 can come right after the donor
reaches payroll-signin: wait a few seconds and send the same request again.
enrollment.activated arriving on your webhook proves activation — it is the signal your production integration will live on. That is
scenario 1 done.
5 Scenario 2: the donation that stops on a job change
The donor changes jobs and the payroll deduction dies with the old employer. This is the case where a donor believes they are giving and are not, so it is the one your product most needs to handle — and the one you are least likely to hit by accident while testing.
Run it on a second enrollment, because a paused one refuses the value change of
scenario 3: repeat steps 2–4 with another test number and new idempotency keys,
save the new enrollment as PAUSE_ENROLLMENT_ID, and send it in step 4 in place
of ENROLLMENT_ID.
In production the payroll provider reports it. In the sandbox you play the provider, through the same edge:
curl -X POST "$ROSTER_BASE_URL/sandbox/simulate/connection-broken" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: broken-1" \
-H "Content-Type: application/json" \
-d '{"enrollment_id": "'$PAUSE_ENROLLMENT_ID'", "church_id": "'$CHURCH_ID'"}'
You receive enrollment.paused with reason: "payroll_connection_broken"
— the same event type the donor's own pause produces, so branch on the
reason, never on the type alone. The enrollment leaves the settlement batch
immediately and a 60-day reconnection grace starts; no money moves for it
until the donor comes back. Resumption is two donor actions in the hosted
flow, not one: reconnect the paycheck (in the sandbox, through the payroll
provider's test window, or with POST /sandbox/simulate/payroll-connected
playing the provider),
and then Resume now. Only that second step emits enrollment.resumed and
returns the enrollment to active; a reconnected enrollment that was never
resumed stays paused. If the grace ends without a reconnection,
GET /v1/enrollments/{id} reads terminated; today no event announces that
ending, so read the enrollment to see it.
Optional status field, if you want to exercise the vendor's whole
vocabulary: user_action_required (the default), unavailable and
customer_disabled all break the connection. degraded and active are the
two that deliberately do not — a vendor hiccup must not tell a donor their
giving stopped. Send them to prove your integration stays quiet.
6 Scenario 3: the value change
Changing the amount is the donor's act, always: it needs their own
authentication, so there is no partner-side simulator for it and there will
not be one. Use the active enrollment from step 4 (ENROLLMENT_ID). Open the
hosted manage screen as your test donor — the link is on the confirmation page
from step 3, at /manage.html?church=$CHURCH_ID on the sandbox flow host —
verify with the test code 000000 (no text message is sent), choose the new
value and explicitly accept the newly presented Recurring Transfer
Authorization. A new verification code does not replace this acceptance.
You receive enrollment.updated with the new mode and value once the
change applies. A decrease applies at once, and so does an increase of up to 5
percentage points that stays at 15% or less (10% → 15%, for example). Any other
increase is scheduled: the donor's screen says it applies within 24–48 hours,
and nothing reaches your webhook or feed until it does. Your integration must
not treat the donor's confirmation as the change.
7 Scenario 4: the cancellation
Also the donor's act, in the same manage screen, on the same enrollment
(ENROLLMENT_ID). Cancelling is deliberately frictionless — no retention flow,
no waiting period.
You receive enrollment.cancel_requested at once: from that instant the
enrollment is out of the settlement batch, and that is the event your product
should act on. enrollment.cancelled — the one the bar reads — only comes
after the payroll vendor confirms the deduction is actually gone. Cancelling
opens that removal with the payroll vendor, and the manage screen offers the
donor Finish turning it off (data-action="finish-disconnect") to complete
it on the vendor's side. In the sandbox you play the vendor instead; if the
removal could not be opened, the simulator says so with 400, and choosing
Finish turning it off as your test donor opens it again:
curl -X POST "$ROSTER_BASE_URL/sandbox/simulate/payroll-disconnected" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: deallocate-1" \
-H "Content-Type: application/json" \
-d '{"enrollment_id": "'$ENROLLMENT_ID'", "church_id": "'$CHURCH_ID'"}'
That closes scenario 4. An enrollment cancelled before its paycheck was
connected has nothing to remove: enrollment.cancelled follows within seconds
and payroll-disconnected answers 400.
Roster then tells the bank that the donor left the program; closing the donor's
sub-account is the bank's call, on its own clock. You can play that confirmation
too. Nothing changes in the partner API afterwards: the enrollment keeps reading
cancelled and no event is sent.
curl -X POST "$ROSTER_BASE_URL/sandbox/simulate/account-closure" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: closure-1" \
-H "Content-Type: application/json" \
-d '{"enrollment_id": "'$ENROLLMENT_ID'", "church_id": "'$CHURCH_ID'"}'
The gap between cancel_requested and cancelled is real in production too —
vendors take their time. Treating the request as the stop signal is what keeps
you from charging someone who already cancelled.
8 Cross-check on the event feed
curl "$ROSTER_BASE_URL/v1/events?type=enrollment.activated" \
-H "Authorization: Bearer $ROSTER_KEY"
The same event, by pull. Store next_cursor after every page that has events
and poll with since_cursor for reconciliation — the feed is the source of
truth when a webhook is missed. A page with no events means you are caught up;
keep the cursor you already had and never replace it with null (see
Poll without losing your place). The
certification bar reads this same feed, page by page: if a check is dark, the
event is not there, and no amount of retrying your endpoint will light it.
9 Deposit
Simulate the payroll deposit that funds the gift. Name the enrollment; Roster finds the donor's simulated account. The donor must have finished the hosted flow first (the account opens when identity verification passes), and the deposit is held, not rejected, if the enrollment is not active yet — exactly as an early real deposit would be. Follow the financial prerequisites and processing window.
Check the clock before you deposit. A sandbox deposit does not skip the processing window.
The donation becomes a transfer at the first weekday cutoff, 4:00 p.m. America/New_York, that
comes at least 120 minutes after the deposit's bank_ts (inside simulated in the 202), and
only while its enrollment is active and its church is ready for payouts. A deposit made after
2:00 p.m. New York time misses that day's cutoff, so a deposit on a Friday afternoon becomes a
transfer on Monday. A church on a weekly payout_schedule
receives on the next Monday instead. Step 10 needs that transfer, so deposit early in the day if
you want to finish both in one session.
Use a third enrollment that is active, not the one you cancelled in step 7 or paused in
step 5: repeat steps 2–4 with another test number and new idempotency keys, save the new
enrollment as DEPOSIT_ENROLLMENT_ID, and send it in step 4 in place of ENROLLMENT_ID.
curl -X POST "$ROSTER_BASE_URL/sandbox/simulate/deposit" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: deposit-1" \
-H "Content-Type: application/json" \
-d '{"church_id":"'$CHURCH_ID'","enrollment_id":"'$DEPOSIT_ENROLLMENT_ID'","amount":25}'
The 202 tells you when the money can move. eligible_at is when the 120-minute
processing window after bank_ts ends, and next_run_at is the earliest weekday
4:00 p.m. America/New_York settlement run that can pick the donation up. A deposit
simulated after 2:00 p.m. ET misses that day's run, and one simulated on a Friday
afternoon waits until Monday, so plan step 10, which needs an executed transfer,
around next_run_at. That run still checks the enrollment and the church, so it is
the earliest time, not a promise; there is no approval to request from Roster.
Watch for donation.updated on your webhook (step 0 subscribed to it), then read
GET /v1/financial_events with its own cursor. Verify the amount and church in your
software. A received deposit is not an executed transfer; transfer acceptance is not
confirmed final credit.
In the sandbox the transfer needs no approval and no request to Roster: once the
deposit is 120 minutes old, the next weekday cutoff at 4:00 p.m. America/New_York
instructs it on its own, unless a check holds it.
Do not resubmit the deposit to accelerate scheduled processing.
10 Returned transfer
First wait for the deposit from step 9 to become an executed transfer, and use its
donation ID as OCCURRENCE_ID. That happens at a weekday 4:00 p.m. America/New_York cutoff, at
least 120 minutes after the deposit's bank_ts (see step 9); the sandbox
instructs it at the cutoff by itself, so there is nothing to ask Roster for. Until then the
donation's transfers array is empty and this simulator refuses the request with 400.
curl -X POST "$ROSTER_BASE_URL/sandbox/simulate/late-return" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: return-1" \
-H "Content-Type: application/json" \
-d '{"church_id":"'$CHURCH_ID'","occurrence_id":"'$OCCURRENCE_ID'","return_code":"R03","leg":"church","after_days":0}'
Verify the returned church leg and return details in donation.updated and
GET /v1/financial_events, then check your reporting records. after_days does
not advance the clock: a future-dated test can be refused as not_due.
11 Identity check
Open a donor session and complete the flow with a test number from +1 202-555-0190
to 0199. After the donor reviews and accepts, the flow shows that the account is in
review, and the enrollment stays pending. Take the enrollment_id from
GET /v1/donor_sessions/{id} (as in step 3), save it as REVIEW_ENROLLMENT_ID and
decide the review:
curl -X POST "$ROSTER_BASE_URL/sandbox/simulate/kyc" \
-H "Authorization: Bearer $ROSTER_KEY" \
-H "Idempotency-Key: identity-1" \
-H "Content-Type: application/json" \
-d '{"church_id":"'$CHURCH_ID'","enrollment_id":"'$REVIEW_ENROLLMENT_ID'","outcome":"passed"}'
With passed, the review is over, but the donor still has to accept the payroll
authorization, and the in-review screen does not move on by itself (by now the donor's window
may be closed anyway). Bring the donor back to the flow:
-
Create a new donor session for the same church and the same test donor (the same number and
date of birth), with a new
Idempotency-Key, and open itsurl. -
After the code, the flow resumes at the recurring authorization (
confirm), without the review screen and without creating another enrollment. Tick it and authorize; the flow then waits onconnect. -
Continue with step 4 as for any donor, sending
REVIEW_ENROLLMENT_ID.
With failed, the identity check ends without passing: the enrollment never activates and the
flow keeps showing the neutral in-review screen, as a review in production does. It does not
lead to the declined screen, which appears only when a case is closed for good.
An enrollment that is not yours, or does not exist, gets 400. This does not emit a
public identity-result event, and a passing result alone does not prove bank-account
approval. POST /sandbox/simulate/kyc exists only in the sandbox: in production, a
review stays in review until Roster resolves it.
Checklist
- Webhook endpoint registered, test delivery
delivered: true - Churches imported with per-item results handled
- Test church ready:
payout_readyistrue(andchurch.onboardedreceived, where it is sent) - Donor session created; hosted flow completed by a test donor
-
Scenario 1 —
enrollment.createdandenrollment.activatedreceived and deduplicated byevent_id -
Scenario 2 —
enrollment.pausedreceived, and your code branches onreason, not on the type -
Scenario 3 —
enrollment.updatedreceived for a change that applies at once; your product does not treat a scheduled increase as applied untilenrollment.updatedarrives -
Scenario 4 —
enrollment.cancel_requestedacted on immediately;enrollment.cancelledreceived after the payroll vendor confirms - Feed polling with cursor persistence in place
After these scenarios, complete the production readiness checks.
Financial and reporting acceptance
The four portal indicators above cover enrollment lifecycle only. For a paycheck
giving integration, also handle donation.updated and donor.updated (step 0
subscribed to both), and refund.created and refund.updated if you offer
refunds, consume GET /v1/financial_events with its separate cursor, and complete the
financial scenarios.
Verify actual amounts, transfer outcomes, returned legs, donor matching and contact
updates in your reporting model and existing ChMS connector. A green enrollment
bar does not attest to those results.
For simulated identity verification, each test donor gets its own stable fictional address, the same across retries and churches, so test donors never share a household. In production, review cases remain review cases: never count a passed identity check or session handoff as account approval. Preserve a blocked case and its support references for investigation.