Webhook Events
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, aggregating 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.verified | A user’s expenses have been fully 213d classified | user_id |
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 a connect account 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.verified |
| Classification | transactions.ingested, expenses.verified |
| Account connection | connection.created, connection.status_changed, connection.synced, connection.synced.initial, connection.synced.profile, connection.synced.activity |
API Reference › Objects and events