Webhooks
Zing sends webhooks to your endpoint when events occur (e.g. workout completed, user deleted). Use them to stay in sync without polling. The Pull API remains the source of truth—after you receive a webhook, fetch full resource details from the API when needed.
How to implement a webhook consumer
- Verify the signature — Validate
X-Signatureusing your webhook secret so you only process requests from Zing. - Check the timestamp — Reject requests with
X-Timestampoutside a small window (e.g. a few minutes) to limit replay risk. - Respond quickly with 2xx — Acknowledge receipt immediately after enqueueing work. Do heavy processing asynchronously so the request does not time out.
- Deduplicate by
event_id— Use the envelopeevent_idas an idempotency key. Ignore or skip duplicates. - Do not rely on ordering — Events may arrive out of order or be retried. Use the Pull API to get the current state when order matters.
- Handle retries — Zing retries failed deliveries. Your endpoint should be idempotent so retries are safe.
Security: verifying the signature
Zing sends two headers on each webhook request:
X-Timestamp— UTC timestamp (e.g. Unix seconds or ISO 8601, depending on implementation). Used for replay protection.X-Signature— HMAC-SHA256 signature of the payload.
The signature is computed over the raw body using a secret you get from the Admin UI. Typical construction: HMAC-SHA256(secret, "{timestamp}.{raw_body}") where timestamp is the value of X-Timestamp and raw_body is the request body as received (no parsing or re-serialization). Verify that your computed signature matches X-Signature (e.g. constant-time compare). Reject the request if it does not match or if the timestamp is too old.
Webhook envelope
Each delivery includes a common envelope so you can route and deduplicate without parsing the full payload. The contract defines these in OpenAPI 3.1; see the Reference (opens in new window) for full schemas. Example shape:
{
"event_id": "evt_abc123",
"event_type": "workout.completed",
"occurred_at": "2025-03-12T10:00:00.000Z",
"schema_version": 1,
"partner_user_id": "partner-usr-456"
}event_id— Globally unique; use as idempotency key.event_type— Event name (see event types below).occurred_at— When the event occurred (UTC ISO 8601).schema_version— Payload schema version for forward compatibility.partner_user_id— Your user identifier; may be null for partner-level events.
The full payload includes event-specific fields (e.g. workout_id, test_id, measurement_id) as defined in the Reference.
Event types
| Event type | Description |
|---|---|
workout.completed | A workout was completed. Use Pull API GET /workouts/{workout_id} for full detail. |
workout.updated | A workout was updated. |
workout.deleted | A workout was deleted. |
test.fitness_result | A fitness test result is available. Use GET /tests/{test_id} for full detail. |
test.flexibility_result | A flexibility test result is available. |
body_composition.measurement | A body composition measurement was recorded. Use GET /users/{partner_user_id}/body_composition or /body_composition/latest as needed. |
user.deleted | A user was deleted (e.g. after DELETE /users/{partner_user_id} or internal removal). Clean up local references. |
After receiving an event, fetch the current resource from the Pull API when you need the full snapshot. Do not assume the webhook payload contains every field you need; the API is the source of truth.
Delivery and retries
- Zing sends the webhook with a fire-and-forget style: your endpoint is called asynchronously. The originating action (e.g. workout complete) does not wait for your response.
- If your endpoint returns a non-2xx status or times out, Zing may retry according to an internal schedule. Treat each delivery as potentially duplicated and use
event_idto deduplicate. - You can inspect delivery status and debug missed webhooks via the Pull API: GET /webhooks/deliveries. Filter by
partner_user_id,status, or time range.
Example: minimal verification (pseudo-code)
1. Read X-Timestamp and X-Signature from headers.
2. If timestamp is older than 5 minutes (or your chosen window), return 401.
3. Read raw request body (do not parse JSON yet).
4. Compute: signature = HMAC-SHA256(webhook_secret, X-Timestamp + "." + raw_body).
5. If signature != X-Signature (constant-time compare), return 401.
6. Parse JSON, read event_id; if already processed, return 200 and exit.
7. Enqueue work (e.g. fetch resource from Pull API, update DB).
8. Return 200.Considerations
- HTTPS only — Configure your endpoint with a valid TLS certificate. Zing will not send webhooks to plain HTTP in production.
- Gzip — Requests may be gzip-encoded. Decode before verifying the signature on the raw bytes (after decompression, the body used for HMAC is the decompressed payload if that is what Zing signs; otherwise follow the exact canonicalization Zing documents).
- IP allowlisting — If you restrict by IP, use the ranges Zing provides in their admin or support documentation.
- User deletion — On
user.deleted, remove or anonymize the user in your systems and honor data retention policies. The user may re-register later with a clean profile.
For listing users, workouts, tests, and body composition, and for reconciling after missed webhooks, use the Pull API. For the full webhook payload schemas and all event types, see the Reference (opens in new window).