Skip to content

Webhooks

Receive real-time notifications when token events occur. Webhooks push event data to your server as they happen, eliminating the need to poll for changes.

Developer+ Tier Required

Webhooks are available on the Developer and Partner tiers. See Authentication for the full tier matrix.

A delivery in one picture — see Retry Policy for the details:

Sequence diagram of webhook delivery: the dispatcher POSTs the event payload with X-Webhook headers to your HTTPS endpoint; a 2xx response within 5 seconds acknowledges delivery; failures are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, and 12 hours, after which the delivery is marked exhausted; 3 consecutive failed deliveries auto-disable the webhookSequence diagram of webhook delivery: the dispatcher POSTs the event payload with X-Webhook headers to your HTTPS endpoint; a 2xx response within 5 seconds acknowledges delivery; failures are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, and 12 hours, after which the delivery is marked exhausted; 3 consecutive failed deliveries auto-disable the webhook

Events

Registration Events

EventDescription
token.mintedA new token registration has been claimed for a symbol on a chain
token.releasedA token owner has voluntarily released their registration
token.expiredA registration has expired due to inactivity and is now claimable

Heartbeat Events

EventDescription
heartbeat.warningToken heartbeat score dropped below warning threshold
heartbeat.criticalToken heartbeat score reached critical level
heartbeat.recoveredToken heartbeat score recovered to healthy level
heartbeat.changeToken heartbeat status transitioned (e.g. healthy → warning)

Holder & Trading Events

EventDescription
holder.newA new significant holder detected for a token
trade.largeA large trade occurred (>5% volume or >$10k)
concentration.alertA single wallet's token concentration exceeded threshold
milestone.reachedToken reached a holder count milestone

Alert Events

EventDescription
alert.triggeredA health alert has fired
alert.acknowledgedAn alert has been acknowledged by the token owner
alert.resolvedAn alert has been resolved (manually or auto-recovered)

Profile & Subscription Events

EventDescription
profile.updatedToken profile metadata was updated
subscription.upgradedUser plan tier was upgraded
subscription.downgradedUser plan tier was downgraded

Usage & Quota Events

EventDescription
usage.thresholdAPI usage crossed 80% or 90% of your daily quota (generic — use the quota.* variants to filter by level)
quota.threshold_warningAPI usage crossed 80% of your daily quota. Fires at most once per day
quota.exceededAPI usage crossed 90% of your daily quota. Fires at most once per day

Community & Interest Events

EventDescription
community.member_joinedA wallet joined a token's community for the first time (holder subscription, gated-link unlock, etc.)
community.member_leftA wallet was removed from a token's community (owner-initiated delete or self-unsubscribe)
interest.expressedSomeone expressed interest in a token deal or feature

Operational Events

EventDescription
ci.autofix.failedA CI auto-fix attempt failed (internal ops signal for app developers)

Event Discovery

Fetch the authoritative list of subscribable event types (with descriptions) at runtime:

GET /api/v2/webhooks/events

Returns { "events": [{ "type": "token.minted", "description": "…" }, …] }. Use this to build subscription UIs or to defensively validate event names before registering.

Setup

1. Register a Webhook Endpoint

POST /api/v2/webhooks
json
{
  "url": "https://example.com/webhooks/chaindaddy",
  "events": ["token.minted", "heartbeat.warning", "holder.new"],
  "filter": {
    "crown_ids": [123, 456],
    "chains": ["eip155:42161", "eip155:8453"]
  }
}
FieldTypeRequiredDescription
urlstringYesYour HTTPS endpoint that receives POST requests
eventsstring[]YesEvent types to subscribe to
filterobjectNoOptional filter to scope events to specific tokens or chains
filter.crown_idsint[]NoOnly receive events for these token IDs (field name kept for API compatibility). Omit for all tokens
filter.chainsstring[]NoOnly receive events for these chains (e.g. eip155:42161). Omit for all chains

The response includes a secret field, save this immediately. It is only shown once and is used to verify webhook signatures.

HTTPS Required

Webhook endpoints must use HTTPS. HTTP endpoints are rejected during registration.

Webhook limits per tier:

