Skip to content

Webhooks ​

A webhook is a URL on your server that Chain Daddy sends a POST request to when something happens: a token is registered, an alert fires, a deal is accepted, an item sells in your store. You get the event within seconds instead of asking for it again and again.

Plan

Webhooks need the Developer plan (5 webhooks), which is in beta and free while it is, or a Partner plan (25). A store's own webhook does not: it comes with the store, on any paid plan. See the API feature matrix.

Managing webhooks needs only your API key. Every route and field is in the API Explorer under Webhooks; you can also manage them at chaindaddy.io/_api.

Set one up ​

1. Create it ​

bash
curl -X POST https://api.chaindaddy.io/api/v2/webhooks \
  -H "X-API-Key: $CHAINDADDY_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/chaindaddy",
    "events": ["token.minted", "heartbeat.warning"]
  }'
json
{
  "success": true,
  "webhook": {
    "id": "b2a1…",
    "url": "https://example.com/webhooks/chaindaddy",
    "secret": "whsec_4f1c…",
    "events": ["token.minted", "heartbeat.warning"],
    "active": true,
    "failure_count": 0
  }
}

Save secret now. It is shown only in this response, and you need it to check signatures. url must be https:// on a public address. An event type that does not exist is a 400; creating more webhooks than your plan allows is a 403 with webhook_limit_reached.

2. Check the signature ​

Every delivery is signed the Standard Webhooks way, so any Standard Webhooks library checks it with the secret exactly as you saved it, whsec_ included:

HeaderValue
webhook-idThe event's id, the same on every retry of it
webhook-timestampWhen this attempt was sent, in unix seconds
webhook-signaturev1, and the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}

The HMAC key is the base64-decode of what follows whsec_. Sign the raw bytes you received, before any JSON parsing: a re-serialised body will not match. Because the timestamp is signed, you can refuse a delivery older than a few minutes, and a captured one cannot be replayed later.

javascript
const { Webhook } = require('standardwebhooks');   // npm i standardwebhooks
const wh = new Webhook(process.env.WEBHOOK_SECRET);   // the secret as you saved it, whsec_ included

app.post('/webhooks/chaindaddy', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try {
    event = wh.verify(req.body, req.headers);      // checks the signature and refuses a stale timestamp
  } catch {
    return res.status(401).end();
  }
  res.status(200).end();                 // answer first
  console.log(event.type, event.data);   // then do the work
});
python
import os
from flask import Flask, request
from standardwebhooks.webhooks import Webhook   # pip install standardwebhooks

app = Flask(__name__)
wh = Webhook(os.environ['WEBHOOK_SECRET'])

@app.route('/webhooks/chaindaddy', methods=['POST'])
def handle():
    try:
        event = wh.verify(request.get_data(), request.headers)
    except Exception:
        return '', 401
    print(event['type'], event['data'])
    return '', 200
go
func verify(body []byte, h http.Header, secret string) bool {
	key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
	if err != nil {
		return false
	}
	ts, err := strconv.ParseInt(h.Get("webhook-timestamp"), 10, 64)
	if err != nil || math.Abs(float64(time.Now().Unix()-ts)) > 300 {
		return false // missing, or more than 5 minutes off
	}
	mac := hmac.New(sha256.New, key)
	mac.Write([]byte(h.Get("webhook-id") + "." + h.Get("webhook-timestamp") + "."))
	mac.Write(body)
	want := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil))
	for _, sig := range strings.Fields(h.Get("webhook-signature")) {
		if hmac.Equal([]byte(sig), []byte(want)) {
			return true
		}
	}
	return false
}

Retries make the same event arrive more than once; skip a webhook-id you have already handled. A whole receiver that does all of this with no framework (Node's own http and the same library) is in Take payments on your site: A complete handler.

Don't want to trust the webhook alone? A store payment can be verified on chain, with no trust in us at all.

The older X-Webhook-Signature header is deprecated and is sent until 2027-10-31. It is the hex HMAC-SHA256 of the body alone, keyed with the whole secret string (whsec_ included). It signs no timestamp, so it cannot tell a fresh delivery from a replayed one. If you check it today, move to webhook-signature before that date.

3. Answer with a 2xx within 5 seconds ​

Any other status, or no answer in 5 seconds, counts as a failed delivery. Answer first and do slow work afterwards.

4. Send yourself a test ​

bash
curl -X POST https://api.chaindaddy.io/api/v2/webhooks/WEBHOOK_ID/test \
  -H "X-API-Key: $CHAINDADDY_API_KEY"
# {"success":true,"message":"…","event":{"id":"evt_test.ping_…","type":"test.ping","created_at":"…","data":{…}}}

This queues a real, signed delivery of a test.ping event to your URL. To try a real event's shape, send a body: {"eventType": "heartbeat.warning", "data": {…}}. Then read what happened:

bash
curl -H "X-API-Key: $CHAINDADDY_API_KEY" \
  "https://api.chaindaddy.io/api/v2/webhooks/WEBHOOK_ID/deliveries?limit=20"
# {"deliveries":[{"id":"…","event_type":"test.ping","status":"delivered","status_code":200,"attempt":1,…}]}

Your server has to be reachable from the internet. To test against your own computer, open a tunnel (ngrok http 3000, or npx localtunnel --port 3000) and use its https:// URL as the webhook's url.

What a delivery looks like ​

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…"
  }
}
HeaderValue
webhook-idThe event's id, the same on every retry of it
webhook-timestampWhen this attempt was sent (unix seconds)
webhook-signaturev1, + base64 HMAC-SHA256 of id.timestamp.body (check it)
X-Webhook-EventThe event type, e.g. token.minted
X-Webhook-DeliveryThe event's id (same as webhook-id)
X-Webhook-TimestampWhen the event was created (ISO 8601); unsigned
X-Webhook-SignatureDeprecated, sent until 2027-10-31: hex HMAC-SHA256 of the body alone
User-AgentChainDaddy-Webhook/1.0

