Skip to content

Webhooks

Holyheld uses webhooks to notify integrations about important events such as risk assessment updates and settlement status changes.

During integration setup you must provide an HTTPS endpoint that will receive webhook requests.

Verifying webhook requests

Always verify incoming webhooks

Never process a webhook payload without first confirming it came from Holyheld. Without verification, an attacker could send forged events to your endpoint and trigger unintended actions in your system.

Holyheld signs every webhook request with your API key value. The X-Api-Key header is included in every delivery:

X-Api-Key: <your API key>

Before processing any event, confirm that the header value matches your configured API key:

javascript
// Node.js / Express example
app.post('/webhook', (req, res) => {
  const receivedKey = req.headers['X-Api-Key'];

  if (!receivedKey || receivedKey !== process.env.BRRR_API_KEY) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  // Safe to process the event
  const event = req.body;
  console.log('Received event:', event.type);

  res.status(200).send('OK');
});
python
# Python / Flask example
from flask import Flask, request, abort
import os

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    received_key = request.headers.get('X-Api-Key')

    if received_key != os.environ['BRRR_API_KEY']:
        abort(401)

    event = request.json
    print('Received event:', event['type'])

    return 'OK', 200

Return HTTP 200 to acknowledge successful receipt. Any other status code (or a network timeout) will cause BRRR to retry the delivery.

Delivery and retries

BRRR expects your endpoint to return an HTTP 2xx status within 10 seconds. If your endpoint returns any other status code, returns an error, or does not respond in time, the delivery is considered failed and BRRR will retry.

Retry schedule:

AttemptDelay after previous failure
1st retry5 minutes
2nd retry30 minutes
3rd retry2 hours
4th retry24 hours

After the 4th retry (approximately 26 hours after the initial delivery attempt), the event is dropped. Contact support@holyheld.com if you need a missed event replayed.

Make your handler idempotent

Because retries can cause the same event to be delivered more than once, always check whether you have already processed an event before taking action. Use quoteId (for settlement events) or a combination of type + timestamp + payload identifier as your deduplication key.

Event ordering

Webhook events are not guaranteed to arrive in strict chronological order. Network conditions, retry schedules, and concurrent processing mean that a later event may be delivered before an earlier one.

For example, you may receive SETTLEMENT_STATUS_CHANGE with newStatus: "FINISHED" before you receive the earlier newStatus: "CONFIRMED" event.

Recommended approach — use timestamp as ground truth:

Always compare the timestamp field in the incoming event against the last known timestamp you have stored for that resource. If the incoming event is older than what you have already processed, discard it.

javascript
// Example: idempotent status handler
async function handleSettlementStatusChange(event) {
  const { quoteId, newStatus } = event.payload;
  const eventTimestamp = event.timestamp;

  const stored = await db.getSettlement(quoteId);

  // Discard out-of-order events
  if (stored && stored.lastEventTimestamp >= eventTimestamp) {
    console.log('Ignoring stale event for', quoteId);
    return;
  }

  await db.updateSettlement(quoteId, {
    status: newStatus,
    lastEventTimestamp: eventTimestamp,
  });
}

Never rely on event order for state transitions

Design your handler to accept any status transition, not just the expected sequence. Always fetch the current status via Get Settlement Status if your system state is uncertain.

Event format

All webhook events follow the same structure:

json
{
  "type": "EVENT_TYPE",
  "timestamp": 1724247261,
  "payload": {}
}

Event catalogue

Event typeEmitted byTriggered when
RISK_ASSESSMENTPartner SettlementRisk evaluation completes for a monitored wallet address
SETTLEMENT_STATUS_CHANGEPartner SettlementSettlement moves through its lifecycle (CREATEDCONFIRMEDFINISHED)
IBAN_REGISTEREDPartner SettlementA partner customer's IBAN is registered via Add IBAN to a Customer
IBAN_REMOVEDPartner SettlementA partner customer's IBAN is removed
OTC_ORDER_STATUS_CHANGEOTC APIAn OTC order changes state (CONFIRMED, PROCESSING, SETTLED, etc.)
OFFRAMP_STATUS_CHANGECard APIOfframp (crypto-to-card or crypto-to-SEPA) transitions state
ONRAMP_STATUS_CHANGECard APIOnramp (buy crypto) transitions state
SEPA_TRANSFER_STATUS_CHANGECard APIA SEPA transfer leg moves from queued → sent → settled
GASLESS_TX_BROADCASTCard APIA gasless offramp transaction has been broadcast on-chain
CARD_TOPUP_RECEIVEDCard APIFunds from an offramp reached the customer's card balance
TAG_HASH_EXPIREDCard APIA one-time Tag Hash expired without being consumed

Event types

RISK_ASSESSMENT

Sent when a risk evaluation is completed for a monitored wallet address.

json
{
  "type": "RISK_ASSESSMENT",
  "timestamp": 1724247261,
  "payload": {
    "customerId": "<customer ID>",
    "addressEVM": "<customer address>",
    "risk": "LOW",
    "reviewTimestamp": 1724247261,
    "reviewType": "TOPUP", // "INITIAL" - for initial wallet registration, "TOPUP" - for activity
    "reviewComment": "comment", // optional
    "reviewData": { // optional
      "fromAddress": "0x26b92eD884B9FE3f572252ee78172BfBC1653dC1",
      "amount": "10000",
      "totalAmount": "754699"
    }
  }
}

SETTLEMENT_STATUS_CHANGE

Sent when the status of a settlement changes.

