Appearance
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:
Events
Registration Events
| Event | Description |
|---|---|
token.minted | A new token registration has been claimed for a symbol on a chain |
token.released | A token owner has voluntarily released their registration |
token.expired | A registration has expired due to inactivity and is now claimable |
Heartbeat Events
| Event | Description |
|---|---|
heartbeat.warning | Token heartbeat score dropped below warning threshold |
heartbeat.critical | Token heartbeat score reached critical level |
heartbeat.recovered | Token heartbeat score recovered to healthy level |
heartbeat.change | Token heartbeat status transitioned (e.g. healthy → warning) |
Holder & Trading Events
| Event | Description |
|---|---|
holder.new | A new significant holder detected for a token |
trade.large | A large trade occurred (>5% volume or >$10k) |
concentration.alert | A single wallet's token concentration exceeded threshold |
milestone.reached | Token reached a holder count milestone |
Alert Events
| Event | Description |
|---|---|
alert.triggered | A health alert has fired |
alert.acknowledged | An alert has been acknowledged by the token owner |
alert.resolved | An alert has been resolved (manually or auto-recovered) |
Profile & Subscription Events
| Event | Description |
|---|---|
profile.updated | Token profile metadata was updated |
subscription.upgraded | User plan tier was upgraded |
subscription.downgraded | User plan tier was downgraded |
Usage & Quota Events
| Event | Description |
|---|---|
usage.threshold | API usage crossed 80% or 90% of your daily quota (generic — use the quota.* variants to filter by level) |
quota.threshold_warning | API usage crossed 80% of your daily quota. Fires at most once per day |
quota.exceeded | API usage crossed 90% of your daily quota. Fires at most once per day |
Community & Interest Events
| Event | Description |
|---|---|
community.member_joined | A wallet joined a token's community for the first time (holder subscription, gated-link unlock, etc.) |
community.member_left | A wallet was removed from a token's community (owner-initiated delete or self-unsubscribe) |
interest.expressed | Someone expressed interest in a token deal or feature |
Operational Events
| Event | Description |
|---|---|
ci.autofix.failed | A 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/eventsReturns { "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/webhooksjson
{
"url": "https://example.com/webhooks/chaindaddy",
"events": ["token.minted", "heartbeat.warning", "holder.new"],
"filter": {
"crown_ids": [123, 456],
"chains": ["eip155:42161", "eip155:8453"]
}
}| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Your HTTPS endpoint that receives POST requests |
events | string[] | Yes | Event types to subscribe to |
filter | object | No | Optional filter to scope events to specific tokens or chains |
filter.crown_ids | int[] | No | Only receive events for these token IDs (field name kept for API compatibility). Omit for all tokens |
filter.chains | string[] | No | Only 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:
| Tier | Max Webhooks |
|---|---|
| Developer | 5 |
| Partner | 25 |
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}), 200go
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": { ... }
}| Field | Type | Description |
|---|---|---|
id | string | Unique event ID (use for deduplication) |
type | string | Event type identifier |
created_at | string | ISO 8601 timestamp of when the event was created |
data | object | Event-specific payload (see below) |
HTTP Headers
Every webhook delivery includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Signature | HMAC-SHA256 hex digest of the request body |
X-Webhook-Event | Event type (e.g. token.minted) |
X-Webhook-Delivery | Unique delivery ID (same as id in payload) |
X-Webhook-Timestamp | ISO 8601 timestamp of the event |
User-Agent | ChainDaddy-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..."
}
}| Field | Type | Description |
|---|---|---|
crown_id | string | Token ID (field name preserved for API compatibility) |
symbol | string | Token symbol claimed |
chain | string | Chain identifier (e.g. eip155:42161) |
owner | string | Wallet address of the new token owner |
tx_hash | string | Transaction 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..."
}
}| Field | Type | Description |
|---|---|---|
crown_id | string | Token ID (field name preserved for API compatibility) |
symbol | string | Token symbol now available |
chain | string | Chain identifier |
previous_owner | string | Wallet 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..."
}
}| Field | Type | Description |
|---|---|---|
crown_id | string | Token ID (field name preserved for API compatibility) |
symbol | string | Token symbol |
chain | string | Chain identifier |
previous_owner | string | Previous 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
}
}| Field | Type | Description |
|---|---|---|
crown_id | string | Token ID (field name preserved for API compatibility) |
symbol | string | Token symbol |
chain | string | Chain identifier |
score | number | Current heartbeat score |
threshold | number | Warning 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
}
}| Field | Type | Description |
|---|---|---|
crown_id | string | Token ID (field name preserved for API compatibility) |
symbol | string | Token symbol |
chain | string | Chain identifier |
score | number | Current heartbeat score |
days_to_inactive | number | Estimated 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
}
}| Field | Type | Description |
|---|---|---|
crown_id | string | Token ID (field name preserved for API compatibility) |
symbol | string | Token symbol |
chain | string | Chain identifier |
score | number | Current (recovered) heartbeat score |
previous_score | number | Score 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"
}
}| Field | Type | Description |
|---|---|---|
crown_id | string | Token ID (field name preserved for API compatibility) |
symbol | string | Token symbol |
chain | string | Chain identifier |
previous_status | string | Status before transition |
new_status | string | Status after transition |
current_score | number | Current heartbeat score |
timestamp | string | ISO 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"
}
}| Field | Type | Description |
|---|---|---|
crown_id | string | Token ID (field name preserved for API compatibility) |
symbol | string | Token symbol |
chain | string | Chain identifier |
holder_address | string | Address of the new holder |
timestamp | string | ISO 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"
}
}| Field | Type | Description |
|---|---|---|
crown_id | string | Token ID (field name preserved for API compatibility) |
symbol | string | Token symbol |
chain | string | Chain identifier |
trade_volume_usd | number | Trade volume in USD |
direction | string | Trade direction (buy or sell) |
timestamp | string | ISO 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"
}
}| Field | Type | Description |
|---|---|---|
crown_id | string | Token ID (field name preserved for API compatibility) |
symbol | string | Token symbol |
chain | string | Chain identifier |
concentration_percent | number | Current concentration percentage |
previous_concentration | number | Previous concentration percentage |
threshold | number | Threshold that was exceeded |
timestamp | string | ISO 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"
}
}| Field | Type | Description |
|---|---|---|
crown_id | string | Token ID (field name preserved for API compatibility) |
symbol | string | Token symbol |
chain | string | Chain identifier |
milestone_type | string | Type of milestone reached |
milestone_value | number | Milestone threshold crossed |
previous_value | number | Value before crossing milestone |
timestamp | string | ISO 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"
}
}| Field | Type | Description |
|---|---|---|
instance_id | string | Alert instance ID |
wallet_address | string | Wallet that acknowledged the alert |
alert_type | string | Type of alert (e.g. heartbeat_critical) |
crown_symbol | string | Symbol of the affected token |
timestamp | string | ISO 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"
}
}| Field | Type | Description |
|---|---|---|
instance_id | string | Alert instance ID |
wallet_address | string | Wallet that owns the alert |
alert_type | string | Type of alert |
crown_symbol | string | Symbol of the affected token |
reason | string | Resolution reason (e.g. manual, auto_recovered) |
timestamp | string | ISO 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"]
}
}| Field | Type | Description |
|---|---|---|
crown_id | string | Token ID (field name preserved for API compatibility) |
symbol | string | Token symbol |
chain | string | Chain identifier |
updated_fields | string[] | 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"
}
}| Field | Type | Description |
|---|---|---|
wallet | string | Wallet address |
previous_tier | string | Tier before upgrade |
new_tier | string | Tier after upgrade |
subscription.downgraded
Fired when a user's plan tier is downgraded.
json
{
"data": {
"wallet": "0x1234...",
"previous_tier": "developer",
"new_tier": "free"
}
}| Field | Type | Description |
|---|---|---|
wallet | string | Wallet address |
previous_tier | string | Tier before downgrade |
new_tier | string | Tier 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:
| Retry | Delay | Cumulative Time |
|---|---|---|
| 1 | 1 minute | 1 minute |
| 2 | 5 minutes | 6 minutes |
| 3 | 30 minutes | 36 minutes |
| 4 | 2 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/webhooksbash
# 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/webhooksUsing the Test Endpoint
Send a test event to verify your endpoint receives and processes webhooks correctly:
POST /api/v2/webhooks/{id}/testThis 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/eventsReturns every subscribable event type with a description. See Event Discovery.
List Webhooks
GET /api/v2/webhooksReturns 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/webhooksjson
{
"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}/reactivateRe-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=0Returns 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}/testQueues a test delivery through the full webhook pipeline to verify your endpoint.
Best Practices
- Verify signatures: Always validate the
X-Webhook-Signatureheader 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