Skip to main content
Publishing is synchronous, so POST /v1/posts already tells you what happened — webhooks exist for the outcomes nobody is holding a request open for: a scheduled post that failed at 09:00 while everyone was asleep, and a profile whose token expired between one post and the next. That is why the event list is short and all of it is about outcomes. A feed of every state change would be more events and no more information.

Events

post.published and post.partially_published are separate events rather than one event with a status field, so an endpoint can subscribe to just the bad news:
The three profile events are one account’s life story — connected when it becomes publishable, needs_attention when its credentials stop working, disconnected when it is taken away — so an integration that mirrors accounts into its own UI can keep that list correct without polling GET /v1/profiles.
All three fire on the transition. profile.needs_attention is sent once when an account breaks, not once per post until you fix it; profile.connected is sent when an account starts being publishable, and a reconnection of an account that is already live is a credential refresh rather than a new connection, so it is silent.

Set one up

1

Register the endpoint

POST /v1/webhooks with an https URL. Omit events to receive everything.The response contains the signing secret. It is in that response and no other — store it before you move on. If you lose it, rotate rather than asking for it back: an endpoint that hands out signing secrets would turn a leaked API key into forged events.
2

Verify signatures

Every request carries Nylon-Signature. Check it before you trust the body — see below.
3

Test it

POST /v1/webhooks/{webhookId}/test delivers a signed webhook.test event and waits for your endpoint to answer, returning the status code and body it gave. It is the only delivery that is not queued, because wiring up a receiver should be one round trip.

The request

The object under data.post is the same shape GET /v1/posts/{postId} returns, and data.profile is the same shape as a profile. There is nothing to learn twice.
id is the delivery id, and it is also the Nylon-Delivery header. A retry reuses it, so deduplicate on it and you can treat delivery as at-least-once without processing anything twice.

Verifying the signature

Nylon-Signature: t=<unix timestamp>,v1=<hex>, where the hex is HMAC-SHA256(secret, "<t>.<raw body>"). Three things matter: use the raw body — not a re-serialised object, because key order and whitespace change the hash — compare in constant time, and reject an old timestamp. Without the timestamp check, a captured delivery can be replayed at any point in the future.
Frameworks that parse JSON for you usually discard the raw body. In Express, mount express.raw({ type: 'application/json' }) on the webhook route; in Next.js App Router, read await request.text() before parsing.

Delivery, retries and failure

Its own durable run

The delivery is recorded inside the same operation that records the outcome and then handed to a workflow run of its own. Your endpoint being slow never slows down anyone’s publish, and a delivery is never lost to a process that ended.

Answer with a 2xx

Any 2xx settles the delivery. Anything else — a 500, a timeout, a DNS failure — is a retry. Ten seconds is the timeout, so acknowledge first and do the work afterwards.

Seven attempts over a day

Then 1m, 5m, 30m, 2h, 6h, 24h — the run sleeps between them, so a retry lands on the schedule rather than on the next sweep of a queue. Coarse on purpose: an endpoint is usually either fine or down for a deploy, and retries a second apart are a denial of service against the thing you are trying to reach.

Disabled after 20 in a row

Twenty consecutive failed deliveries disables the endpoint, with the reason on it. Any success resets the count. Fix the endpoint and PATCH status back to active.
next_attempt_at on a pending delivery is when that run will wake. GET /v1/webhooks/{webhookId}/deliveries is the answer to “did you send it?”. Every row carries the payload, the response status and the first 2000 characters of the response body, which is what lets you tell a signature your receiver rejected from an event that was never queued.

Rotating the secret

POST /v1/webhooks/{webhookId}/secret issues a new secret and returns it once. There is no overlap window — the next delivery is signed with the new secret — so deploy it to your receiver first, then rotate. That is a deliberate simplification. Two valid secrets means a receiver that tries both and a window in which a leaked secret still works, which is more machinery than a webhook endpoint deserves.

Not agent tools

The MCP server exposes publishing, profiles and networks, and deliberately not these endpoints. Webhook configuration is infrastructure — where your servers listen and what signs the traffic — and an agent reconfiguring it is not a workflow anyone wants. Register endpoints from your own code or your deploy pipeline.