Platform
Webhooks
Signed, replayable delivery for every event your grant can see.
/v1/webhooksCreate an endpoint
Register an HTTPS URL and the events you want to receive. Vexo answers with the endpoint object and a signing secret, whsec_…, shown exactly once. Endpoints only receive events covered by an active grant, so a wearer who never approved captures:read never appears in your queue.
curl https://api.vexo.dev/v1/webhooks \
-H "Authorization: Bearer vx_app_7hd93k2m4qzt" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.yourapp.com/vexo/events",
"events": ["capture.created", "consent.revoked"],
"description": "Production event sink"
}'Event catalog
Seven event types cover the platform. Subscribe to the ones you handle, not the whole list. Every payload carries an id like evt_9f2ka81mc03d that stays stable across retries, so deduplicate on it.
| Name | Type | Description |
|---|---|---|
| capture.created | event | A new capture was written for a wearer your grant can see. Payload carries the full capture object. |
| capture.distributed | event | A capture reached one of its destinations: an app, a webhook, or an MCP server. Fires once per destination. |
| consent.granted | event | The wearer approved a scope for your app. Payload lists the exact scopes and the grant id. |
| consent.revoked | event | The wearer withdrew a scope. Delivered ahead of queue order. Stop processing that wearer immediately. |
| device.paired | event | A ring finished pairing with your app. Payload includes device id, firmware version, and ring size. |
| device.battery_low | event | Battery dropped below 15 percent. Useful for pausing high-frequency subscriptions gracefully. |
| firmware.updated | event | The ring applied a firmware update. Signal precision tiers may change between versions, re-read them. |
Event names are frozen. New types are added, existing ones never change shape without a version bump.
Verify signatures
Every delivery carries a vexo-signature header: a unix timestamp and an HMAC-SHA256 of {timestamp}.{raw_body} keyed with your endpoint secret. Verify against the raw request body, before any JSON parsing, and compare in constant time.
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300; // five minutes, hard limit
export function verifyVexoSignature(
rawBody: string,
header: string, // the vexo-signature header
secret: string,
): boolean {
// Header format: t=1767952811,v1=5f8a...
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=") as [string, string]),
);
const age = Math.floor(Date.now() / 1000) - Number(parts.t);
if (age > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(parts.v1, "hex"),
);
}Retries
Failed deliveries retry on exponential backoff: 8 attempts over 24 hours, starting at 30 seconds and roughly doubling to a final gap of about 8 hours. A timeout, a connection error, or any non-2xx status counts as a failure. After the eighth attempt the event goes dead and the endpoint keeps receiving newer events, one bad payload never blocks the queue.
Delivery states
| Name | Type | Description |
|---|---|---|
| pending | state | Queued and waiting for its first delivery attempt. Most events leave this state within a second. |
| delivered | state | Your endpoint returned a 2xx within 10 seconds. Terminal state, the event will not be sent again. |
| retrying | state | A previous attempt failed or timed out. The next attempt is scheduled on the backoff curve. |
| dead | state | All 8 attempts failed over 24 hours. Dead events are held for 30 days and replayable from the dashboard. |
Inspect any delivery's state and attempt history at GET /v1/webhooks/{id}/deliveries.
Consent events first
Delivery is ordered per wearer, with one exception, revocation jumps the line. That is deliberate: the wearer's decision to stop data flow outranks your backlog.
