> ## Documentation Index
> Fetch the complete documentation index at: https://docs.setaside.money/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Create transaction webhooks, configure authentication headers, and verify signed deliveries.

See [Transaction webhooks](/guides/transaction-webhooks) for setup in the web app and through MCP.

## Authentication and endpoints

Use your personal API key with the developer API. Developer API access is required; the key needs `data.read` for listing or `data.write` for changes.

The API key that authenticates your request to Set Aside Money is separate from the destination token inside `headers`.

### Create

```bash theme={null}
curl --request POST 'https://api.setaside.money/v1/webhooks' \
  --header "Authorization: Bearer $SET_ASIDE_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "My automation",
    "url": "https://example.com/webhook",
    "headers": {
      "Authorization": "Bearer YOUR_DESTINATION_TOKEN"
    }
  }'
```

Creation returns HTTP `201`, the webhook's ID and settings, and a `secret` beginning with `whsec_`. Store that signing secret securely; it cannot be retrieved later. Destination header values are omitted from the response.

### List, update, test, or remove

| Method | Endpoint | Behavior |
| - | - | - |
| `GET` | `/v1/webhooks` | List your webhooks, configured header names, and delivery status. |
| `PATCH` | `/v1/webhooks/{id}` | Replace headers and/or change the `enabled` state. |
| `POST` | `/v1/webhooks/{id}/test` | Queue a synthetic test; returns HTTP `202`. |
| `DELETE` | `/v1/webhooks/{id}` | Remove the webhook and its queued deliveries. |

For example, replace an existing webhook's authentication header:

```bash theme={null}
curl --request PATCH "https://api.setaside.money/v1/webhooks/$WEBHOOK_ID" \
  --header "Authorization: Bearer $SET_ASIDE_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{"headers":{"Authorization":"Bearer YOUR_DESTINATION_TOKEN"}}'
```

Omitting `headers` preserves the saved values. Sending `"headers": {}` clears them. Sending `"enabled": false` pauses the webhook and cancels its queued backlog; `true` resumes delivery for future transactions.

You can have up to ten webhooks per user per Set Aside Money account, including paused webhooks. Tests are limited to one per minute per webhook. Changing a destination URL or signing secret requires creating a replacement webhook and removing the old one. Revoking the API key used to create a webhook does not remove that webhook.

## Transaction events

A `transaction.created` event is sent for each newly observed pending or booked transaction, including supported bank imports, Wallet/FinanceKit imports, and manual transactions created through the API.

Initial bank history is skipped. Updates, deletions, pending-to-posted replacements, and replacements of already observed Wallet transactions do not emit additional events. Delivery timing depends on provider availability and the next sync; a bank purchase is not necessarily available immediately.

Example payload with synthetic identifiers:

```json theme={null}
{
  "eventId": "evt_transaction_00000000-0000-4000-8000-000000000001",
  "name": "transaction.created",
  "timestamp": "2026-10-18T12:00:00.000Z",
  "data": {
    "set_aside_account_id": "00000000-0000-4000-8000-000000000002",
    "transaction": {
      "id": "00000000-0000-4000-8000-000000000001",
      "account_id": "00000000-0000-4000-8000-000000000003",
      "amount": 1250,
      "currency": "USD",
      "concept": "Example purchase",
      "booked_at": "2026-10-18",
      "created_at": "2026-10-18T12:00:00.000Z",
      "type": "expense",
      "status": "pending",
      "category_id": null,
      "expense_goal_id": null,
      "provider": "simplefin"
    }
  }
}
```

`amount` is the stored transaction amount in integer cents. `type` describes its classification, and `concept` is the merchant description. `set_aside_account_id` identifies the personal or joint account; `transaction.account_id` identifies the bank or Wallet account within it.

Payloads omit bank credentials, raw provider data, and private notes. Test events use `name: "webhook.test"` and a unique `evt_test_…` event ID.

## Verify and acknowledge deliveries

Each request is a JSON `POST` with:

| Header | Value |
| - | - |
| `webhook-id` | The event ID, unchanged on retries. |
| `webhook-timestamp` | The signing time in Unix seconds. |
| `webhook-signature` | `v1,<base64 HMAC>`. |
| `X-Set-Aside-Webhook-Id` | Your webhook's ID. |

Remove `whsec_` from the signing secret and base64-decode the remainder to obtain the HMAC key. Compute HMAC-SHA256 over the exact UTF-8 bytes of:

```text theme={null}
webhook-id.webhook-timestamp.raw_request_body
```

Use the actual header values and the unmodified raw request body, separated by periods. Compare signatures using a constant-time comparison, reject timestamps outside a five-minute window, and parse JSON after verification. Deduplicate by webhook ID and event ID before applying effects. Return a `2xx` response promptly to acknowledge receipt.

## Retries and troubleshooting

Delivery is at least once, with no ordering guarantee. Timeouts, network failures, HTTP `408`, `429`, and `5xx` retry up to eight total attempts. Backoff begins at 30 seconds and doubles, capped at one hour. Other non-`2xx` responses are terminal for that event. HTTP `410` also pauses the webhook.

Redirects are not followed. Destinations must resolve to public addresses; private, loopback, and link-local addresses are refused.

### HTTP 401 or 403

The destination rejected the request. A displayed header name confirms that a header is configured, but does not prove that its value is valid for the destination.

Check that the header value uses the destination's required authentication scheme, token, and permissions. For bearer authentication, enter `Bearer YOUR_DESTINATION_TOKEN` as the value, without repeating `Authorization:`. Replace the headers if necessary, then send a new test and refresh status. A previous failed event is not automatically retried after a `401` or `403`.

### No deliveries

Confirm the webhook is active, the creator is still an account member, and the account has an active subscription. Check that the relevant bank account remains subscribed and visible. Initial history does not trigger events. Use **Send test** to test the destination separately from bank-sync timing.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.