HatchedDocs
Reference

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 no deliveryId in the body.
  • X-Hatched-Timestamp — its own header (unix seconds), not embedded in the signature.
  • X-Hatched-Signaturesha256=<hex>, where the hex is HMAC-SHA256 of `${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-Delivery header.
  • 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-Timestamp header is older than 5 minutes to defend against replay attacks. The SDK's verifySignature does this automatically when you pass the timestamp via options.timestamp (the framework adapters extract it for you).