# Create Webhooks

Source: https://developers.bitgo.com/docs/crypto-as-a-service-webhooks

## Overview

You can use webhooks to receive real-time HTTP callbacks when specific events occur. This enables you to automate workflows, notify your end-users, and keep your platform in sync with BitGo.

As a CaaS provider, you operate at the Organization level and manage numerous child Enterprises (your end-users). This guide covers the two scopes of webhooks you can integrate:

* **Organization Webhooks** - Notifications for events that apply to your entire CaaS organization such as organization-level policy changes or new bank accounts.
* **Enterprise Webhooks** - Notifications for events specific to one of your end-users such as an end-user KYC status change or a new bank account added to an end-user enterprise.

BitGo signs all webhook notifications, enabling you to verify their authenticity.

* **Webhook Secret** - When you create a webhook, you can set a secret. BitGo uses this secret to generate an HMAC-SHA256 signature.
* **Signature Header** - BitGo sends the signature in the `x-signature-sha256` HTTP header.
* **Verification** -  To verify the payload, compute your own HMAC-SHA256 signature of the raw request body using your secret and compare it to the signature in the header. This is a critical security step.

> 📘 **Note**
>
> You may receive multiple of the same webhook notification due to at-least-once delivery infrastructure. Always design your webhook handlers to be idempotent so you can identify duplicate notifications.

### Enterprise Webhook Types

| Type | Triggers When |
| :--- | :------------ |
| `accessToken` | A user creates an access token. |
| `bankAccount` | A user adds a bank account. |
| `reversal` | BitGo reverses a fiat transaction. |

### Organization Webhook Types

| Type | Triggers When |
| :--- | :------------ |
| `accessToken` | A user creates an access token. |
| `bankAccount` | A user adds a bank account. |
| `enterpriseKycState` | Enterprise KYC status changes. |
| `identityStatus` | A user identity status changes. |
| `idvStatus` | A user IDV status changes. |
| `pendingapproval` | A transaction request is pending approval. |
| `reversal` | BitGo reverses a fiat transaction. |
| `transfer` | A transfer succeeds or fails. |
| `txRequest` | A transaction request changes status. |
| `txRequestTransaction` | A transaction request transaction state changes. |
| `userKycState` | A user KYC verification status changes. |
| `tradeOrder` | A trade order status changes. |

## Webhook Payload Schemas

Each webhook type returns a different JSON payload. Use these example schemas to parse and handle incoming webhook notifications.

## Cookbooks

Need just the steps? Expand a cookbook below to get started:

<Cookbook slug="caas-webhooks-enterprise" title="Create Webhooks (Enterprise)" />

<Cookbook slug="caas-webhooks-organization" title="Create Webhooks (Organization)" />

<Tabs>
<Tab title="Access Token">

Triggered when a user creates an access token.

```json JSON
{
  "userId": "59cd72485007a239fb00282a",
  "username": "user@example.com",
  "tokenCreateTime": "2024-01-15T10:30:00.000Z",
  "tokenExpireTime": "2024-01-15T18:30:00.000Z",
  "spendingLimits": "{\"txValue\":1000000,\"txValueLimit\":5000000}",
  "tokenIp": "192.168.1.100",
  "tokenIpWhitelist": ["192.168.1.0/24"],
  "tokenScope": "wallet_view_all wallet_spend_all",
  "enterpriseName": "Test Enterprise",
  "id": "59cd72485007a239fb00282b",
  "enterpriseId": "59cd72485007a239fb00282c",
  "organizationId": "59cd72485007a239fb00282d"
}
```

</Tab>
<Tab title="Bank Account">

Triggered when a user adds a bank account or its verification state changes.

```json JSON
{
  "bankAccountId": "82156b5e-4e22-413f-8b53-8ce960f0b543",
  "bankAccountName": "TestBank+2-Citi3-Operations",
  "bankAccountType": "ach",
  "enterpriseId": "6967d9efb19e363649a0a30559ae29ec",
  "organizationId": "695d68b362ce13d38fa6a4fe2c7a6a82",
  "idHash": "e4af999a758c5b74",
  "accountNumber": "******4455",
  "verificationState": "approved",
  "type": "bankAccount"
}
```

</Tab>
<Tab title="Enterprise KYC State">

Triggered when an enterprise KYC verification status changes.

> 📘 **Note**
>
> The `kycState` field can be `approved`, `rejected`, or `pending`.

