Webhooks

Overview

Bakkt API uses webhooks to provide you with real-time, asynchronous updates on the status of various processes. Instead of constantly polling the API for changes, you'll receive push notifications directly to your designated webhook endpoints whenever key events occur.

Setup

Bakkt supports a webhook endpoints API that lets you manage multiple webhook endpoints, each with its own URL, optional event filters, and per-endpoint signing secret. This is useful when you want to:

  • Send different event types to different URLs (e.g. transactions to one service, bank accounts to another)
  • Use a different secret per endpoint for verification
  • Add or remove endpoints without changing a single global webhook URL

Managing Webhook Endpoints

ActionEndpointDescription
CreatePOST /merchant/webhook-endpointsCreate a new endpoint with url, description, and optional subscribed_events. Returns uuid, status, url, and signing_secret.
ListGET /merchant/webhook-endpointsList all your endpoints. Optional parameter: status (active, disabled, auto_disabled).
Get oneGET /merchant/webhook-endpoints/{id}Get a single endpoint by UUID.
UpdatePATCH /merchant/webhook-endpoints/{id}Update url, status (active/disabled), or subscribed_events.
Rotate secretPATCH /merchant/webhook-endpoints/{id}/secretRotate the signing secret for that endpoint. Returns the new signing_secret; the previous one is invalidated.
DeleteDELETE /merchant/webhook-endpoints/{id}Remove the endpoint. It will no longer receive events.

All of these require merchant API key authentication.

Creating an Endpoint

When you create an endpoint you must send:

  • url (required): HTTPS URL where Bakkt will send webhook payloads.
  • description (required): Short description of the endpoint (max 255 characters).
  • subscribed_events (optional): Array of event types to receive. If omitted, the endpoint receives all event types.

Supported subscribed_events values include: fiatToCrypto, cryptoToFiat, bakktBankAccount, walletCreated, linkedBankAccountProfile, linkBankAccount, duplicateUser, senderNameMismatch, otpNotification.

Example request:

POST /merchant/webhook-endpoints
{
  "url": "https://api.yourcompany.com/webhooks/transactions",
  "description": "Production transaction events",
  "subscribed_events": ["fiatToCrypto", "cryptoToFiat"]
}

The response includes the endpoint uuid and signing_secret. Store the secret securely and use it to verify webhook authenticity for requests sent to this URL.

Verifying Webhooks

Each webhook endpoint has its own signing secret. When Bakkt sends a webhook to that endpoint's URL, the request is signed with that endpoint's secret.

Bakkt will include the Webhook Secret in the Authorization header of every webhook sent to your URL.

Webhook Header Example:

{
  "Accept": "application/json",
  "Content-type": "application/json",
  "Authorization": "API-Key <YOUR_WEBHOOK_SECRET>"
}

To verify authenticity:

  1. Extract the value from the Authorization header of the incoming webhook request.
  2. Compare it with the Webhook Secret for that URL.
  3. If the values match, the webhook is authentic. If they don't match, discard the webhook as it may be fraudulent.

If you rotate the secret via PATCH /merchant/webhook-endpoints/{id}/secret, use the new secret for all subsequent requests to that URL.

Delivery and Reliability

  • Retry Attempts: Bakkt will attempt to deliver a webhook up to 7 times in total (the initial attempt + 6 retries).
  • Retry Schedule: The delay between retries increases with each attempt, calculated as: n^6 + 2 seconds, where n is the attempt number (starting from n=1 for the first retry).
    • After 1st failed attempt (n=1): ~3 seconds
    • After 2nd failed attempt (n=2): ~66 seconds (~1 minute)
    • After 3rd failed attempt (n=3): ~731 seconds (~12 minutes)
    • After 4th failed attempt (n=4): ~4,098 seconds (~1 hour 8 minutes)
    • After 5th failed attempt (n=5): ~15,627 seconds (~4 hours 20 minutes)
    • After 6th failed attempt (n=6): ~46,658 seconds (~13 hours)

If all 7 attempts fail, the webhook will not be sent again for that specific event.

Data Structure

All webhooks follow a consistent structure:

{
  "type": "webhookType",
  "subType": "webhookSubType",
  "uuid": "entityUuid",
  "data": {
    // ... event-specific data ...
  }
}
  • type: Broad category of the webhook event.
  • subType: Granular classification within the type.
  • uuid: The unique identifier of the user or corporate entity associated with the event.
  • data: A JSON object containing detailed information specific to the webhook event.

Webhook Types

fiatToCrypto Webhooks

Triggered by events in fiat-to-stablecoin transactions.

