← Files YCloud Developer KitARCHIVED FILE

references/webhook-contract.md

7.04 KB · Oct 2, 2026 · 00:32 UTC

↓ Download file

# Webhook receiver contract

This reference is the Developer Kit's single source for receiver signing,
event names, and delivery semantics. Endpoint CRUD paths and schemas remain in
the Webhook Skill's generated `openapi.md`.

## Authority

- Provider contract: [Webhook integration guide](https://docs.ycloud.com/reference/webhook-integration-guide), [Configure webhooks](https://docs.ycloud.com/reference/configure-webhooks), and the pinned OpenAPI `EventType` schema.
- Developer Kit receiver policy: the explicitly labeled defaults below.
- Golden vectors: `webhook-signature-vectors.tsv`.
- Canonical event catalog: `webhook-event-types.txt`.

Legacy examples that append a final period, serialize an already parsed JSON
object, or use underscore event aliases are not contract sources. Generated
code must fail closed instead of preserving those behaviors.

## Signature contract

`YCloud-Signature` has exactly this value shape:

```text
t=<positive Unix timestamp in seconds>,s=<64 lowercase hexadecimal characters>
```

Construct the HMAC message as bytes, in this exact order:

```text
ASCII(decimal timestamp) || 0x2e || exact raw HTTP request body bytes
```

Use the endpoint secret bytes as the HMAC key and HMAC-SHA256 as the digest.
There is one separator byte before the body and no trailing delimiter. Compare
the 32 received/computed digest bytes with a constant-time primitive. Do not
compare ordinary strings.

For `Content-Type: application/json`, YCloud sends JSON encoded as UTF-8. The
receiver must capture the exact bytes before parsing. Whitespace, key order,
Unicode escaping, and a terminal newline are signed data. Parsing and then
serializing the same JSON value produces different bytes and must fail the
signature check.

## Recommended receiver policy

These values are Developer Kit defaults, not YCloud provider guarantees:

- Reject when `abs(nowUnixSeconds - t) > 300`. This checks both stale and
  future timestamps; make the 300-second default configurable.
- Compute comparisons for every explicitly configured candidate secret and
  combine the boolean results without returning on the first secret. This lets
  an application support a bounded current/previous-secret cutover without
  claiming that YCloud provides an old-secret overlap window.
- After verification, atomically claim
  `(webhook_endpoint_id, timestamp, signature)` for at least 300 seconds to
  block an identical transport replay inside the accepted timestamp window.
- Durably deduplicate business processing by
  `(provider, webhook_endpoint_id, event_id)`. The Developer Kit default inbox
  retention is 24 hours and may be increased for the project's recovery
  window. It is not an event-retention guarantee from YCloud.
- Resolve endpoint and tenant identity from trusted receiver configuration,
  never from the event body.

## Event identity and message lifecycle

Verify the signature before parsing or trusting `event.id`, `type`,
`whatsappMessage.id`, or any status. An invalid signature is a rejected transport
attempt: it does not enter the event inbox, has no business duplicate/conflict
classification, and never updates a message projection. A project may retain a
redacted transport-attempt hash for security diagnostics, but that is not an
accepted webhook event.

Keep these identities and outcomes separate:

- `Event.id` is the webhook event identity and the only payload field used in
  the durable inbox key `(provider, webhook_endpoint_id, event_id)`.
- `Event.whatsappMessage.id` is the message identity used to correlate status
  observations. It is never an event deduplication key.
- `inboxClassification` is `new`, `duplicate`, or `conflict`, and exists only
  after successful signature verification and durable claim.
- `projectionOutcome` is a separate consumer result such as `applied`,
  `no_change`, `out_of_order`, `unsupported`, or `failed`.

The exact same scoped event ID and payload hash is a duplicate. The same scoped
event ID with a different hash is a conflict. Different event IDs are distinct
inbox events even when they have the same event type, message ID, or message
status. In particular, `sent`, `delivered`, `read`, and `failed` observations for
one message must not be collapsed into duplicate events. More than one
`delivered` event for the same message can also be a distinct event; an
idempotent projection may report `no_change` without changing the event's `new`
classification.

Message status notifications are not guaranteed to arrive in order, and
`delivered` and `failed` observations can appear in either order. Preserve an
immutable observation history. A project-owned current-state projection may use
the message `updateTime` plus event provenance, but must retain contradictory,
equal-time, missing-time, and older observations for reconciliation instead of
inventing a globally monotonic status rank. Receive time is not event order.

Use `webhook-message-lifecycle-fixtures.json` for executable receiver and
projection tests. Never trust the mock-only
`X-YCloud-Mock-Event-Classification` header as receiver input; compute the
classification locally from the verified body and durable inbox state.

Rotating an endpoint secret is a high-risk management action. Deploy the new
secret immediately after rotation. The public contract does not define the old
secret's validity, a dual-secret overlap, atomic cutover, or rollback. A
receiver may accept an explicitly configured candidate list, but the Skill must
keep the provider overlap itself in `CANNOT`.

## Delivery semantics

- Return any `2xx` promptly after signature verification and durable
  acceptance; the response body is ignored. Move business work to a queue.
- A non-`2xx` response or failure to respond triggers retries after 10 seconds,
  30 seconds, 5 minutes, 30 minutes, 1 hour, 2 hours, and 2 hours. After those
  seven retries, YCloud does not retry that event again.
- Delivery may be repeated. Message status notifications are not guaranteed to
  arrive in order. Do not model transport acceptance as exactly-once or as a
  monotonic business state transition.
- A frequently failing URL can be suspended for 3 minutes. YCloud sends no
  requests to it during suspension and resumes automatically. The public
  contract does not promise backfill for events created during suspension.
- Treat unfamiliar dotted event types as compatible additions: verify,
  durably record an observable unsupported-event state, acknowledge, and avoid
  business side effects.

## Canonical event names

The ordered contents of `webhook-event-types.txt` must exactly match the pinned
OpenAPI `components.schemas.EventType.enum`. Every public event type is dotted.
In particular, use `whatsapp.inbound_message.received` and
`whatsapp.message.updated`; do not emit legacy underscore aliases or singular
update variants.

## Vector format

`webhook-signature-vectors.tsv` is UTF-8 TSV with base64 for every byte string.
`raw_body_base64` and `verification_secrets_base64` are the verifier inputs.
The `signing_*` fields make each header reproducible, including intentionally
bad trailing-delimiter and body-reserialization cases. Synthetic secrets and
payloads only are included.

SHA-256: eaeec8ed51441d163f734821dcf27518c59d3d0a452ff23f6e537e87472974e4