Skip to content
YutaiDocs
GET /v1/statusGet API key

Integrate / Webhooks

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.

View as .md

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.
Bank link connected connect_linked
A person's bank account is linked through Connect: the distributor's own reference for that person.
Bank link disconnected connect_revoked
A person's bank link stops: the distributor's own reference for that person.
Owner and customer status changed owner_customer_changed
Whether a person counts as an owner-customer of a company changed: the distributor's own reference, the company and the new answer.

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 kinds 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": "Tier 1",
  "tier": "Tier 2"
}
Benefit unlock payload
{
  "benefitId": "bft_01J9ZQ5B7C8D9E0F1G2H3J4K5M",
  "tier": "Tier 2"
}
Claim confirmation payload
{
  "claimId": "clm_01J9ZQ6N8P9Q0R1S2T3V4W5X6Y",
  "benefitId": "bft_01J9ZQ5B7C8D9E0F1G2H3J4K5M"
}
Redemption record payload
{
  "redemptionId": "red_01J9ZQ7P9R0S1T2V3W4X5Y6Z8A",
  "claimId": "clm_01J9ZQ6N8P9Q0R1S2T3V4W5X6Y"
}
Bank link connected payload
{
  "external_ref": "customer-4471"
}
Bank link disconnected payload
{
  "external_ref": "customer-4471"
}
Owner and customer status changed payload
{
  "external_ref": "customer-4471",
  "company_id": "cmp_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E",
  "owner_customer": true
}

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 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. Yutai's team can send a spent delivery 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 keyed hash, 64 lowercase hex characters. For example Yutai-Signature: t=1781000000,v1=2fc9d3550390166c913528cae3e8469e437e5e9642d7f94f2a808dd72eda3c7a.

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 header's v1 is a keyed hash over the raw body's exact bytes, not over a message built from the delivery: HMAC-SHA256 under the subscription's signing key over t, a dot and the raw body bytes as received, never a body written again after parsing. The example header above signs this body:

The exact body bytes signed
{
  "eventId": "tier-update-1",
  "kind": "tier_changed",
  "payload": {
    "asOf": "2026-10-08T12:00:00.000Z",
    "credentialId": "crd_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E",
    "holderId": "hld_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E",
    "previousTier": "Tier 1",
    "tier": "Tier 2"
  }
}

Today's stored deliveries carry the simulated digest proof instead, and that proof binds the delivery's own message rather than the header's v1:

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":"Tier 1","tier":"Tier 2"}

The real signing key is not issued yet. Until it is, the signature a stored delivery carries is the simulated digest proof over the message above, not the header's v1, and no header is sent. The header and its v1 are an example computed with a demo key.

Check a delivery

A receiver checks a delivery in this order, before it trusts or reads any of the body:

  1. Read the Yutai-Signature header. Refuse a delivery whose t is missing, repeated or not a whole number of seconds, and whose v1 is missing, repeated or not 64 lowercase hex characters. Refuse a header over 1024 characters, and a part that is not a name and a value. Any other field in the header is ignored, so a later scheme can ride beside v1.
  2. Refuse a t further than 300 seconds from your own clock, either way. That stops a captured delivery being replayed later.
  3. Recompute HMAC-SHA256 under the subscription's signing key over t, a dot and the exact raw body bytes as received, before any JSON parsing, then compare the answer to v1 in constant time. A body written again after parsing can differ by a byte and must never be the bytes you check.
  4. Only then keep one record per event id: a repeat of an event id you have already processed is the same change, so answer it without acting on it again.

Two more rules hold across attempts and across a key change:

  • Each attempt is signed at its own send instant, so a retry carries a fresh t and the tolerance applies to that attempt, not to the first one.
  • After a key change, a receiver may accept either the old or the new signing key for an overlap window it chooses.

The real signing key is not issued yet, so until it is no header is sent, and a receiver cannot verify a real delivery yet.