Search docs

Search the Vexo developer documentation

Get the ring

Platform

Webhooks

Signed, replayable delivery for every event your grant can see.

POST/v1/webhooks

Create 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.

Register an endpoint
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.

NameTypeDescription
capture.createdeventA new capture was written for a wearer your grant can see. Payload carries the full capture object.
capture.distributedeventA capture reached one of its destinations: an app, a webhook, or an MCP server. Fires once per destination.
consent.grantedeventThe wearer approved a scope for your app. Payload lists the exact scopes and the grant id.
consent.revokedeventThe wearer withdrew a scope. Delivered ahead of queue order. Stop processing that wearer immediately.
device.pairedeventA ring finished pairing with your app. Payload includes device id, firmware version, and ring size.
device.battery_loweventBattery dropped below 15 percent. Useful for pausing high-frequency subscriptions gracefully.
firmware.updatedeventThe 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.

HMAC verification
verify.ts
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

NameTypeDescription
pendingstateQueued and waiting for its first delivery attempt. Most events leave this state within a second.
deliveredstateYour endpoint returned a 2xx within 10 seconds. Terminal state, the event will not be sent again.
retryingstateA previous attempt failed or timed out. The next attempt is scheduled on the backoff curve.
deadstateAll 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.

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.