json
{
  "type": "SETTLEMENT_STATUS_CHANGE",
  "timestamp": 1724247261,
  "payload": {
    "quoteId": "<quote ID>",
    "oldStatus": "CREATED",
    "newStatus": "CONFIRMED"
  }
}

IBAN_REGISTERED

Sent when a partner customer's IBAN is successfully registered via Add IBAN to a Customer.

json
{
  "type": "IBAN_REGISTERED",
  "timestamp": 1724247261,
  "payload": {
    "customerId": "cust_a1b2c3d4",
    "ibanId": "iban_01HXYZ123456",
    "iban": "DE89370400440532013000",
    "beneficiaryName": "John Doe",
    "bankName": "Commerzbank",
    "bankCountry": "DE"
  }
}

IBAN_REMOVED

Sent when a partner customer's IBAN is removed.

json
{
  "type": "IBAN_REMOVED",
  "timestamp": 1724247261,
  "payload": {
    "customerId": "cust_a1b2c3d4",
    "ibanId": "iban_01HXYZ123456"
  }
}

OTC_ORDER_STATUS_CHANGE

Sent when an OTC order moves between states.

json
{
  "type": "OTC_ORDER_STATUS_CHANGE",
  "timestamp": 1724247261,
  "payload": {
    "orderId": "F0E2D8B3-1A4C-4F6E-9D5B-8C7F3E2A1B0D",
    "orderType": "SELL_CRYPTO", // "SELL_CRYPTO" | "BUY_EUR"
    "oldStatus": "CONFIRMED",
    "newStatus": "PROCESSING",
    "network": "ethereum",
    "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
    "tokenAmount": "10245.182341",
    "fiatAmount": "9500.00",
    "chainTxHash": "0xabcd..." // present once crypto leg is broadcast
  }
}

OFFRAMP_STATUS_CHANGE

Sent as a Card API offramp (crypto to card or crypto to SEPA) moves between lifecycle states — the same states returned by Get Offramp Status.

json
{
  "type": "OFFRAMP_STATUS_CHANGE",
  "timestamp": 1724247261,
  "payload": {
    "HHTXID": "F0E2D8B3-1A4C-4F6E-9D5B-8C7F3E2A1B0D",
    "oldState": "QUEUED",
    "newState": "PENDING", // WAITFORTX | QUEUED | PENDING | EXECUTING | SUCCESS | CANCELLED | FAILED
    "destination": {
      "type": "SEPA", // "CARD" | "SEPA" | "TAG"
      "iban": "DE89370400440532013000"
    },
    "tokenAmount": "10",
    "EURAmount": "18032.06",
    "chainId": 1,
    "chainTxHash": "0x9c8b7a..." // present once on-chain tx is observed
  }
}

ONRAMP_STATUS_CHANGE

Sent as a Card API onramp (buy crypto) moves between states — the same states returned by Get Onramp Status.

json
{
  "type": "ONRAMP_STATUS_CHANGE",
  "timestamp": 1724247261,
  "payload": {
    "HHTXID": "5721128E-DDB3-4132-9791-39D91D022D61",
    "oldStatus": "approved",
    "newStatus": "success", // not_approved | approved | success | failed | declined
    "chainId": 1,
    "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "amountEUR": "100.00",
    "txHash": "0x1234abcd...", // present when newStatus = "success"
    "reason": "unknown" // present when newStatus = "failed"
  }
}

SEPA_TRANSFER_STATUS_CHANGE

Sent when a SEPA transfer associated with a Card API offramp moves state.

json
{
  "type": "SEPA_TRANSFER_STATUS_CHANGE",
  "timestamp": 1724247261,
  "payload": {
    "HHTXID": "F0E2D8B3-1A4C-4F6E-9D5B-8C7F3E2A1B0D",
    "iban": "DE89370400440532013000",
    "beneficiaryName": "John Doe",
    "eurAmount": "499.65",
    "feeAmount": "0.35",
    "oldStatus": "PENDING",
    "newStatus": "EXECUTING" // PENDING | EXECUTING | SUCCESS | FAILED
  }
}

GASLESS_TX_BROADCAST

Sent when a gasless offramp (Execute Gasless Transaction) has been broadcast on-chain on the customer's behalf.

json
{
  "type": "GASLESS_TX_BROADCAST",
  "timestamp": 1724247261,
  "payload": {
    "HHTXID": "F0E2D8B3-1A4C-4F6E-9D5B-8C7F3E2A1B0D",
    "chainId": 1,
    "chainTxHash": "0x9c8b7a...",
    "fromAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0Ad",
    "tokenAmount": "10",
    "EURAmount": "18032.06"
  }
}

CARD_TOPUP_RECEIVED

Sent when funds from an offramp top-up have been credited to the customer's card balance.

json
{
  "type": "CARD_TOPUP_RECEIVED",
  "timestamp": 1724247261,
  "payload": {
    "HHTXID": "F0E2D8B3-1A4C-4F6E-9D5B-8C7F3E2A1B0D",
    "tagName": "SDKTEST",
    "EURAmount": "18032.06",
    "source": {
      "type": "CRYPTO", // "CRYPTO" | "SEPA"
      "chainId": 1,
      "chainTxHash": "0x9c8b7a..."
    }
  }
}

TAG_HASH_EXPIRED

Sent when a one-time Tag Hash (issued by Get Tag Hash or Create SEPA Transfer) expired before being consumed by an offramp transaction.

json
{
  "type": "TAG_HASH_EXPIRED",
  "timestamp": 1724247261,
  "payload": {
    "tagHash": "0xabc...def",
    "issuedAt": 1724246661,
    "expiredAt": 1724247261
  }
}