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

# Webhook Guide

## Setting Up Webhooks

When creating your `link_session_token` with the [POST /link-session](/link-session/create-link-session) endpoint, you will pass your webhook URL with the webhook field

```
"webhook": "https://yourdomain.com/moneykit_webhook"
```

MoneyKit currently offers two webhooks:

* [Link State Changed](#link-state-changed-webhook)
* [Transactions Updates Available](#transaction-updates-available-webhook)

When an end user successfully links their accounts at a financial institution, the webhook updates for that link will be sent to the webhook URL you provided when creating the `link_session_token` with [POST /link-session](/link-session/create-link-session)

<Tip>The webhook body will always include the `link_id` of the link that was updated.</Tip>

## Webhook Security

The webhook payload is signed and will include a moneykit-signature header that can be used to authenticate the webhook.

MoneyKit offers a [JSON Web Key Set (JWKS)](/authentication/json-web-key-set) you can fetch in order to verify the JWT in the header. The JWKS used to sign is rotated regularly, you can use this endpoint to fetch the latest key set.

<Note>
  Note that only one key returned from the [JWKS endpoint](/authentication/json-web-key-set) is meant to be used for
  verification.{" "}
</Note>

* The `kid` in the webhook JWT header will match up with the *key id* of one of the keys returned from the [JWKS endpoint](/authentication/json-web-key-set).
* Depending on the language, some JWT packages will handle this for you. You can pass in the Key Set with the token when decoding (instead of a single key).
* To verify the webhook, first extract the `moneykit-signature` from the webhook header, then fetch the latest key set with the above endpoint, and then validate the `moneykit-signature` using the key set with your preferred JWT library.

```json
{
  "moneykit-signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6IkEtN1ZQRHFURDhFUUlnVGRRVXA3MUtfeHNvS08zSWVzV0xuZ2p4X2giLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FwaS5tb25leWtpdC5jb20iLCJpYXQiOjE2OTgxNTA0MzMsInJlcXVlc3RfYm9keV9zaGEyNTYiOiI2ODk4Yzc2YzA5ZDEwNWQxMjM4OTcxMTAwNGIyYjkxODZjYTE0ZjBkMjJlZDJhZjkzOTIzMWJiMjc4MmYxZGQ0In0.TAT0jQxQnHoBrAcSijtFsYpme35WD5RGsKWOrgzUOCaEiY3CSflkVXO9xWVyqh0-k-98_1MFHbMxpNExrOXaJg"
}
```

## Link State Changed Webhook

* You will receive this webhook when the state of a link has changed

* There are five different states for a link, outlined in the enum `LinkState`
  ```js LinkState
  CONNECTING = "connecting";
  AWAITING_TOKEN_EXCHANGE = "awaiting_token_exchange";
  CONNECTED = "connected";
  DELETED = "deleted";
  ERROR = "error";
  ```

* If the state is `ERROR`, you will be provided an error type via the `LinkError` enum
  ```js LinkError
  SYSTEM_ERROR = "system_error";
  PROVIDER_ERROR = "provider_error";
  INSTITUTION_ERROR = "institution_error";
  USER_ERROR = "user_error";
  AUTH_EXPIRED = "auth_expired";
  INCOMPLETE = "incomplete";
  ```

* Here are two examples of the Link State Changed webhook response body

  <CodeGroup>
    ```python Python
      webhook_event: Literal["link.state_changed"] = "link.state_changed"
      webhook_major_version: Literal[1] = 1
      webhook_minor_version: Literal[0] = 0

      webhook_idempotency_key: str
      webhook_timestamp: datetime

      link_id: str
      link_tags: list[str]

      state: LinkState
      error: LinkError | None
      error_message: str | None
    ```

    ```json JSON
    {
      "webhook_event": "link.state_changed",
      "webhook_major_version": 1,
      "webhook_minor_version": 0,
      "webhook_idempotency_key": "52e5d1896805459e899e803f2f3a8446",
      "webhook_timestamp": "2023-10-24T12:18:22.432381",
      "link_id": "mk_a2vfuwDx8qRh77uwfCwT9X",
      "link_tags": [],
      "state": "connected",
      "error": None,
      "error_message": None
    }


    {
      "webhook_event": "link.state_changed",
      "webhook_major_version": 1,
      "webhook_minor_version": 0,
      "webhook_idempotency_key": "ab127b5c1b2340e18ed30ce246f619e0",
      "webhook_timestamp": "2023-10-24T12:27:13.177113",
      "link_id": "mk_RPGN6BqZa4iWXRz5n328Ab",
      "link_tags": [],
      "state": "error",
      "error": "system_error",
      "error_message": "IntegrationError: Integration error no_accounts: No accounts found"
    }

    ```
  </CodeGroup>

* Any link\_tags you provide when creating your `link_session_token` with the [POST /link-session](/link-session/create-link-session) endpoint will be included in the Link State Changed webhook body:
  ```json
    "link_tags": [
      "user_type:admin"
    ],
  ```

## Link Product Refresh Webhook

You will receive this webhook after aggregation for a product has been completed.

There are three scenarios in which this will happen:

1. After an end user completes [linking](/link-session/create-link-session) their accounts, if prefetch for a product is set as `true`.

2. After requesting a product refresh using the [product refresh endpoint](/products/refresh-products).

3. After a scheduled, periodic aggregation.

Here is the Link Product Refresh webhook class in Python:

```python
webhook_event: Literal["link.product_refresh"] = "link.product_refresh"
webhook_major_version: Literal[1] = 1
webhook_minor_version: Literal[0] = 0

webhook_idempotency_key: str
webhook_timestamp: datetime

link_id: str
link_tags: list[str]

product: Product
state: LinkProductState
state_changed_at: datetime
error_message: str | None
```

Here is an example of the Link Product Refresh webhook response body in JSON:

```json
{
  "webhook_event": "link.product_refresh",
  "webhook_major_version": 1,
  "webhook_minor_version": 0,
  "webhook_idempotency_key": "2HgPyn8H8CdaYhtLd3sdkf",
  "webhook_timestamp": "2023-11-06T10:31:39.515578",
  "link_id": "mk_BwujtU5hcym7te8AYaHSxC",
  "link_tags": [],
  "product": "account_numbers",
  "state": "completed",
  "state_changed_at": "2023-11-06T10:31:39.475714+00:00",
  "error_message": null
}
```

## Transaction Updates Available Webhook

You will receive this when transactions have been aggregated for a link.

Here is the Transactions Updates Available webhook class in Python:

```python
webhook_event: Literal["transactions.updates_available"] = "transactions.updates_available"
webhook_major_version: Literal[1] = 1
webhook_minor_version: Literal[0] = 0

webhook_idempotency_key: str
webhook_timestamp: datetime

link_id: str
has_history: bool
```

Here is an example of the Transactions Update Available webhook response body in JSON:

```json
{
  "webhook_event": "transactions.updates_available",
  "webhook_major_version": 1,
  "webhook_minor_version": 0,
  "webhook_idempotency_key": "9682969d4b6b4a92ad4dea214ea6aa2c",
  "webhook_timestamp": "2023-10-24T12:29:22.344868",
  "link_id": "mk_EtHQBP4swwN5CKrjnrqAPJ",
  "has_history": true
}
```

In order to trigger an aggregation of transactions immediately after an end user completes linking their financial institution, mark prefetch as `true` for the Transactions product when creating your `link_session_token` with the [POST /link-session](/link-session/create-link-session) endpoint.

In order to aggregate the full transaction history for the link, mark `extend_history` as `true` for the Transactions product when creating your `link_session_token` with the [POST /link-session](/link-session/create-link-session) endpoint.

```json
"products": {
  "transactions": {
    "required": true,
    "prefetch": true,
    "extend_history": true
  }
}
```

If the **Transactions Updates Available** webhook includes the full transaction history, `has_history` in the webhook body will return `true`.

## Testing Webhooks in Sandbox

MoneyKit makes it easy to test webhooks on demand in the sandbox environment.

The [Test Link Event](/webhooks/test-link-event) webhook allows you to trigger a webhook for one of your Sandbox Links.