crown_id is the token's registration id, and chain is a CAIP-2 id (eip155:8453 is Base).

When a delivery fails ​

A delivery fails when your endpoint answers anything but a 2xx, takes more than 5 seconds, or cannot be reached.

Every event is written down before it is sent, so nothing is lost while your endpoint is down or while we deploy.

  • A failed event is tried again 1 minute later, then 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, 24 hours and 24 hours after that: 9 attempts over about 3 days. After the last one its status is exhausted, and you can still send it again.
  • A webhook that has failed for 72 hours is switched off. The clock starts at its first failure and a success stops it, so an endpoint that is down for a day loses nothing: its retries arrive when it is back. At the first failure more than 72 hours in, nothing more is sent and the account that owns the webhook is notified. A test delivery is tried once and never counts toward this.
  • Switch it back on once your endpoint is fixed. Everything that was waiting is sent at once:
bash
curl -X POST https://api.chaindaddy.io/api/v2/webhooks/WEBHOOK_ID/reactivate \
  -H "X-API-Key: $CHAINDADDY_API_KEY"

Because of retries, the same event can arrive more than once. Keep the ids you have handled and skip repeats.

Send a delivery again ​

Any past delivery, delivered or given up on, can be sent again. It goes out with a fresh set of attempts (up to 9 over about 3 days: after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, then a day and another day) and the same event id, so webhook-id repeats and your de-duplication skips it if you already handled it:

bash
curl -X POST https://api.chaindaddy.io/api/v2/webhooks/WEBHOOK_ID/deliveries/DELIVERY_ID/redeliver \
  -H "X-API-Key: $CHAINDADDY_API_KEY"
# {"queued":true,"eventId":"evt_token.minted_…"}

DELIVERY_ID is a delivery's id from GET /api/v2/webhooks/WEBHOOK_ID/deliveries. Ten redeliveries a minute.

A creator store catches up from its event feed instead: POST /api/v2/iap/stores/{id}/webhook/redeliver {"after": 0, "limit": 1000} sends the store's events after after to the store's webhook again (only the event types the webhook is subscribed to), under their original ids, including any missed while the webhook was off. It answers {queued, last}; pass last as after to continue. A store key needs events:read; the CLI is chaindaddy iap webhook redeliver <store> --after <id>.

Receive only some events ​

Add a filter when you create or update a webhook:

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

crown_ids keeps events about those registrations, chains keeps events on those chains, and both together keep events that match both. Leave a list out for no filtering on it. Events with no token, such as subscription.upgraded, are always delivered.

Events about your account reach only your own webhooks: your alerts, your API usage, your plan, your coin's community, your deals and your store. Events about a token's public record (registrations, heartbeat, holders, trades, milestones, profile updates) reach every webhook subscribed to them, narrowed by its filter.

When you update a webhook, events replaces the whole list. Two old types, concentration.alert and ci.autofix.failed, are dropped when you save, so a list you read back can be saved as it is. Holder concentration now arrives as alert.triggered from a top10_concentration alert rule.

Events ​

GET /api/v2/webhooks/events returns this list with descriptions. Every event below also has id, type and created_at.

Token events ​

EventWhendata
token.mintedA token was registeredcrown_id, symbol, chain, owner, tx_hash
token.releasedIts owner gave the registration upcrown_id, symbol, chain, previous_owner
token.expiredA registration lapsed from inactivity and can be claimed againcrown_id, symbol, chain, previous_owner
token.copycat_detectedA token imitating one of yours was foundsightings (each with chain_id, chain_name, address, explorer_url), action_url
profile.updatedA coin page's profile changedcrown_id, symbol, chain, updated_fields
heartbeat.warningThe health score fell below the warning linecrown_id, symbol, chain, score, threshold
heartbeat.criticalThe health score is criticalcrown_id, symbol, chain, score, days_to_inactive
heartbeat.recoveredThe health score is healthy againcrown_id, symbol, chain, score, previous_score
heartbeat.changeThe health status changed, e.g. healthy to warningcrown_id, symbol, chain, previous_status, new_status, current_score, timestamp
holder.newA significant new holder appearedcrown_id, symbol, chain, holder_address, timestamp
trade.largeA trade over 5% of daily volume, or over $10,000crown_id, symbol, chain, trade_volume_usd, direction (buy or sell), timestamp
milestone.reachedHolders passed a milestone: 10, 25, 50, 100, 250, 500, 1K, 2.5K, 5K, 10K, 25K, 50K, 100K, 250K, 500K or 1Mcrown_id, symbol, chain, milestone_type, milestone_value, previous_value, timestamp