subTypeDescription
PENDINGTransaction has been created and is awaiting processing.
IN_PROGRESSBakkt acknowledges receipt of fiat and is processing the fiat-to-stablecoin transaction.
CRYPTO_TRANSFER_ISSUEDStablecoin transaction has been sent to the network. data will include transactionHash and amountCrypto.
SUCCESSStablecoin transaction is finalized and process completed.
ON_HOLDTransaction is temporarily paused. Check data.status for the reason (e.g., ON_HOLD_KYC, ON_HOLD_PROCESS).
FAILEDFiat-to-stablecoin transaction failed. Contact support for assistance.
REFUNDEDFiat-to-stablecoin transaction was refunded to the customer.

data object structure:

{
  "status": "string",
  "transactionUuid": "uuid",
  "direction": "fiatToCrypto",
  "amountCrypto": 0,
  "amountFiat": 0,
  "amountRefunded": 0,
  "walletAddress": "string",
  "transactionHash": "string",
  "currencyFiat": "string",
  "currencyCrypto": "string",
  "fees": 0,
  "internalBankAccountId": "uuid",
  "chain": "string",
  "senderDetails": {
    "senderName": "string",
    "senderCountry": "string",
    "accountDetails": {
      "currency": "EUR",
      "iban": "1234567890"
    }
  },
  "sendingWalletAddress": "string",
  "linkedBankAccountUuid": "string"
}

Note: If you ever need to contact support over a transaction, please provide the transactionUuid value.

cryptoToFiat Webhooks

Triggered by events in stablecoin-to-fiat transactions (off-ramping).

subTypeDescription
PENDINGTransaction has been created and is awaiting processing.
IN_PROGRESSBakkt acknowledges receipt of stablecoins and is processing the stablecoin-to-fiat transaction.
FIAT_TRANSFER_ISSUEDFiat transfer has been initiated. data will include amountFiat.
SUCCESSFiat transfer completed successfully.
ON_HOLDTransaction is temporarily paused. Check data.status for the reason.
FAILEDStablecoin-to-fiat transaction failed. Contact support for assistance.
LIMIT_BREACHEDUser sent stablecoin amount below the minimum limit for the chain.
REFUNDEDStablecoin-to-fiat transaction was refunded to the customer.

data object structure:

{
  "status": "string",
  "transactionUuid": "uuid",
  "direction": "cryptoToFiat",
  "amountCrypto": 0,
  "amountFiat": 0,
  "amountRefunded": 0,
  "transactionHash": "string",
  "currencyFiat": "string",
  "currencyCrypto": "string",
  "fees": 0,
  "sendingAddress": "string",
  "remoteBankAccountUuid": "uuid",
  "chain": "string",
  "limit": 0,
  "beneficiaryDetails": {
    "receiverName": "string",
    "accountDetails": {
      "currency": "NGN",
      "accountNumber": "1234567890",
      "bankCode": "044",
      "bankName": "Access Bank"
    }
  }
}

bakktBankAccount Webhooks

Triggered by updates to the status of stablecoin fiat accounts.

subTypeDescription
statusUpdateStatus of a stablecoin fiat account has been updated. Check data.status.

data object structure:

{
  "uuid": "string",
  "currency": "string",
  "bank_country": "string",
  "iban": "string",
  "bic": "string",
  "account_number": "string",
  "sort_code": "string",
  "swift_code": "string",
  "bank_code": "string",
  "bank_name": "string",
  "status": "ACTIVE | WAITING_CREATION | FAILED_CREATION | DISABLED",
  "error": "string"
}

linkedBankAccountProfile Webhooks

Triggered by updates to the status of profile creation for linked bank accounts.

subTypeDescription
statusUpdateStatus of a profile has been updated. Check data.status.

data object structure:

{
  "uuid": "string",
  "status": "ACTIVE | WAITING_CREATION | FAILED_CREATION | DISABLED",
  "error": "string"
}

linkBankAccount Webhooks

Triggered when the status of a linked bank account (e.g. after the user completes Plaid) changes.

Full webhook payload:

{
  "type": "linkBankAccount",
  "subType": "statusUpdate",
  "uuid": "<user-uuid-or-company-uuid>",
  "data": {
    "uuid": "<linked-bank-account-uuid>",
    "status": "ACTIVE",
    "error": "<optional; present when status is FAILED>"
  }
}
FieldDescription
typeAlways linkBankAccount.
subTypestatusUpdate — the linked bank account status has changed.
uuidUser UUID (or company UUID for corporates).
data.uuidLinked bank account UUID.
data.statusOne of: ACTIVE, CHECK_COMPLETED, PENDING, FAILED, DISABLED.
data.errorOptional. Error message when data.status is FAILED.

otpNotification Webhooks

Triggered by events happening on the delivery of the OTP (currently delivered via email).

subTypeDescription
EMAIL_DELIVERY_FAILEDThe email to deliver the OTP has bounced back.

data object structure:

{
  "failureType": "string",
  "emailAddress": "string",
  "diagnosticCode": "string",
  "timestamp": "ISO8601"
}