API reference/Webhooks
updated 2026-07-25
API reference

Webhooks

Get notified when things happen in Protocol. Register an endpoint, choose events, verify the signature, and read the delivery log. Owner only.

Advanced  Webhooks let Protocol push event notifications to your own server the moment something happens, instead of you polling the API for changes. Register an HTTPS endpoint, pick the events you care about, and Protocol will POST a signed JSON payload to it every time one of those events occurs.

Owner only. Only a team owner can see this screen and manage webhook endpoints. Invited team coaches don’t have access.

Where to find it

Open your profile menu (your avatar, top-right) → Integrations → the Webhooks tab.

Integrations · Webhooks
Endpoints
+ Create endpoint
https://api.example.com/hooks/protocol Sync new clients to our CRM
Client created Purchase succeeded Enabled 12 Jul
https://hooks.zapier.com/hooks/catch/abc123
All events Disabled 3 Jun
The Webhooks tab lists your endpoints: URL, the events it's subscribed to, its status, and when it was created. Row actions (left to right): edit the URL, description or events, view deliveries, roll the signing secret, enable/disable, and delete.

Create an endpoint

Click Create endpoint, give it your URL, an optional description, and choose which events it should receive.

Create Webhook Endpoint
Endpoint URL
https://api.example.com/hooks/protocol
Must be an https URL that is reachable from the public internet.
Description
e.g. Sync new clients to our CRM
Events
The signing secret used to verify deliveries is shown only once, right after this endpoint is created.
CancelCreate Endpoint
Pick specific events, or check All events to receive everything, including event types added later.

The URL must be https and publicly reachable. Protocol rejects URLs that resolve to private, loopback, or cloud-metadata addresses (for example anything in 10.0.0.0/8, 192.168.0.0/16, 127.0.0.1, or 169.254.169.254), both when you create the endpoint and again before every delivery. This is a safety check, not something you need to configure: it just means your endpoint has to live on the public internet, not on a private network or a local machine.

Your signing secret

When you create an endpoint, Protocol shows you its signing secret exactly once.

Webhook Endpoint Created
⚠ Copy this signing secret now and store it somewhere safe. For security reasons it will not be shown again once you close this dialog.
https://api.example.com/hooks/protocol
whsec_a1b2c3d4e5f6…9f8e7d6c5b4a ⧉ Copy
Copy SecretDone
Copy the secret before closing this dialog. Protocol never shows it again, and it never appears in the endpoint list.

Store it right away, next to wherever your endpoint’s other secrets live. You’ll use it to verify that a delivery really came from Protocol (see below).

Change an endpoint later

Click Edit on an endpoint to change its URL, its description, or which events it receives. This is how you subscribe an existing endpoint to an event type that was added after you set it up. Editing never changes the signing secret, so your existing verification code keeps working, and the delivery history stays intact.

Repointing an endpoint at a new URL also clears its failure count, so an endpoint that was close to being auto-disabled gets a clean slate once you have fixed it.

If the secret leaks, or you just want to rotate it, click Roll secret on the endpoint. Rolling generates a brand new secret and shows it once, the same way. It takes effect immediately: the old secret stops working right away, so update your verification code with the new one before (or right after) you roll it.

Which events you can subscribe to

Clients

Event What it means
client.created A new client was added to your account.
client.updated Something about an existing client changed. changed says what: client for their details, or health_profile, fitness_profile, behavioral_profile, nutrition_profile, genomic_profile when one of their profiles was edited.
client.status.changed A client was paused or reactivated. Carries isActive and previousIsActive, so you can tell which way it moved.
client.lifecycle_stage.changed A client moved to a different lifecycle stage (for example Lead to Active). Carries lifecycleStageId and previousLifecycleStageId; either can be null.

Plans

Event What it means
plan.assigned A training program or a meal plan was assigned to a client. planType is program or nutrition_template.
plan.changed An assigned training program’s status, active flag, dates or sharing changed: made live, paused, expired, rescheduled. changed lists which fields moved, and previousStatus gives you the transition. Editing the plan’s contents does not fire this. Programs only, see the note below.

Money