TierMax Webhooks
Developer5
Partner25

2. Verify Payload Signatures

Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 signature of the raw request body, signed with your webhook secret.

javascript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Express middleware example
app.post('/webhooks/chaindaddy', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const isValid = verifyWebhookSignature(
    req.body,
    signature,
    process.env.WEBHOOK_SECRET
  );

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(req.body);
  console.log(`Received ${event.type}`, event.data);

  res.status(200).json({ received: true });
});
python
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

def verify_signature(payload, signature, secret):
    expected = hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

@app.route('/webhooks/chaindaddy', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Webhook-Signature')
    is_valid = verify_signature(
        request.get_data(as_text=True),
        signature,
        os.environ['WEBHOOK_SECRET']
    )

    if not is_valid:
        return jsonify({'error': 'Invalid signature'}), 401

    event = request.json
    print(f'Received {event["type"]}', event['data'])

    return jsonify({'received': True}), 200
go
func verifySignature(payload []byte, signature, secret string) bool {
	h := hmac.New(sha256.New, []byte(secret))
	h.Write(payload)
	expected := hex.EncodeToString(h.Sum(nil))
	return hmac.Equal([]byte(signature), []byte(expected))
}

3. Respond with 2xx

Your endpoint must respond with a 2xx status code within 5 seconds to acknowledge receipt. Any other status code or timeout triggers a retry.

Payload Format

All webhook payloads share a common envelope:

json
{
  "id": "evt_token.minted_1708617600000000000",
  "type": "token.minted",
  "created_at": "2026-02-22T12:00:00Z",
  "data": { ... }
}
FieldTypeDescription
idstringUnique event ID (use for deduplication)
typestringEvent type identifier
created_atstringISO 8601 timestamp of when the event was created
dataobjectEvent-specific payload (see below)

HTTP Headers

Every webhook delivery includes these headers:

HeaderDescription
Content-Typeapplication/json
X-Webhook-SignatureHMAC-SHA256 hex digest of the request body
X-Webhook-EventEvent type (e.g. token.minted)
X-Webhook-DeliveryUnique delivery ID (same as id in payload)
X-Webhook-TimestampISO 8601 timestamp of the event
User-AgentChainDaddy-Webhook/1.0

Event Payloads

token.minted

Fired when a new token registration is claimed for a symbol on a chain.

json
{
  "id": "evt_token.minted_1708617600000000000",
  "type": "token.minted",
  "created_at": "2026-02-22T12:00:00Z",
  "data": {
    "crown_id": "42",
    "symbol": "DOGE",
    "chain": "eip155:42161",
    "owner": "0x1234567890abcdef1234567890abcdef12345678",
    "tx_hash": "0xabcdef..."
  }
}
FieldTypeDescription
crown_idstringToken ID (field name preserved for API compatibility)
symbolstringToken symbol claimed
chainstringChain identifier (e.g. eip155:42161)
ownerstringWallet address of the new token owner
tx_hashstringTransaction hash of the mint

token.released

Fired when a token owner voluntarily releases their registration.

json
{
  "data": {
    "crown_id": "42",
    "symbol": "DOGE",
    "chain": "eip155:42161",
    "previous_owner": "0x1234..."
  }
}
FieldTypeDescription
crown_idstringToken ID (field name preserved for API compatibility)
symbolstringToken symbol now available
chainstringChain identifier
previous_ownerstringWallet that released the registration

token.expired

Fired when a registration expires due to inactivity and becomes claimable.

json
{
  "data": {
    "crown_id": "42",
    "symbol": "DOGE",
    "chain": "eip155:42161",
    "previous_owner": "0x1234..."
  }
}
FieldTypeDescription
crown_idstringToken ID (field name preserved for API compatibility)
symbolstringToken symbol
chainstringChain identifier
previous_ownerstringPrevious owner whose registration expired

heartbeat.warning

Fired when a token's heartbeat score drops below the warning threshold.

json
{
  "data": {
    "crown_id": "42",
    "symbol": "DOGE",
    "chain": "eip155:42161",
    "score": 55,
    "threshold": 60
  }
}
FieldTypeDescription
crown_idstringToken ID (field name preserved for API compatibility)
symbolstringToken symbol
chainstringChain identifier
scorenumberCurrent heartbeat score
thresholdnumberWarning threshold that was crossed

heartbeat.critical

Fired when a token's heartbeat score reaches critical level.

json
{
  "data": {
    "crown_id": "42",
    "symbol": "DOGE",
    "chain": "eip155:42161",
    "score": 25,
    "days_to_inactive": 3
  }
}
FieldTypeDescription
crown_idstringToken ID (field name preserved for API compatibility)
symbolstringToken symbol
chainstringChain identifier
scorenumberCurrent heartbeat score
days_to_inactivenumberEstimated days until the registration goes inactive

heartbeat.recovered

Fired when a token's heartbeat score recovers to healthy level.

json
{
  "data": {
    "crown_id": "42",
    "symbol": "DOGE",
    "chain": "eip155:42161",
    "score": 85,
    "previous_score": 45
  }
}
FieldTypeDescription
crown_idstringToken ID (field name preserved for API compatibility)
symbolstringToken symbol
chainstringChain identifier
scorenumberCurrent (recovered) heartbeat score
previous_scorenumberScore before recovery

heartbeat.change

Fired on any heartbeat status transition (e.g. healthy → warning, warning → critical).

json
{
  "data": {
    "crown_id": "42",
    "symbol": "DOGE",
    "chain": "eip155:42161",
    "previous_status": "healthy",
    "new_status": "warning",
    "current_score": 58,
    "timestamp": "2026-02-22T12:00:00Z"
  }
}
FieldTypeDescription
crown_idstringToken ID (field name preserved for API compatibility)
symbolstringToken symbol
chainstringChain identifier
previous_statusstringStatus before transition
new_statusstringStatus after transition
current_scorenumberCurrent heartbeat score
timestampstringISO 8601 timestamp of the transition

holder.new

Fired when a new significant holder is detected for a registered token.

json
{
  "data": {
    "crown_id": "42",
    "symbol": "DOGE",
    "chain": "eip155:42161",
    "holder_address": "0xabcd...",
    "timestamp": "2026-02-22T12:00:00Z"
  }
}
FieldTypeDescription
crown_idstringToken ID (field name preserved for API compatibility)
symbolstringToken symbol
chainstringChain identifier
holder_addressstringAddress of the new holder
timestampstringISO 8601 timestamp

trade.large

Fired when a large trade occurs (>5% of daily volume or >$10k).

json
{
  "data": {
    "crown_id": "42",
    "symbol": "DOGE",
    "chain": "eip155:42161",
    "trade_volume_usd": 15000.50,
    "direction": "buy",
    "timestamp": "2026-02-22T12:00:00Z"
  }
}
FieldTypeDescription
crown_idstringToken ID (field name preserved for API compatibility)
symbolstringToken symbol
chainstringChain identifier
trade_volume_usdnumberTrade volume in USD
directionstringTrade direction (buy or sell)
timestampstringISO 8601 timestamp

concentration.alert

Fired when a single wallet's token concentration exceeds the threshold.

json
{
  "data": {
    "crown_id": "42",
    "symbol": "DOGE",
    "chain": "eip155:42161",
    "concentration_percent": 82.5,
    "previous_concentration": 70.0,
    "threshold": 75.0,
    "timestamp": "2026-02-22T12:00:00Z"
  }
}
FieldTypeDescription
crown_idstringToken ID (field name preserved for API compatibility)
symbolstringToken symbol
chainstringChain identifier
concentration_percentnumberCurrent concentration percentage
previous_concentrationnumberPrevious concentration percentage
thresholdnumberThreshold that was exceeded
timestampstringISO 8601 timestamp

milestone.reached

Fired when a token reaches a holder count milestone (e.g. 100, 500, 1000 holders).

json
{
  "data": {
    "crown_id": "42",
    "symbol": "DOGE",
    "chain": "eip155:42161",
    "milestone_type": "holder_count",
    "milestone_value": 1000,
    "previous_value": 999,
    "timestamp": "2026-02-22T12:00:00Z"
  }
}
FieldTypeDescription
crown_idstringToken ID (field name preserved for API compatibility)
symbolstringToken symbol
chainstringChain identifier
milestone_typestringType of milestone reached
milestone_valuenumberMilestone threshold crossed
previous_valuenumberValue before crossing milestone
timestampstringISO 8601 timestamp

alert.triggered

Fired when a health alert is triggered for a crown.

The data payload varies by alert type and includes title, body, action_url, action_label, symbol, and optional crown_id / chain_id fields.

alert.acknowledged

Fired when an alert is acknowledged by the token owner.

json
{
  "data": {
    "instance_id": "alert_abc123",
    "wallet_address": "0x1234...",
    "alert_type": "heartbeat_critical",
    "crown_symbol": "DOGE",
    "timestamp": "2026-02-22T12:00:00Z"
  }
}
FieldTypeDescription
instance_idstringAlert instance ID
wallet_addressstringWallet that acknowledged the alert
alert_typestringType of alert (e.g. heartbeat_critical)
crown_symbolstringSymbol of the affected token
timestampstringISO 8601 timestamp

alert.resolved

Fired when an alert is resolved (manually or by auto-recovery).

json
{
  "data": {
    "instance_id": "alert_abc123",
    "wallet_address": "0x1234...",
    "alert_type": "heartbeat_critical",
    "crown_symbol": "DOGE",
    "reason": "manual",
    "timestamp": "2026-02-22T12:00:00Z"
  }
}
FieldTypeDescription
instance_idstringAlert instance ID
wallet_addressstringWallet that owns the alert
alert_typestringType of alert
crown_symbolstringSymbol of the affected token
reasonstringResolution reason (e.g. manual, auto_recovered)
timestampstringISO 8601 timestamp

profile.updated

Fired when a token's profile metadata is updated.

json
{
  "data": {
    "crown_id": "42",
    "symbol": "DOGE",
    "chain": "eip155:42161",
    "updated_fields": ["description", "avatar", "links"]
  }
}
FieldTypeDescription
crown_idstringToken ID (field name preserved for API compatibility)
symbolstringToken symbol
chainstringChain identifier
updated_fieldsstring[]List of fields that changed

subscription.upgraded

Fired when a user's plan tier is upgraded.

json
{
  "data": {
    "wallet": "0x1234...",
    "previous_tier": "free",
    "new_tier": "developer"
  }
}
FieldTypeDescription
walletstringWallet address
previous_tierstringTier before upgrade
new_tierstringTier after upgrade

subscription.downgraded

Fired when a user's plan tier is downgraded.

json
{
  "data": {
    "wallet": "0x1234...",
    "previous_tier": "developer",
    "new_tier": "free"
  }
}
FieldTypeDescription
walletstringWallet address
previous_tierstringTier before downgrade
new_tierstringTier after downgrade

Filtering

Webhook subscriptions support optional filtering to scope events to specific tokens or chains. This reduces noise and lets you build targeted integrations.

Per-Token Filtering

Subscribe to events for specific tokens only:

json
{
  "url": "https://example.com/webhook",
  "events": ["heartbeat.warning", "heartbeat.critical"],
  "filter": {
    "crown_ids": [42, 108]
  }
}

This webhook only fires for heartbeat events on token IDs 42 and 108.

Per-Chain Filtering

Subscribe to events on specific chains only:

json
{
  "url": "https://example.com/webhook",
  "events": ["token.minted", "holder.new"],
  "filter": {
    "chains": ["eip155:42161", "eip155:8453"]
  }
}

This webhook only fires for minting and new holder events on Arbitrum and Base.

Combined Filtering

Combine both filters for precise targeting:

json
{
  "filter": {
    "crown_ids": [42],
    "chains": ["eip155:42161"]
  }
}

Omitting a filter field (or omitting the filter object entirely) means no filtering on that dimension, you receive events for all tokens and/or all chains.

TIP

Events without token context (e.g. subscription.upgraded, subscription.downgraded) are always delivered regardless of token/chain filters.

Retry Policy

The initial delivery is immediate. Failed deliveries are retried up to 5 times with exponential backoff:

RetryDelayCumulative Time
11 minute1 minute
25 minutes6 minutes
330 minutes36 minutes
42 hours~2.5 hours
5 (final)12 hours~14.5 hours

After all attempts fail, the delivery is marked as exhausted and logged in your delivery history.

A delivery is considered failed if:

  • Your endpoint returns a non-2xx status code
  • The connection times out (5-second limit)
  • DNS resolution or TLS handshake fails

Auto-disable: After 3 consecutive failed deliveries across any events, your webhook is automatically disabled to prevent further failures. Use the reactivate endpoint to re-enable it after fixing the issue.

Duplicate Events

Events may be delivered more than once during retries. Use the id field for deduplication in your handler.

Testing Locally

Using a Tunnel Service

Forward webhook traffic to your local development server using an HTTPS tunnel:

bash
# Start tunnel to local port 3000
ngrok http 3000

# Copy the https://xxxx.ngrok.io URL and use it as the "url"
# when registering via POST /api/v2/webhooks
bash
# Start tunnel to local port 3000
npx localtunnel --port 3000

# Copy the https://xxxx.loca.lt URL and use it as the "url"
# when registering via POST /api/v2/webhooks

Using the Test Endpoint

Send a test event to verify your endpoint receives and processes webhooks correctly:

POST /api/v2/webhooks/{id}/test

This queues a real delivery through the webhook pipeline with a test payload. Your endpoint receives a token.minted event with sample data, delivered with a valid HMAC signature.

Manual cURL Testing

Simulate a webhook delivery to your local endpoint:

bash
curl -X POST http://localhost:3000/webhooks/chaindaddy \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: test_signature_skip_verification" \
  -H "X-Webhook-Event: token.minted" \
  -d '{
    "id": "evt_token.minted_test",
    "type": "token.minted",
    "created_at": "2026-02-22T12:00:00Z",
    "data": {
      "crown_id": "1",
      "symbol": "TEST",
      "chain": "eip155:421614",
      "owner": "0x0000000000000000000000000000000000000001",
      "tx_hash": "0x0000000000000000000000000000000000000000000000000000000000000000"
    }
  }'

