Learn the webhook event catalog and payload shape Sail sends as your data changes.
Sail sends webhook events as data moves through your integration. Each event has an event field that identifies what happened, and a data field with the resource ids you need to fetch the current state.
A webhook payload has the following shape:
{
"id": "evt_7f3k9m",
"event": "connection.synced",
"created_at": "2026-08-02T14:31:07Z",
"user_id": "usr_abc123",
"data": {
"connection_id": "conn_4d81",
"job_id": "job_r8v2",
"balances_updated": true,
"activity_updated": false,
"expenses_updated": true
}
}
Expand a resource below to see the events it can send.
Connections
Events that fire for all connection types (card, store, benefit_account).
| Event | Fires when | data fields |
|---|---|---|
connection.created | A user completes a connect account flow | connection_id, type |
connection.status_changed | Any status transition (for example, syncing to active, or to error) | connection_id, previous_status, status, error_code |
connection.synced | A sync completes with no status change | connection_id, job_id, balances_updated, activity_updated, expenses_updated |
connection.synced.initial | Initial basic account info syncs for a benefit account connection | connection_id, account_id |
connection.synced.profile | Account profile data syncs for a benefit account connection | connection_id, account_id |
connection.synced.activity | Account activity syncs for a benefit account connection | connection_id, account_id |
connection.deleted | A connection is revoked | connection_id |
Expenses
Events that fire when expenses have been fully classified.
| Event | Fires when | data fields |
|---|---|---|
expenses.213d_verified | Every expense on the user has finished Section 213(d) classification and review. Sent only if expense notifications are enabled on your partner account. | None (user_id is in the envelope) |
Transactions
Events that fire when you push your own transaction data through an external connection.
| Event | Fires when | data fields |
|---|---|---|
transactions.ingested | A batch finishes enrichment | connection_id, job_id, created, updated |
Receipts
Events that fire when receipts are uploaded through the API or the Upload Receipt Widget.
| Event | Fires when | data fields |
|---|---|---|
receipt.uploaded | A receipt upload landed | expense_id |
receipt.processed | Itemization and classification finished | expense_id, expense_ids |
receipt.failed | Receipt unreadable or rejected | expense_id, reason |
Reimbursements
Events that fire when a reimbursement changes status.
| Event | Fires when | data fields |
|---|---|---|
reimbursement.status_changed | Any reimbursement status transition | reimbursement_id, previous_status, status, failure_reason |
Connect Account Widgets
| Event | Fires when | data fields |
|---|---|---|
hosted_session.expired | A session ended without the user finishing | session_id, flow |
Users
| Event | Fires when | data fields |
|---|---|---|
user.deleted | Purge completed after deleting a user | user_id (in the envelope) |
Events by integration path
Not every integration needs every event. Here's what to listen for based on your use case.
| Integration path | Events to listen for |
|---|---|
| Expenses | connection.created, connection.status_changed, connection.synced, expenses.213d_verified |
| Classification | transactions.ingested, expenses.213d_verified |
| Account connection | connection.created, connection.status_changed, connection.synced, connection.synced.initial, connection.synced.profile, connection.synced.activity |