Webhook payloads
Common event payloads Hatched emits, with the shape of the body you'll receive.
There is no wrapping envelope. The POST body is the raw event payload —
the per-type object shown below, with snake_case keys. Event metadata
travels only in HTTP headers, never in the body.
Headers on every delivery:
Content-Type: application/json
X-Hatched-Event: badge.awarded
X-Hatched-Delivery: <delivery uuid>
X-Hatched-Timestamp: <unix_seconds>
X-Hatched-Signature: sha256=<hex HMAC-SHA256 over `${timestamp}.${rawBody}`>X-Hatched-Event— the event name (e.g.badge.awarded).X-Hatched-Delivery— the delivery id. This is the dedupe key — there is nodeliveryIdin the body.X-Hatched-Timestamp— its own header (unix seconds), not embedded in the signature.X-Hatched-Signature—sha256=<hex>, where the hex isHMAC-SHA256of`${timestamp}.${rawBody}`.
Verify with WebhooksResource.verifySignature from @hatched/sdk-js — see
Handle webhooks.
Event types
The shapes below cover the most commonly subscribed events. They are not the
full catalog — Hatched emits many more (mystery box, lottery, league, kudos,
group quest, booster, and others). The canonical, always-current list of every
event name is served by GET /webhook-configs/events; treat that endpoint as
the source of truth for what you can subscribe to.
buddy.hatched
Emitted when an egg's hatch operation completes.
{
"buddy_id": "buddy_01…",
"egg_id": "egg_01…",
"user_id": "user_42",
"name": "Pip",
"image_url": "https://cdn.hatched.live/…",
"thumb_url": "https://cdn.hatched.live/…",
"evolution_stage": 1
}buddy.evolved
{
"buddy_id": "buddy_01…",
"previous_stage": 1,
"new_stage": 2,
"image_url": "https://cdn.hatched.live/…"
}coins.earned / coins.spent
{
"buddy_id": "buddy_01…",
"amount": 10,
"balance": 120,
"reason": "lesson_completed",
"reference_id": "ref_01…",
"ledger_id": "ledger_01…",
"is_lucky": false
}token.earned / token.spent
{
"buddy_id": "buddy_01…",
"token_type": "gem",
"amount": 1,
"reason": "rule:weekly_quiz_passed"
}gate.unlocked
Fires when a buddy spends tokens to unlock a token gate.
{
"buddy_id": "buddy_01…",
"gate_key": "premium_lessons",
"token_key": "gem",
"cost": 5,
"unlocked_at": "2026-06-01T12:00:00.000Z"
}skill.level_up
{
"buddy_id": "buddy_01…",
"skill_key": "pronunciation",
"level": 3,
"previous": 2,
"change": 1
}skill.updated
Fires whenever a buddy's skill value changes — including rule-engine reward
paths. Use this when you want every skill movement rather than only the
threshold crossings that skill.level_up reports.
{
"buddy_id": "buddy_01…",
"updates": {
"vocabulary": { "level": 78, "previous": 80, "change": -2 }
}
}skill.decayed
Fires when a skill-decay rule lowers (or refreshes) a buddy's skill level for
a decay period. A drop also emits a paired skill.updated.
{
"buddy_id": "buddy_01…",
"user_id": "user_123",
"skill_key": "vocabulary",
"previous_level": 80,
"new_level": 78,
"delta": -2,
"rule_id": "sdr_01…",
"period_key": "2026-W22"
}badge.ready / badge.awarded
badge.ready fires for manual badges awaiting review; badge.awarded
fires when the badge actually attaches. badge.ready carries only
{ buddy_id, badge_key }; badge.awarded carries the full shape below.
{
"buddy_id": "buddy_01…",
"badge_key": "week_warrior",
"label": "Week Warrior",
"awarded_at": "2026-04-22T10:30:00Z",
"coin_reward": 50,
"reason": null
}streak.milestone
{
"buddy_id": "buddy_01…",
"current_streak": 7,
"longest_streak": 12,
"milestone": 7,
"event_id": "evt_01…"
}streak.milestone only fires on the configured thresholds. To react when a
streak is about to lapse, subscribe to streak.at_risk.
item.purchased / item.equipped
{
"buddy_id": "buddy_01…",
"item_id": "item_cowboy_hat",
"item_name": "Cowboy Hat",
"price_paid": 50,
"purchase_id": "purchase_01…"
}evolution.ready
{
"buddy_id": "buddy_01…",
"current_stage": 1,
"next_stage": 2,
"progress": 1,
"auto_evolve": false
}buddy.prestiged
Fires when a buddy crosses the Prestige threshold and resets its evolution stage with the prestige counter incremented. The buddy keeps its history; the public share page picks up a prestige aura automatically.
{
"buddy_id": "buddy_01…",
"prestige_level": 1,
"from_evolution_stage": 5,
"season_id": "season_2026q2",
"occurred_at": "2026-05-25T10:30:00Z"
}team_membership.role_upgraded
Fires when a buddy is granted a higher role within a Team — currently the
only upgrade path is to mentor (after meeting the configured mentor-hour
threshold). Use this to surface a "you're now a mentor" notification in your
product.
{
"buddy_id": "buddy_01…",
"from_role": "member",
"to_role": "mentor",
"occurred_at": "2026-05-25T10:30:00Z"
}cause.threshold_reached
Symbolic Humanity Hero counter crossed a configured unit threshold
(e.g. every 100 trees planted, every 1,000 meals served). Unlike other
webhook types, this event delivers to the cause's own webhook_url —
configured per-cause in the dashboard — rather than the customer-wide
endpoints. Tenants typically wire this to a charity API or internal
reporting service.
{
"event": "cause.threshold_reached",
"cause_id": "cause_01…",
"cause_key": "trees_planted",
"customer_id": "cus_01…",
"total_units": 1300,
"threshold_unit": 1300,
"is_test": false,
"occurred_at": "2026-05-25T10:30:00Z"
}Unlike the other events, the cause payload embeds the event name and
timestamp in the body (event + occurred_at) rather than relying solely
on the headers — this is a deliberate distinct shape, kept for compatibility
with charity/reporting consumers. The delivery is still HMAC-signed the same
way as every other webhook (same headers, same signature). is_test is true
when the dashboard's "Send test delivery" button triggers the call.
council.proposal_approved
Fires when a Council proposal (UGC narrative line for a level-up slot) is approved by the configured moderator quorum. Use this to ship the new copy to your product — for example, refreshing localized strings in your CMS or re-rendering cached share pages.
{
"customer_id": "cus_01…",
"proposal_id": "council_prop_01…",
"target_slot": "level_up_copy.stage_3",
"occurred_at": "2026-05-25T10:30:00Z"
}target_slot is dot-notation into the customer's narrative JSONB —
i.e. level_up_copy.stage_3 points at the level-3 evolution flavor text.
This lets a downstream system know exactly which surface to invalidate.
Delivery semantics
- At-least-once. Retries happen on non-2xx. Dedupe on the
X-Hatched-Deliveryheader. - No global ordering. Events for the same buddy tend to arrive in order but don't rely on it — carry your own sequence numbers in the payload if ordering matters.
- Replay window on your end. Reject requests where the
X-Hatched-Timestampheader is older than 5 minutes to defend against replay attacks. The SDK'sverifySignaturedoes this automatically when you pass the timestamp viaoptions.timestamp(the framework adapters extract it for you).