Managing Webhooks

List Event Types

GET /api/v2/webhooks/events

Returns every subscribable event type with a description. See Event Discovery.

List Webhooks

GET /api/v2/webhooks

Returns all webhook subscriptions for the authenticated wallet, including filter configuration and delivery stats.

Get Webhook

GET /api/v2/webhooks/{id}

Create Webhook

POST /api/v2/webhooks
json
{
  "url": "https://example.com/webhook",
  "events": ["token.minted", "heartbeat.warning"],
  "filter": {
    "crown_ids": [42],
    "chains": ["eip155:42161"]
  }
}

Update Webhook

PATCH /api/v2/webhooks/{id}

Update the endpoint URL, subscribed events, active status, or filters:

json
{
  "url": "https://new-endpoint.example.com/webhook",
  "events": ["token.minted", "token.released"],
  "active": true,
  "filter": {
    "crown_ids": [42, 108],
    "chains": ["eip155:42161"]
  }
}

Delete Webhook

DELETE /api/v2/webhooks/{id}

Reactivate Webhook

POST /api/v2/webhooks/{id}/reactivate

Re-enables a webhook that was auto-disabled due to consecutive delivery failures. Resets the failure counter.

View Delivery History

GET /api/v2/webhooks/{id}/deliveries?limit=50&offset=0

Returns delivery attempts with status (delivered, failed, exhausted), timestamps, status codes, and error messages. Limit range: 20-100 (default: 20).

Send Test Event

POST /api/v2/webhooks/{id}/test

Queues a test delivery through the full webhook pipeline to verify your endpoint.

Best Practices

  • Verify signatures: Always validate the X-Webhook-Signature header before processing
  • Respond quickly: Return 2xx immediately, then process events asynchronously via a queue
  • Handle duplicates: Store processed event IDs and skip duplicates
  • Use filters: Scope webhooks to specific tokens or chains to reduce noise
  • Monitor delivery health: Check delivery history for persistent failures
  • Subscribe selectively: Only subscribe to events you need
  • Set up alerting: Configure your own alerts for repeated delivery failures