Event What it means
purchase.succeeded A client completed a purchase, such as a package or a subscription payment.
purchase.updated A purchase changed status: paused, resumed, cancelled, past due, or expired. Carries status and previousStatus. Fires alongside purchase.succeeded on the transition into active.

Progress and reports

Event What it means
progress_entry.created A client logged a new progress entry (weight, measurements, photos, and similar check-ins).
progress_report.created A progress report was drafted. source is automation (an automation wrote it) or manual (you did).
progress_report.updated A report was edited, approved or discarded. Approval is the moment it reaches the client: look for status: "APPROVED" with dispatchedToClient: true.

Forms and check-ins

Event What it means
form.submitted A form was submitted: an onboarding questionnaire, a survey, a weekly check-in, or a public lead form. specialPurpose tells you which (INITIAL_QUESTIONNAIRE, SURVEY, CHECK_IN, OTHER, or null), so you can route without fetching the form back.
checkin.reminder_sent A client was reminded about a check-in. step is due (it just came due) or pre_due (the optional heads-up beforehand).
checkin.missed A check-in was missed and a nudge went out. missCount is which one (1st, 2nd, …) and maxMisses is your configured limit.
checkin.fully_missed The check-in ladder ran out. This is the one that means “this client has gone quiet.”

Messaging

Event What it means
message.received A client sent you a message. Coach-sent messages don’t fire this, so an integration that replies won’t trigger itself.

Everything

Event What it means
* Wildcard. Subscribe to every event type, including ones added after you set up the endpoint.

Some events carry identifiers rather than full content. message.received tells you a client wrote to you, in which conversation and when, but never the message text: chat threads routinely carry health information, and we don’t widen where that travels. The checkin.* events work the same way, telling you a check-in was missed without carrying what was answered. Read the message or the submission back through the API with your key when you need the content.

One client.updated per surface means you can receive several in quick succession: onboarding can fill a client’s health, fitness, nutrition and behavioural profiles in a single submission, and that is four events, each naming a different changed. If you only care that something about the client moved, treat them as one and refetch the client once.

Two more deliberate quiet spots. Biometric readings are not on this list: wearable sync writes thousands of points a day per client, and a webhook feed is the wrong shape for that; pull them from the API instead. And plan.changed covers training programs only, because meal plans have no status or date window to change: once a meal plan is assigned to a client they can see it, so plan.assigned is the whole story for those.

What a delivery looks like

Every delivery is an HTTP POST with a JSON body wrapping the event in an envelope:

{
  "id": "evt_1a2b3c4d5e6f",
  "type": "client.created",
  "api_version": "2026-07-20",
  "created": 1752998400,
  "data": {
    "object": {
      "id": "cli_9f8e7d6c",
      "name": "Jamie Rivera",
      "email": "jamie@example.com"
    }
  }
}
  • id identifies this event.
  • type is one of the event strings above.
  • api_version identifies the shape of the payload. It’s fixed per event and won’t change under you; if Protocol ever changes the envelope shape, it ships under a new version.
  • created is a Unix timestamp (seconds) of when the event happened.
  • data.object is the affected resource.

Each request also carries four headers:

Header Contains
Webhook-Signature t=<unix-timestamp>,v1=<hex-hmac>, used to verify the delivery. See below.
Webhook-Event-Id The ID of the event, identical to the envelope’s id. This is the one to deduplicate on.
Webhook-Id The ID of this particular delivery attempt chain. Useful for matching a request against a row in the delivery log when you contact support.
Webhook-Event-Type The event type string, e.g. client.created, same as the envelope’s type.

Verify the signature

Anyone can POST to your endpoint claiming to be Protocol, so verify the Webhook-Signature header before trusting a delivery.

The header looks like t=1752998400,v1=5257a869e.... t is the Unix timestamp (seconds) the request was sent, and v1 is an HMAC-SHA256 hex digest computed over the string "{t}.{raw body}" using your endpoint’s signing secret.

You must use the raw, unparsed request body. Most frameworks parse JSON automatically before your handler runs, which changes whitespace and key order and breaks the signature. Read the body as a raw string (or raw bytes) for this route specifically, compute the signature from that, and only then parse it as JSON.