```json JSON
{
  "organizationId": "695d68b362ce13d38fa6a4fe2c7a6a82",
  "kycState": "approved",
  "type": "enterpriseKycState",
  "id": "cmlqyh7x4001m0dix1rypheuq",
  "enterpriseId": "6994b772b8f7aead2dd0e4c42d1181fe"
}
```

</Tab>
<Tab title="Identity Status">

Triggered when a user identity verification status changes.

```json JSON
{
  "id": "bf12d8e0-1207-4b5a-bb3d-b296a9caf9ef",
  "organizationId": "695d68b362ce13d38fa6a4fe2c7a6a82",
  "enterpriseId": "698f4b72460eb817cb415f8e6ba6565e",
  "userId": "698f4b6f460eb817cb415e26ac1bdb71",
  "identityId": "803b761a-7dfa-438d-8f92-b4270c136bd1",
  "type": "identityStatus",
  "status": "approved",
  "signaturesSubmitted": [
    "csa",
    "mpa"
  ],
  "signaturesRequired": []
}
```

</Tab>
<Tab title="IDV Status">

Triggered when a user IDV (Identity Verification) status changes.

```json JSON
{
  "type": "idvStatus",
  "idvId": "98f20884-f3ce-4949-a969-4a6010ae00e1",
  "status": "approved",
  "verificationType": "penny-drop",
  "userId": "6a048467a9198fefd14a12e39ffdf7a1",
  "enterpriseId": "6a04846aa9198fefd14a143de0d6756f",
  "organizationId": "69e291d44afc2b668a3ac76d16fef106",
  "rejectLabels": null
}
```

</Tab>
<Tab title="Pending Approval">

Triggered when a transaction request requires approval or its approval state changes.

```json JSON
{
  "type": "pendingapproval",
  "pendingApprovalType": "transactionRequest",
  "pendingApprovalId": "699727b1281a7931afe20b3549a9d93d",
  "walletId": "6967dbd76e03e014ca3ccec6839a046a",
  "walletLabel": "TestUser+3-ofc-wallet",
  "state": "approved",
  "simulation": false
}
```

</Tab>
<Tab title="Reversal">

Triggered when BitGo reverses a fiat transaction.

```json JSON
{
  "type": "reversal",
  "reversalType": "fiatTransaction",
  "txId": "tx_123",
  "enterpriseId": "ent_456",
  "organizationId": "org_789",
  "transactionType": "ach",
  "state": "posted",
  "reversesTxId": "tx_000",
  "reversalReason": "insufficient_funds",
  "rejectedReason": "ach_return_r01",
  "timestamp": "2023-12-03T12:00:00.000Z"
}
```

</Tab>
<Tab title="Transfer">

Triggered when a transfer succeeds or fails.

```json JSON
{
  "hash": "abbea60e9d0f9a2c480344f454b747e2e64ebcc96e3a819ccf87adddeb808e2b",
  "transfer": "6995f3e4382a02d65d57e6ea60e8513d",
  "coin": "ofctusd",
  "type": "transfer",
  "state": "confirmed",
  "wallet": "6967dbd76e03e014ca3ccec6839a046a",
  "walletType": "trading",
  "transferType": "receive",
  "baseValue": 10000,
  "baseValueString": "10000",
  "value": 10000,
  "valueString": "10000",
  "feeString": null,
  "initiator": [
    "external"
  ],
  "systemNotes": {
    "warning": "Do not rely solely on this webhook for final state. Always verify the current status via the API to ensure consistency. View the BitGo developer documentation and guides for more information."
  }
}
```

