# Webhook payloads

> Every event type Hatched emits, with the shape of the payload you'll receive.

Source: https://docs.hatched.live/docs/reference/webhook-payloads

All webhook requests share the same envelope:

```json
{
  "deliveryId": "wh_01HX…",
  "eventId": "evt_01HX…",
  "type": "badge.awarded",
  "customerId": "cus_01HX…",
  "createdAt": "2026-04-22T10:30:00Z",
  "data": { "...": "see per-type shapes below" }
}
```

Headers on every delivery:

```
Hatched-Signature: t=<unix_seconds>,v1=<hex HMAC-SHA256 over `${t}.${rawBody}`>
Hatched-Delivery-Id: wh_01HX…
Hatched-Event-Type: badge.awarded
```

Verify with `WebhooksResource.verifySignature` from `@hatched/sdk-js` — see
[Handle webhooks](/docs/guides/handle-webhooks).

## Event types

### buddy.hatched

Emitted when an egg's hatch operation completes.

```json
{
  "buddyId": "buddy_01…",
  "userId": "user_42",
  "configVersionId": "cfg_v12",
  "image": { "url": "https://cdn.hatched.live/…", "stage": 1 }
}
```

### buddy.evolved

```json
{
  "buddyId": "buddy_01…",
  "fromStage": 1,
  "toStage": 2,
  "image": { "url": "…", "stage": 2 }
}
```

### coin.earned / coin.spent

```json
{
  "buddyId": "buddy_01…",
  "amount": 10,
  "balanceAfter": 120,
  "source": { "type": "event", "eventType": "lesson_completed" }
}
```

### token.earned / token.spent

```json
{
  "buddyId": "buddy_01…",
  "tokenKey": "gem",
  "amount": 1,
  "balanceAfter": 3,
  "source": { "type": "event", "eventType": "weekly_quiz_passed" }
}
```

### skill.leveled

```json
{
  "buddyId": "buddy_01…",
  "skillKey": "pronunciation",
  "fromLevel": 2,
  "toLevel": 3,
  "value": 65
}
```

### skill.decayed

Fires once per buddy per cadence period when a [decay rule](/docs/concepts/skill-decay)
subtracts skill points. Idempotent — the same period for the same buddy
will only deliver one event even if the sweep is re-run.

```json
{
  "buddy_id": "buddy_01…",
  "user_id": "user_42",
  "skill_key": "vocabulary",
  "previous_level": 80,
  "new_level": 78,
  "delta": -2,
  "rule_id": "dec_01…",
  "period_key": "2026-05-06"
}
```

A `skill.updated` event with the same change is also emitted so
listeners that already handle `skill.updated` from rule-engine paths
don't need a separate decay branch.

### badge.ready / badge.awarded

`badge.ready` fires for manual badges awaiting review; `badge.awarded`
fires when the badge actually attaches.

```json
{
  "buddyId": "buddy_01…",
  "badgeId": "badge_week_warrior",
  "awardedAt": "2026-04-22T10:30:00Z"
}
```

### streak.ticked / streak.milestone / streak.burned

```json
{
  "buddyId": "buddy_01…",
  "streakKey": "daily_lesson",
  "count": 7,
  "milestone": 7
}
```

`streak.milestone` only fires on the configured thresholds. `streak.burned`
fires when the streak resets on a missed day.

### marketplace.purchased / marketplace.equipped

```json
{
  "buddyId": "buddy_01…",
  "itemId": "item_cowboy_hat",
  "price": { "type": "coin", "amount": 50 }
}
```

### evolution.ready

```json
{
  "buddyId": "buddy_01…",
  "nextStage": 2,
  "conditions": { "xp": true, "badgeStreak7": true }
}
```

### leaderboard.snapshot.ready

```json
{
  "leaderboardId": "lb_weekly",
  "snapshotId": "snap_01…",
  "window": { "from": "…", "to": "…" }
}
```

## Delivery semantics

- **At-least-once.** Retries happen on non-2xx. Dedupe on `deliveryId`.
- **No global ordering.** Events for the same buddy _tend_ to arrive in
  order but don't rely on it — compare `createdAt` or carry sequence
  numbers in `data` if ordering matters.
- **Replay window on your end.** Reject requests where `t=` in the
  `Hatched-Signature` header is older than 5 minutes to defend against
  replay attacks. The SDK's `verifySignature` does this automatically.