Account events ​

EventWhendata
alert.triggeredOne of your alert rules firedalert_id, alert_type, severity, title, body, wallet, symbol, chain, token_address, metric_value, threshold_value, timestamp
alert.acknowledgedAn alert was marked as seeninstance_id, wallet_address, alert_type, crown_symbol, timestamp
alert.resolvedAn alert was closed, by hand or by recoveringinstance_id, wallet_address, alert_type, crown_symbol, reason, timestamp
subscription.upgraded, subscription.downgradedYour plan changedwallet, previous_tier, new_tier
usage.thresholdAPI usage passed 80% or 90% of your daily capthreshold, used, limit, date
quota.threshold_warningAPI usage passed 80% of your daily cap. At most once a daythreshold, used, limit, date
quota.exceededAPI usage passed 90% of your daily cap. At most once a daythreshold, used, limit, date
community.member_joinedA wallet joined your coin's communitycrown_id, wallet, source, timestamp
community.member_leftA wallet left it, or was removedcrown_id, wallet, source, timestamp

An event that is also sent to you as a notification (an alert on your own token, a deal, a copycat) carries title, body, action_url, action_label and symbol in data too.

Deal Events ​

EventWhen
deal.createdAn offer arrived on a registration you hold
deal.acceptedThe holder accepted an offer you made
deal.rejectedThe holder rejected an offer you made
deal.cancelledA deal you are a party to was cancelled before it settled
deal.completedA deal settled: the registration has moved to its new holder
deal.failedA deal was closed out without settling

data carries deal_id, title, body, symbol, chain_id and action_url, and never the deal's terms. Read the deal with GET /api/v2/deals/{dealId} (Deals API).

In-app Store Events ​

Delivered to a store's own webhook (set in the store's settings) and to the webhooks of the wallet that opened the store. A subscription that is both gets each event once.

EventWhen
iap.order.createdA buyer or your server created an order
iap.order.paidAn order's payment is on chain, not yet confirmed
iap.order.confirmedAn order reached its confirmations: items granted, receipt issued
iap.order.expiredAn order's payment window passed unpaid
iap.order.revertedA paid order's payment left the chain (a reorg); the order is pending again
iap.order.refundedThe store's owner recorded paying the buyer back; the items and receipt were revoked
iap.order.mismatchA payment named an order's id but not its quote. The order is unchanged and can still be paid; nothing was granted
iap.order.duplicate_paymentAn order was paid a second time. Nothing more was granted; the second payment reached the seller, who settles it with the buyer
iap.order.checkout_paid_againA second order confirmed for a checkout session that was already paid. Both orders delivered; data.firstOrderId names the first, and only the seller can send the second payment back
iap.order.fulfilment_updatedPhysical items are sold only by Chain Daddy's own store for now. The store's own server reported a physical order's fulfilment: data.status is in_production, shipped, delivered, failed or canceled, with carrier, trackingNumber, trackingUrl and message when given
iap.entitlement.grantedItems were added to a wallet
iap.entitlement.consumedItems were spent
iap.entitlement.revokedItems were taken back
iap.entitlement.expiringA wallet's timed item ends within 72 hours (only for an item held longer than that)
iap.entitlement.expiredA wallet's timed item has ended
iap.drop.claimedA wallet claimed a drop
iap.drop.awardedYour server awarded a wallet from a drop
iap.drop.exhaustedA drop reached its cap: everything in it has been given out
iap.airdrop.award.submittedA token prize your server awarded is on chain, waiting for confirmation
iap.airdrop.award.confirmedA token prize was paid: the winner has the tokens
iap.airdrop.award.failedA token prize was not paid (error says why); its amount is back in the pool's budget
iap.sku.sold_outA confirmed sale took an item to its maxSupply. Fires again only if supply is freed and it sells out again
iap.store.not_liveThe store stopped selling because the wallet that opened it lost the plan (reason: "plan_lapsed")
iap.store.liveThe store is selling again

A subscription receives the event types it listed when it was saved; to receive a new type, save it again with that type included. data carries the store event as the store's event feed returns it; payloads are in the store guide and, for token prizes, Token Prizes. To be sure nothing is missed, read the feed from your last cursor with GET /api/v2/iap/stores/{storeId}/events?after= when a webhook arrives.

Delivered to the webhooks of the wallet that owns a paid app, and to nobody else.

EventWhen
app.order.confirmedAn order made through your app was confirmed: an item, an install licence or a payment. data carries the page, the amount, the fee and your share: the payload