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 endpointValidation order
A donation request is checked in this order; the first failure is returned.
| Check | Failure |
|---|---|
| School exists and is active | 404 SCHOOL_NOT_ACTIVE |
| Your bank is authorized for that school | 403 FORBIDDEN |
| fund_designation is active for the school | 422 FUND_INVALID |
| Amount clears the school and fund minimum | 422 AMOUNT_BELOW_MINIMUM |
| bank_transaction_ref not already used | 409 DONATION_DUPLICATE |
Donation lifecycle
| Status | Meaning |
|---|---|
| pending | Accepted and persisted. You have your 202. |
| processing | Being pushed to the school's platform. |
| completed | Confirmed. school_confirmation_id and a tax receipt are available. |
| failed | Could 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.