A fiat withdrawal is identified by `walletType: "trading"`, `transferType: "send"`, and `subType: "ofc_withdrawal"`. When you receive one, call the [Confirm Fiat Withdrawal](/docs/crypto-as-a-service-fiat-withdraw#6-confirm-fiat-withdrawal) endpoint to approve or reject it.

</Tab>
<Tab title="Transaction Request">

Triggered when an on-chain transaction request changes status.

> 📘 **Note**
>
> Go Account to Go Account transfers are off-chain (BitGo internal ledger) and do not trigger this webhook.

```json JSON
{
  "enterpriseId": "6967d9efb19e363649a0a30559ae29ec",
  "organizationId": "695d68b362ce13d38fa6a4fe",
  "walletId": "6994da8007e963313a01c6a77e28f94a",
  "txRequestId": "c8450292-61f8-406d-bdc7-c1ca4bbb8147",
  "txRequestVersion": 10,
  "webhookType": "txRequest",
  "sequenceIds": [],
  "oldState": "pendingDelivery",
  "newState": "delivered"
}
```

</Tab>
<Tab title="Transaction Request Transaction">

Triggered when a transaction request underlying transaction changes state.

```json JSON
{
  "enterpriseId": "59cd72485007a239fb00282a",
  "organizationId": "59cd72485007a239fb00282b",
  "walletId": "59cd72485007a239fb00282c",
  "txRequestId": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "txRequestVersion": 1,
  "webhookType": "txRequestTransaction",
  "transactionIndex": 0,
  "sequenceId": "my-unique-sequence-id",
  "oldState": "initialized",
  "newState": "signed"
}
```

</Tab>
<Tab title="User KYC State">

Triggered when a user KYC verification status changes.

> 📘 **Note**
>
> The `kycState` field can be `approved`, `rejected`, or `pending`.

```json JSON
{
  "organizationId": "695d68b362ce13d38fa6a4fe2c7a6a82",
  "kycState": "approved",
  "type": "userKycState",
  "id": "cmlqz57ct001j0esb6vde1qgp",
  "userId": "6994bbd53763ba8c4e74ff38766c0ba8"
}
```

</Tab>
<Tab title="Trade Order">

Triggered when a trade order status changes.

> 📘 **Note**
>
> The `status` field can be `pending_open`, `open`, `completed` with null `settleDate`, or `completed` with populated `settleDate`.

```json JSON
{
  "id": "8c99dfb2-bd3e-455e-8bbd-b5b442d65e8e",
  "accountId": "69979f2926b13520a108df8fe5deb360",
  "enterpriseId": "69979ce2613a65a7a666c35abcc94855",
  "initiatedByUserId": "654e94156e168842aa99bd51ffe3f9f8",
  "canceledByUserId": null,
  "clientOrderId": null,
  "time": "2026-02-26T14:02:32.129212Z",
  "creationDate": "2026-02-26T14:02:30.567219Z",
  "scheduledDate": null,
  "lastFillDate": "2026-02-26T14:02:31.824515Z",
  "completionDate": "2026-02-26T14:02:32.129212Z",
  "settleDate": "2026-02-26T14:03:23.868Z",
  "fundingType": "funded",
  "type": "market",
  "timeInForce": null,
  "status": "completed",
  "reason": "",
  "product": "TSOL-TUSD*",
  "side": "buy",
  "quantity": "6",
  "quantityCurrency": "TUSD*",
  "filledQuantity": "0.06864465",
  "filledQuoteQuantity": "6",
  "leavesQuantity": null,
  "leavesQuoteQuantity": "0",
  "averagePrice": "87.41",
  "limitPrice": null,
  "duration": null,
  "twapInterval": null,
  "rtId": null,
  "notes": null,
  "isFinanced": false
}
```

</Tab>
</Tabs>

## Prerequisites

* [Get Started](/docs/get-started-intro)

<Tabs>
<Tab title="Create Organization Webhook">

## 1. Create Organization Webhooks

The following example creates a webhook that notifies you whenever a user adds a bank account.

>Endpoint: [Create Organization Webhook](/reference/v2organizationwebhookadd)

```shell cURL
export ORGANIZATION_ID="<YOUR_ORGANIZATION_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export URL="<YOUR_WEBHOOK_URL>"
export LABEL="<YOUR_WEBHOOK_NAME>"

curl -X POST \
  https://app.bitgo-test.com/api/v2/organization/$ORGANIZATION_ID/webhook \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "type": "bankAccount",
    "url": "'"$URL"'",
    "label": "'"$LABEL"'"
  }'
```

##### Step Result

```json JSON
{
  "id": "690278897328c7fc41e81887d4b76964",
  "label": "test org webhook 1",
  "created": "2025-10-29T20:26:49.286Z",
  "scope": "organization",
  "organizationId": "68f6af3f2f29893d5863bc0eaf77b3c6",
  "type": "bankAccount",
  "url": "https://webhook.site/2635918b-018b-4179-82e9-6940a0b851e0",
  "version": 1,
  "state": "active",
  "successiveFailedAttempts": 0,
  "listenToFailureStates": false,
  "txRequestStates": [],
  "txRequestTransactionStates": [],
  "identityStatus": []
}
```

## 2. (Optional) Create Webhook Secret

Create a webhook secret that enables you to verify webhook notifications. BitGo uses this secret to generate HMAC-SHA256 signatures for all webhook payloads, enabling you to verify that notifications genuinely originate from BitGo and that no one has tampered with them.

> ❗ **Important**
>
> Store your webhook secret securely. If compromised, rotate it immediately by calling this endpoint again. You can rotate secrets once every 5 minutes.

> 📘 **Note**
>
> You must create your webhook secret before performing the action that triggers the notification. Otherwise, the notification won't include a signature and you won't be able to verify it.

>Endpoint: [Create Webhook Secret](/reference/v2webhooksecretcreate)

```shell cURL
export ORGANIZATION_ID="<YOUR_ORGANIZATION_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"

curl -X POST \
  https://app.bitgo-test.com/api/v2/webhook/secret \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "organizationId": "'"$ORGANIZATION_ID"'"
  }'
```

##### Step Result

```json JSON
{
  "secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
}
```

Save this secret securely—you'll need it to verify incoming webhook notifications using the `x-signature-sha256` header.

## 3. (Optional) Verify Webhook Notification

When BitGo sends a webhook notification, it includes an `x-signature-sha256` header containing an HMAC-SHA256 signature. You can verify the notification is legitimate by passing the signature from this header along with the payload you received.

>Endpoint: [Verify Webhook Notification](/reference/v2webhooknotificationverify)

```shell cURL
export WEBHOOK_ID="<YOUR_WEBHOOK_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export SIGNATURE="<X_SIGNATURE_SHA256_HEADER_VALUE>" # Value from the x-signature-sha256 header in the webhook notification
export PAYLOAD="<YOUR_PAYLOAD>" # JSON payload as a string from the webhook notification body

curl -X POST "https://app.bitgo-test.com/api/v2/webhook/$WEBHOOK_ID/verify" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "signature": "'"$SIGNATURE"'",
    "notificationPayload": "'"$PAYLOAD"'"
  }'
```

##### Step Result

```json JSON
{
  "webhookId": "wh119ecd15a4adf811f8f552fde21b9d819b4dc9a7f04c51513395816703c73511",
  "isValid": true
}
```

## 4. (Optional) Simulate Organization Webhook

You can simulate your webhook with real data from the prior step or with placeholder data (also known as dummy data).

>Endpoint: [Simulate Organization Webhook](/reference/v2organizationwebhooksimulate)

```shell cURL
export ORGANIZATION_ID="<YOUR_ORGANIZATION_ID>"
export WEBHOOK_ID="<YOUR_WEBHOOK_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export ACCESS_TOKEN_ID="<YOUR_ACCESS_TOKEN_ID>"

curl -X POST "https://app.bitgo-test.com/api/v2/organization/$ORGANIZATION_ID/webhook/$WEBHOOK_ID/simulate" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
  -d '{
  "bankAccountId": "string"
}'
```

##### Step Result

```json JSON
{
  "webhookNotifications": [
    {
      "id": "59cd72485007a239fb00282ed480da1f",
      "accessToken": "txRequest",
      "url": "https://webhook.site/f74addc1-c40a-4fce-879a-2d92b8d491c5",
      "hash": "db924f4cf2347ac5a6b464d3e8dc4a20cffc117eb29f4460645fbc34de171bfb",
      "simulation": true,
      "retries": 0,
      "webhook": "68531e154d28af627819bd3e183a930e",
      "updatedAt": "2025-06-19T20:48:43.525Z",
      "version": 1,
      "allowBlockedHosts": true,
      "payload": "string",
      "response": {
        "code": 0,
        "type": "string",
        "body": "string",
        "error": "string"
      }
    }
  ]
}
```
</Tab>
<Tab title="Create Enterprise Webhook">

## 1. Create Enterprise Webhooks

The following example creates a webhook that notifies you whenever a user creates an access token.

>Endpoint: [Create Enterprise Webhook](/reference/v2enterprisewebhookadd)

```shell cURL
export ENTERPRISE_ID="<YOUR_ENTERPRISE_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export URL="<YOUR_WEBHOOK_URL>"
export LABEL="<YOUR_WEBHOOK_NAME>"

curl -X POST \
  https://app.bitgo-test.com/api/v2/enterprise/$ENTERPRISE_ID/webhooks \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "type": "accessToken",
    "url": "'"$URL"'",
    "label": "'"$LABEL"'"
  }'
```

##### Step Result

```json JSON
{
  "id": "6854341f9b1bb1a97f4ad2d8af07f178",
  "label": "my-enterprise-access-token-webhook",
  "created": "2025-06-19T16:00:31.738Z",
  "scope": "enterprise",
  "enterpriseId": "62c5ae8174ac860007aff138a2d74df7",
  "type": "accessToken",
  "url": "https://webhook.site/f74addc1-c40a-4fce-879a-2d92b8d491c5",
  "version": 2,
  "state": "active",
  "successiveFailedAttempts": 0,
  "listenToFailureStates": false,
  "txRequestStates": [],
  "txRequestTransactionStates": [],
  "identityStatus": []
}

```

## 2. (Optional) Create Webhook Secret

Create a webhook secret that enables you to verify webhook notifications. BitGo uses this secret to generate HMAC-SHA256 signatures for all webhook payloads, enabling you to verify that notifications genuinely originate from BitGo and that no one has tampered with them.

> ❗ **Important**
>
> Store your webhook secret securely. If compromised, rotate it immediately by calling this endpoint again. You can rotate secrets once every 5 minutes.

> 📘 **Note**
>
> You must create your webhook secret before performing the action that triggers the notification. Otherwise, the notification won't include a signature and you won't be able to verify it.

>Endpoint: [Create Webhook Secret](/reference/v2webhooksecretcreate)

```shell cURL
export ENTERPRISE_ID="<YOUR_ENTERPRISE_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"

curl -X POST \
  https://app.bitgo-test.com/api/v2/webhook/secret \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "enterpriseId": "'"$ENTERPRISE_ID"'"
  }'
```

##### Step Result

```json JSON
{
  "secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
}
```

## 3. (Optional) Verify Webhook Notification

When BitGo sends a webhook notification, it includes an `x-signature-sha256` header containing an HMAC-SHA256 signature. You can verify the notification is legitimate by passing the signature from this header along with the payload you received.

>Endpoint: [Verify Webhook Notification](/reference/v2webhooknotificationverify)

```shell cURL
export WEBHOOK_ID="<YOUR_WEBHOOK_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export SIGNATURE="<X_SIGNATURE_SHA256_HEADER_VALUE>" # Value from the x-signature-sha256 header in the webhook notification
export PAYLOAD="<YOUR_PAYLOAD>" # JSON payload as a string from the webhook notification body

curl -X POST "https://app.bitgo-test.com/api/v2/webhook/$WEBHOOK_ID/verify" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "signature": "'"$SIGNATURE"'",
    "notificationPayload": "'"$PAYLOAD"'"
  }'
```

##### Step Result

```json JSON
{
  "webhookId": "wh119ecd15a4adf811f8f552fde21b9d819b4dc9a7f04c51513395816703c73511",
  "isValid": true
}
```

## 4. (Optional) Simulate Enterprise Webhook

You can simulate your webhook with real data or with placeholder data (also known as dummy data).

>Endpoint: [Simulate Enterprise Webhook](/reference/v2enterprisewebhooksimulate)

```shell cURL
export ENTERPRISE_ID="<YOUR_ENTERPRISE_ID>"
export WEBHOOK_ID="<YOUR_WEBHOOK_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export ACCESS_TOKEN_ID="<YOUR_ACCESS_TOKEN_ID>"

curl -X GET "https://app.bitgo-test.com/api/v2/enterprise/$ENTERPRISE_ID/webhook/$WEBHOOK_ID/simulate" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
  "accessTokenId": "'"$ACCESS_TOKEN_ID"'"
}'
```

##### Step Result

```json JSON
{
  "webhookNotifications": [
    {
      "id": "59cd72485007a239fb00282ed480da1f",
      "type": "accessToken",
      "enterprise": "62c5ae8174ac860007aff138a2d74df7",
      "url": "https://webhook.site/f74addc1-c40a-4fce-879a-2d92b8d491c5",
      "state": "sent_via_ncc",
      "simulation": true,
      "retries": 0,
      "webhook": "68531e154d28af627819bd3e183a930e",
      "updatedAt": "2025-06-19T20:48:43.525Z",
      "idempotencyKey": "a1b2c3d4e5f6g7h8",
      "payload": "string",
      "response": {}
    }
  ]
}
```
</Tab>
</Tabs>

## See Also

* [API Reference: Create Organization Webhook](/reference/v2organizationwebhookadd)
* [API Reference: Create Webhook Secret](/reference/v2webhooksecretcreate)
* [API Reference: Simulate Organization Webhook](/reference/v2organizationwebhooksimulate)
* [API Reference: Create Enterprise Webhook](/reference/v2enterprisewebhookadd)
* [API Reference: Simulate Enterprise Webhook](/reference/v2enterprisewebhooksimulate)
* [API Reference: Verify Webhook Notification](/reference/v2webhooknotificationverify)
* <a href="https://webhook.site/" target="_blank" rel="noreferrer">Webhook.site</a>
