Webhooks for platforms
Your connected accounts’ events go to a Connect endpoint, each naming the account it happened
on. Your own events (your fees, transfers and accounts’ losses) go to an ordinary endpoint.
A Connect endpoint
Section titled “A Connect endpoint”Create it with "connect": true (fixed once made). It receives only your connected accounts’
events, none of your own.
curl https://api.tuppence.ai/v1/webhook_endpoints \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/tuppence/connect", "connect": true, "enabled_events": ["account.updated", "payment.succeeded"]}'const endpoint = await tuppence.webhookEndpoints.create({ url: "https://example.com/tuppence/connect", connect: true, enabled_events: ["account.updated", "payment.succeeded"],});endpoint = tuppence.webhook_endpoints.create( url="https://example.com/tuppence/connect", connect=True, enabled_events=["account.updated", "payment.succeeded"],)An event it receives:
{ "id": "evt_…", "object": "event", "account": "acct_1…", "type": "payment.succeeded", "data": { "object": { "id": "pay_…", "object": "payment", "amount": 4500 } }}Like every endpoint, it has its own signing secret, retries and three-day switch-off (Webhooks).
Handle each event once
Section titled “Handle each event once”Verify the signature, skip ids you’ve handled, then dispatch on account and type.
app.post("/tuppence/connect", express.raw({ type: "application/json" }), async (req, res) => { const event = tuppence.webhooks.constructEvent( req.body, req.headers["tuppence-signature"], process.env.TUPPENCE_CONNECT_WEBHOOK_SECRET, ); if (await alreadyHandled(event.id)) return res.sendStatus(200); // a retry of one you handled const business = await businessForTuppenceAccount(event.account); if (event.type === "account.updated") await business.onboardingChanged(event.data.object); if (event.type === "payment.succeeded") await business.paid(event.data.object); await markHandled(event.id); res.sendStatus(200);});event = tuppence.webhooks.construct_event(request.body, request.headers["Tuppence-Signature"], secret)if already_handled(event["id"]): return 200 # a retry of one you handledbusiness = business_for_tuppence_account(event["account"])| Fact | So |
|---|---|
| A failed delivery is retried with the same event id | Record handled ids; answer 200 to a repeat. Ids are unique across all your accounts |
| Events can arrive out of order | Read state from data.object (or retrieve it, as the account), not from arrival order |
| One happening can be two events | A transfer is transfer.created on your own stream and on the account’s, with different ids |
What you hear
Section titled “What you hear”On your Connect endpoint, for each connected account:
| Event | When |
|---|---|
account.created, account.closed | You made or closed the account |
account.updated, account.requirements_due, account.charges_enabled, account.payouts_enabled | Verification moved it: onboarding completed, more due by a deadline, payments or payouts on. Each carries the Account object |
account.identity_updated | An identity check on its person was submitted, verified or failed |
capability.updated | card_payments, transfers or card_issuing changed status |
person.created, person.updated, person.deleted | Its people, by id and role, never a name |
account.external_account.created, .updated, .deleted | Its bank accounts, never their numbers |
transfer.created, transfer.reversed, transfer.failed | Money you sent it, or took back |
cashback.statement_ready, cashback.paid | Its month’s cashback statement was made, then paid: your net into your Connect balance, its share_net to it |
card_dispute.* | Its card disputes |
| Everything else it does | payment.*, refund.*, payout.*, booking.*: the events your own account would have |
On your own endpoint:
| Event | When |
|---|---|
application_fee.created, application_fee.refunded | Your fee on an account’s payment, and any you gave back |
transfer.created, transfer.reversed, transfer.failed | Your transfers, from your side |
account.negative_balance | An account was three days below zero, and what covered it (losses) |
Alerts about a connected account come to you, named for it.
Read and resend
Section titled “Read and resend”| To | Call |
|---|---|
| Read an account’s events | GET /v1/events with Tuppence-Account: acct_…, or GET /v1/events?account=acct_… with your own key |
| Resend one to your Connect endpoint | POST /v1/events/{id}/resend with {"webhook_endpoint": "we_…", "account": "acct_…"} |
- Webhooks: signatures, retries, the full event list
- Testing Connect: a Connect endpoint without a server