Recompute the digest yourself, compare it to v1 with a constant-time comparison, and check that t isn’t too old (to reject replayed requests):

import { createHmac, timingSafeEqual } from 'node:crypto';

function isValidWebhookSignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(signatureHeader.split(',').map((p) => p.split('=')));
  const timestamp = Number(parts.t);
  if (!timestamp || !parts.v1) return false;

  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const received = Buffer.from(parts.v1, 'hex');
  const computed = Buffer.from(expected, 'hex');
  if (received.length !== computed.length) return false;
  if (!timingSafeEqual(received, computed)) return false;

  const age = Math.abs(Date.now() / 1000 - timestamp);
  return age <= toleranceSeconds;
}

A plain === string comparison leaks timing information about how many characters matched, which is why timingSafeEqual matters here, not just correctness.

Retries and failures

Protocol treats any 2xx response as success. Anything else, a non-2xx status, a timeout, or a connection error, is treated as a failure and retried with a growing gap between attempts: the first retry comes about 5 seconds later, then 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, and 16 hours, for 8 attempts total spread over roughly 24 hours. Once an attempt succeeds, delivery stops.

Two things are worth knowing about the edges. Protocol does not follow redirects: a 301 or 302 counts as a failure, so register the final URL rather than one that redirects to it. And a URL that resolves to a private, loopback, or cloud-metadata address fails immediately with no retries, because no amount of waiting makes that address acceptable.

If an endpoint keeps failing across many deliveries, Protocol automatically disables it so a dead or misconfigured endpoint doesn’t keep accumulating failed attempts. A disabled endpoint stops receiving new deliveries. Fix whatever was wrong and click Enable on the endpoint (see Where to find it) to resume.

Handle duplicates

Deliveries are at-least-once, not exactly-once. A retry after a timeout, a network hiccup, or Protocol’s own crash-recovery can occasionally result in the same event reaching your endpoint more than once, even after it succeeded the first time.

Use the Webhook-Event-Id header (the same value as the envelope’s id) as an idempotency key: before acting on a delivery, check whether you’ve already processed that ID, and if so, return a 2xx and skip it. This is the single most common integration bug, and the fix is a lookup in whatever store you already use (a database table, a cache, a set), keyed on Webhook-Event-Id.

Key on the event ID, not Webhook-Id. Webhook-Id identifies one delivery, and pressing Resend in the delivery log creates a new delivery for the same event, so it arrives with the same Webhook-Event-Id but a different Webhook-Id. An integration keyed on the wrong one would process a resent event twice.

Don’t rely on the order they arrive in

Protocol sends queued deliveries in parallel, and a delivery that failed comes back later on its retry schedule. Two events raised a moment apart can therefore reach you in either order, and an event from an hour ago can land after one from a minute ago.

If order matters to you, sort on the envelope’s created timestamp rather than on arrival, and treat an event older than the state you already hold as one to ignore. A coach who pauses a plan and then reactivates it produces two plan.changed events; deciding from arrival order can leave you with the plan marked paused when it is live.

Delivery log

Click the deliveries action on an endpoint to see its recent deliveries, filter by status, and inspect what was tried. Delivery history is kept for 30 days; a delivery still waiting to be retried is never removed, however old it is.

Deliveries · https://api.example.com/hooks/protocol
All Pending Succeeded Failed
Succeeded 1 attempt 200 20 Jul, 09:14 ↻ Resend
Failed 8 attempts Connection refused 19 Jul, 22:02 ↻ Resend
Attempt history
1500812 msConnection refused19 Jul, 21:02
2500790 msConnection refused19 Jul, 21:32
Filter deliveries by status, expand one to see its full attempt history (status, duration, and error per try), and use Resend to manually retry a delivery.
  • Creating & using API keys: create a key, authenticate a request, revoke a key. Webhooks and API keys are separate: a webhook pushes events to you, a key lets you pull data from the API.
  • API rate limits & AI usage budget: the per-key request limits and the 429 / 402 contract for calls you make into the API.
  • API reference: the full, always-current endpoint list for managing webhook endpoints and reading delivery logs.