Architecture

What happens between your 202 and the webhook.

Request path

The API is FastAPI with async SQLAlchemy over Postgres. Webhook delivery runs on a separate Redis-backed worker, so a slow or failing endpoint on your side never delays an API response.

Bank backend
   │  POST /v1/donations/request        (X-Wishbone-API-Key)
   ▼
Wishbone API ──▶ validate ──▶ persist "pending" ──▶ 202 Accepted
   │
   ▼  (out of band)
School platform adapter ──▶ PAC io / Evenue / Ticketmaster
   │
   ▼
Webhook worker ──▶ donation.completed | donation.failed ──▶ your endpoint

Validation order

A donation request is checked in this order; the first failure is returned.

CheckFailure
School exists and is active404 SCHOOL_NOT_ACTIVE
Your bank is authorized for that school403 FORBIDDEN
fund_designation is active for the school422 FUND_INVALID
Amount clears the school and fund minimum422 AMOUNT_BELOW_MINIMUM
bank_transaction_ref not already used409 DONATION_DUPLICATE

Donation lifecycle

StatusMeaning
pendingAccepted and persisted. You have your 202.
processingBeing pushed to the school's platform.
completedConfirmed. school_confirmation_id and a tax receipt are available.
failedCould not be completed. failure_reason is set and points_refund_required is true on the webhook.

Idempotency

bank_transaction_ref is the idempotency key for the whole pipeline. Wishbone also sends a provider-native idempotency header downstream keyed on the donation ID, so a lost response between Wishbone and the school cannot double-post a gift either.

Retrying a 5xx with the same bank_transaction_ref is always safe. If the first attempt did land, you get 409 DONATION_DUPLICATE — treat that as success and fetch the donation by reference.

Where the money is not

Wishbone records, routes and confirms. It does not hold or move funds. See compliance for the full statement.