Webhooks
Subscribe to events emitted by Uptend and receive them at your endpoint, signed and delivered with retries.
Webhook endpoints are HTTPS URLs you register against a workspace. When something happens in that workspace — an evaluation cycle finishes and lifecycle states, signals and scores are recomputed, an endpoint is auto-disabled, etc. — Uptend POSTs a JSON payload to every endpoint subscribed to that event type, signed per the Standard Webhooks spec.
For the full request/response shape of every endpoint mentioned below, see the API reference: webhook endpoints, webhook deliveries, and events.
Subscribing
Register an endpoint with POST /v1/webhooks/endpoints (or via Settings → Developer → Webhooks in the admin):
curl https://api.uptend.io/v1/webhooks/endpoints \
-H "Authorization: Bearer uptend_sk_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/uptend",
"subscribedEvents": ["lifecycle.evaluated"]
}'The response includes a secret field — the raw signing secret. Save it now. Uptend retains a hash only; you cannot retrieve the raw secret again.
subscribedEvents must match entries in the published event catalog returned by GET /v1/webhooks/endpoints/event-types. Subscribe to specific types (lifecycle.evaluated) or to a prefix (lifecycle.*).
Delivery format
Every request is a POST with Content-Type: application/json and a small set of webhook-* headers from the Standard Webhooks spec:
| Header | Description |
|---|---|
webhook-id | Unique id of the delivery. Use to dedupe — retries reuse the same id. |
webhook-timestamp | Unix epoch seconds when the signature was computed. Reject deliveries skewed more than ±5 minutes from your clock. |
webhook-signature | Space-separated list of v1,<base64-hmac> signatures. Multiple signatures appear during secret rotation. |
Uptend webhook bodies are thin — they carry an identifier reference (subject) for the resource the event is about, never an embedded snapshot of resource fields:
{
"id": "evt_01HJZQK7XJ8R9YGEWABCDEFGHJK",
"type": "lifecycle.evaluated",
"created": "2026-05-22T15:42:11.012Z",
"subject": {
"id": "wsp_01HJZQK7XJ8R9YGEWABCDEFGHJK",
"type": "workspace",
"url": "/v1/workspaces/wsp_01HJZQK7XJ8R9YGEWABCDEFGHJK"
}
}This shape is the same for every webhook event Uptend emits. To act on the event, follow subject.url and call the resource's retrieve endpoint — for lifecycle.evaluated that is GET /v1/workspaces/{id}.
Hydrating events
After verifying a webhook signature, fetch the affected resource's current state via the path in subject.url:
import { createUptendClient, getWorkspace } from "@uptend/sdk";
const client = createUptendClient({ apiKey: process.env.UPTEND_API_KEY! });
async function handle(event: WebhookEvent) {
if (event.type === "lifecycle.evaluated" && event.subject.type === "workspace") {
const { data } = await getWorkspace({ client, path: { workspaceId: event.subject.id } });
// ...refresh whatever you derive from this workspace's lifecycle data
}
}Two things to keep in mind:
- The resource you fetch reflects current state, not state at event-time. If several evaluation cycles complete between the event firing and your handler running, you'll see the latest version. This is usually what you want.
lifecycle.evaluatedis a workspace-level signal. It tells you that the workspace's lifecycle states, signals and scores were recomputed; it does not enumerate which profiles changed. The subject is the workspace because that is the resource the evaluation ran over, so treat the event as a cue to refresh anything you derive from the workspace rather than as a per-profile change feed.
Retrieving events directly
The same envelope you receive on a webhook can also be retrieved through the API. This is useful for re-fetching, debugging, or building event-driven dashboards without setting up a public endpoint:
# Retrieve a single event by id
curl https://api.uptend.io/v1/events/evt_01HJZQK7XJ8R9YGEWABCDEFGHJK \
-H "Authorization: Bearer uptend_sk_…"
# List recent events in the bound workspace, optionally filtered by type or subject
curl "https://api.uptend.io/v1/events?type=lifecycle.evaluated" \
-H "Authorization: Bearer uptend_sk_…"
curl "https://api.uptend.io/v1/events?subjectType=workspace&subjectId=wsp_01HJZQK7XJ8R9YGEWABCDEFGHJK" \
-H "Authorization: Bearer uptend_sk_…"The response is byte-identical to the body that was POSTed to your webhook endpoint for the same event id — same id, type, created, subject. No additional fields are returned. See retrieve an event and list all events for the full parameter set.
Verifying signatures
The signature is HMAC-SHA256(secret, "<webhook-id>.<webhook-timestamp>.<body>"), base64-encoded, prefixed with the scheme version v1,. Verify it before trusting the payload:
import crypto from "node:crypto";
function verify(req: { headers: Headers; rawBody: string }, secret: string) {
const id = req.headers.get("webhook-id");
const timestamp = req.headers.get("webhook-timestamp");
const signatures = req.headers.get("webhook-signature");
if (!id || !timestamp || !signatures) throw new Error("Missing signature headers");
const skewSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (skewSeconds > 300) throw new Error("Timestamp skewed too far");
const keyBytes = Buffer.from(secret.replace(/^whsec_/, ""), "base64url");
const expected = crypto
.createHmac("sha256", keyBytes)
.update(`${id}.${timestamp}.${req.rawBody}`)
.digest("base64");
const ok = signatures
.split(" ")
.map((s) => s.split(",")[1])
.some((sig) => sig && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
if (!ok) throw new Error("Bad signature");
}Verify against the raw request body bytes — re-serializing parsed JSON may change byte ordering and break the signature. Any Standard-Webhooks-compatible library (e.g. Svix's verify()) works as a drop-in alternative.
Retries
If your endpoint returns a non-2xx response (or fails to respond within 10 seconds), Uptend retries on a Stripe-style schedule across 11 total attempts over ~3 days: 5 min, 30 min, 2 hr, 5 hr, 10 hr, then five 12-hour backoffs. The delivery row tracks every attempt; once the schedule is exhausted, the delivery is marked failed.
You can pause an endpoint at any time via PATCH /v1/webhooks/endpoints/{endpointId} with { "active": false } — this stops new deliveries without losing the endpoint or its history.
Auto-disable
To stop firing at a permanently broken receiver, Uptend auto-disables endpoints after 5 consecutive failed deliveries (each delivery already represents ~11 HTTP attempts over ~3 days, so the trigger means the endpoint has been broken for roughly two weeks of cumulative event-days). Disabled endpoints stay in your list and continue to receive new events into their delivery history as disabled rows, but no HTTP requests are made.
When an endpoint auto-disables, Uptend emits a webhook.endpoint.auto_disabled event and notifies every user in the workspace with workspace:developer:read. Re-enable the endpoint from Settings → Developer → Webhooks in the admin once the underlying issue is fixed.
Replaying a delivery
Every attempt is logged. Inspect a single delivery (request and response bodies, headers, timing) via GET /v1/webhooks/deliveries/{deliveryId}. To resend the original payload, call POST /v1/webhooks/deliveries/{deliveryId}/replay — the endpoint must still be active. Replays write a fresh delivery row; the original is left untouched.
Testing your endpoint
Send a synthetic webhook.ping event without waiting for a real one:
curl -X POST https://api.uptend.io/v1/webhooks/endpoints/{endpointId}/send-test-event \
-H "Authorization: Bearer uptend_sk_…"The response includes the deliveryId so you can tail the outcome via list webhook deliveries. The endpoint must be active.
webhook.ping carries the envelope's id, type, and created fields only — there is no subject because pings are synthetic and don't correspond to a real resource event. Real events always carry subject.
Rotating secrets
POST /v1/webhooks/endpoints/{endpointId}/rotate-secret issues a fresh signing secret. The previous secret remains valid for 24 hours so receivers can roll over without missing deliveries — during the overlap window, Uptend signs each delivery with both secrets and the webhook-signature header carries both values.