# Webhooks

What Yutai's webhooks send: the kinds of change they tell a distributor about, what one delivery carries, how often a failed delivery is tried again, and how a delivery is checked. The values on this page are read from Yutai's own rules when the site is built, so they cannot drift from them.

Yutai does not deliver to real endpoints yet. The deliveries it records run on simulated data, so no change reaches a real address; the rules on this page are the ones its stored deliveries already follow.

## Kinds of change

Every delivery tells a distributor about one change, and every change is one kind. A distributor asks for the kinds of change it wants to hear about; the kinds Yutai's registry names:

Tier change `tier_changed`: A holder's tier changed: the credential the holder carries now names another tier.

Benefit unlock `benefit_unlocked`: A benefit opened through a tier: the benefit and the tier that opens it.

Claim confirmation `claim_confirmed`: A claim a holder made is recorded: the claim and the benefit it was made on.

Redemption record `redemption_recorded`: A redemption of a claim is recorded.

## What a delivery carries

One delivery carries three facts: the event id, the kind and the payload. The bounds are the contracts' own:

`eventId`: The change's own id. It names one change and never changes across attempts, so you can tell a repeat from a new change. It carries 1 to 256 characters of letters, digits, dot, underscore, colon and hyphen.

`kind`: The kind of change the delivery tells about: one of the four above.

`payload`: The change's own facts as flat entries. A key carries 1 to 64 characters of letters, digits, underscore, dot and hyphen. A value is a string of up to 512 characters, a finite number, a boolean or null. At most 32 entries.

Each kind names the payload it carries, as flat entries:

Tier change payload

```
{
  "asOf": "2026-10-08T12:00:00.000Z",
  "credentialId": "crd_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E",
  "holderId": "hld_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E",
  "previousTier": "Holder",
  "tier": "Steward"
}
```

Benefit unlock payload

```
{
  "benefitId": "bft_01J9ZQ5B7C8D9E0F1G2H3J4K5M",
  "tier": "Steward"
}
```

Claim confirmation payload

```
{
  "claimId": "clm_01J9ZQ6N8P9Q0R1S2T3V4W5X6Y",
  "benefitId": "bft_01J9ZQ5B7C8D9E0F1G2H3J4K5M"
}
```

Redemption record payload

```
{
  "redemptionId": "red_01J9ZQ7P9R0S1T2V3W4X5Y6Z8A",
  "claimId": "clm_01J9ZQ6N8P9Q0R1S2T3V4W5X6Y"
}
```

## Delivery rules

How a delivery reaches an endpoint:

At least once: A distributor may receive the same change more than once: a retry after an answer that was slow, or a redelivery. Yutai never promises exactly once.

One delivery per change: A redelivered change is the same delivery: Yutai keeps one delivery per change and endpoint, so a repeat answers the same delivery with the same payload. Record the event ids you have processed and answer a repeat without acting on it again.

No order promised: Changes can arrive in any order, and a later change can arrive before an earlier one after a retry. A distributor that needs the current state reads it from Yutai with the calls the [API reference](/docs/api) names rather than replaying deliveries in arrival order.

## Retries

Each attempt answers `delivered` or `failed`: a failed answer carries the reason it failed, a delivered one carries none. A failed attempt is tried again on the schedule below; a delivered one ends the delivery.

Attempt 1: when the change is sent

Attempt 2: 1 minute later

Attempt 3: 5 minutes later

Attempt 4: 30 minutes later

Attempt 5: 2 hours later

A delivery that has spent its 5 attempts stays failed and is not tried again on its own. Once it is, Yutai's team can send it again by hand, at most 3 times, each named beside the operator who pressed it.

## The signature

A receiver checks a delivery's signature in the `Yutai-Signature` header. The value carries two parts joined by a comma: `t` is the whole number of unix seconds the delivery was signed at, and `v1` is the signature itself, 64 lowercase hex characters. For example `Yutai-Signature: t=1781000000,v1=3f9a1c7e5b2d8f04a6c1e9d3b7f5a2c8e4d6b0f2a8c6e1d3f5a7b9c0e2d4f6a8`.

A receiver checks the timestamp first: only a header whose `t` sits within 300 seconds of the receiver's own clock, either way, is fresh, so a captured delivery cannot be replayed later and pass. The signature binds the delivery's own facts, the one message:

The message the signature binds

```
Yutai webhook delivery
Tenant: tnt_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E
Event: tier-update-1
Kind: tier_changed
Endpoint: https://hooks.example.test/yutai
Payload: {"asOf":"2026-10-08T12:00:00.000Z","credentialId":"crd_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E","holderId":"hld_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E","previousTier":"Holder","tier":"Steward"}
```

The real signing key is not issued yet. Until it is, the signature a stored delivery carries is the simulated digest proof over the same message, not the header above.
