zhyper docs

Webhooks

zhyper posts events to an HTTPS endpoint you register. Delivery is retried with backoff; your endpoint should be idempotent, because a retry can arrive after you have already processed the event.

Registering

Create an endpoint in the panel or via POST /v1/webhooks. The response contains the signing secret once. Store it then; afterwards only a prefix is shown.

Which keys can manage endpoints

An endpoint belongs to a team and a mode, and it also keeps the profile scope that authorized its creation. Panel sessions and unrestricted keys create a team-wide endpoint. An endpoint created with a profile-scoped key keeps an immutable snapshot of that key's authorized profiles; events outside that snapshot are rejected before any HTTP request is made.

The /v1/webhooks surface therefore asks a different question: does this key's profile list cover every profile in this team and mode? If it does, the key manages endpoints normally. If it does not, the call is 403 scope_not_allowed. A key scoped to zero profiles is refused too: an empty claim is not a claim of completeness.

Unrestricted keys and panel sessions cover everything by construction and are not affected.

The management check runs on every request, against the profiles that exist at that moment. Two consequences follow:

  • Adding a profile locks a scoped key out. The moment a new profile appears in that team and mode, a profile-scoped key no longer covers everything, and its next call to this surface is a 403 — including calls about endpoints it registered itself.
  • Events from that new profile do not flow to the old endpoint. Delivery is checked both while fan-out is created and immediately before HTTP delivery. The endpoint continues receiving only events from its stored profile scope.

If automation must keep managing endpoints after profiles are added, use an unrestricted key. A profile-scoped endpoint remains safely active for its stored profiles, but that key loses access to the management surface when it no longer covers the full team-and-mode profile set.

Which event types you can subscribe to

This page deliberately does not list them, because a list here would go stale the day one is added. The authoritative set is the events enum in the API reference, and the API will also hand it to you: subscribe to something invalid and the 400 names the offending value, tells you why it was rejected, and returns the full valid set in details.supportedEvents.

Three rejections read differently and the difference matters:

  • unknown_event — no such name.
  • system_reserved — a real name, but reserved for internal use.
  • not_emitted_yet — a real name that nothing produces today. Subscribing would be silent forever, so it is refused rather than accepted.

An endpoint cannot be updated

There is no update operation. /v1/webhooks/{id} serves GET and DELETE, and nothing else on this API modifies a registered endpoint. PUT /v1/webhooks/settings is a separate Zernio-compatibility singleton, not an edit of an endpoint you created here. Changing an endpoint's URL or its event list therefore means delete the old one and create a new one, and that has a consequence worth planning for before you need it:

The new endpoint gets a new signing secret, and you see it once. The secret is returned only in the create response; reading an endpoint back with GET /v1/webhooks/{id} does not include it. So a delete-and-recreate is a secret rotation whether you wanted one or not. Deploy the new secret to your receiver in the same change, and expect a window in which deliveries signed with the new secret arrive at a receiver still holding the old one.

Because a recreate re-sends the event list from scratch, a name that has since become reserved, or that nothing emits yet, is rejected at that moment rather than quietly carried over. Read back the endpoint you are replacing, drop any name the API no longer accepts, and create the replacement with what is left. This is also why it is worth keeping your intended event list in your own configuration: it is the only copy you can edit.

Verifying

Every delivery carries three headers:

HeaderMeaning
x-zhyper-signatureHMAC-SHA256(timestamp + "." + rawBody), lowercase hex
x-zhyper-timestampUnix seconds
x-zhyper-event-idStable id for deduplication

The SDK does the whole check for you:

export interface WebhookOutcome {
  status: number;
  eventType?: string;
}

export function handleWebhook(input: {
  rawBody: string;
  headers: Record<string, string | string[] | undefined>;
  secret: string;
}): WebhookOutcome {
  let event: ZhyperWebhookEvent;
  try {
    event = constructWebhookEvent({
      // DIKKAT: govde GERCEKTEN ham olmalidir. Ayristirilip yeniden
      // serilestirilmis bir govde anahtar sirasini ya da bosluklari
      // degistirebilir ve imza tutmaz.
      rawBody: input.rawBody,
      headers: input.headers,
      secret: input.secret,
    });
  } catch (error) {
    if (error instanceof ZhyperSignatureError) {
      // Dogrulanmamis bir istegi ISLEMEYIN. 400 donun; yeniden denenmesini
      // istemiyoruz cunku imza tekrar denendiginde de tutmayacak.
      return { status: 400 };
    }
    throw error;
  }

  // 2xx donmeden once isi BITIRMEK zorunda degilsiniz — hizli onaylayip
  // kuyruga almak, teslim yeniden denemelerini tetiklememenin en iyi yoludur.
  return { status: 200, eventType: event.type };
}

Three things matter here:

Use the raw body. A body that has been parsed and re-serialised may reorder keys or change whitespace, and the signature will not match. This is the most common reason a correctly-signed request gets rejected. In Express, capture it with express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString(); } }).

The timestamp is inside the signature. Without it, a captured body could be replayed forever. The SDK enforces a five-minute window in both directions — a one-sided check would let a sender with a fast clock keep a signature valid indefinitely.

Verify before parsing. The SDK parses the JSON only after the signature checks out. Parsing first would expose your parser to anyone who can reach the endpoint.

Idempotency on your side

Retries are expected. Deduplicate on x-zhyper-event-id, not on event contents: two genuinely different events can carry identical data.

Acknowledge quickly — return 2xx as soon as you have durably recorded the event, and do the slow work afterwards. Holding the connection open while you work is the usual cause of spurious retries.

Failure handling

Non-2xx responses and timeouts are retried with exponential backoff. After five consecutive delivery failures, the endpoint circuit opens for 15 minutes. While it is open, deliveries are deferred without consuming an attempt; when the cooldown ends, one delivery is allowed through as a probe. A successful probe closes the circuit, while another failure reopens it.

After the seven-attempt budget is exhausted, the delivery is parked and an operational dead-letter signal is emitted for every event type. You can inspect the failed delivery in the panel's delivery log. There is currently no delivery replay API or panel action; recovery requires producing a new source event (or a new test delivery). This limitation is stated explicitly so an integration does not depend on a replay control that does not exist.

Test-mode webhooks are signed exactly like live ones, so you can build and test your receiver end to end before going live. See Test mode.

On this page