Webhooks
Instead of polling GET /api/posts, register an endpoint and Pubrivo sends it an event each time
a Variant publishes or fails.
Register an endpoint
Section titled “Register an endpoint”Managing webhooks is a permission of its own, webhooks:manage:
- An API key has it if Also allow managing webhooks was ticked when it was created.
- An Authorized app has it if you allowed managing webhooks when you approved it.
curl https://app.pubrivo.com/api/webhooks \ -H "Authorization: Bearer $PUBRIVO_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/pubrivo", "events": ["variant.published", "variant.failed"] }'The answer is a 201 carrying secret, which starts whsec_. It is shown once. Keep it with
your receiver; you need it to check each event came from Pubrivo.
- The URL must be
httpson the standard port, reach the public internet, and not redirect. An address inside a private network, on another port, or one that answers with a redirect is refused with422and codeunsafe_url. - A Workspace can have up to 10 endpoints.
GET /api/webhookslists them, each with its latest deliveries.DELETE /api/webhooks/{id}stops sending to one. Settings › API keys shows the same list, and why a delivery failed.- An endpoint stops when the Member whose key or app registered it leaves the Workspace.
Events
Section titled “Events”| Event | When |
|---|---|
variant.published |
A Variant is live on its Network. permalink links to it. |
variant.failed |
A Variant failed. failureReason says what you can do; failureDetail is the Network’s own words. |
Each event is a POST with a JSON body:
{ "id": "0b7e6a4c-3f1d-4b8e-9c2a-5d6e7f8a9b0c", "type": "variant.published", "createdAt": "2026-10-14T07:00:04.512Z", "data": { "variant": { "id": "…", "status": "published", "permalink": "https://www.threads.net/…" }, "post": { "id": "…", "text": "Fresh out of the oven.", "variants": ["…"] } }}data.post is the Post exactly as GET /api/posts/{id} reads it at that moment, and
data.variant is the one of its Variants the event is about. The full shape is in the
reference.
Verify the signature
Section titled “Verify the signature”Every event carries a Pubrivo-Signature header:
Pubrivo-Signature: t=1791961204,v1=5f2b…c9v1 is the hex HMAC-SHA256 of <t>.<raw body>, keyed with your secret. Compute it over the body
as it arrived, before parsing it, compare in constant time, and refuse a t more than five
minutes old — the timestamp is inside what is signed, so an old event replayed is refused too.
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verify(rawBody, header, secret, toleranceSeconds = 300) { const parts = Object.fromEntries(header.split(',').map((part) => part.split('='))) const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex') const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= toleranceSeconds return ( fresh && typeof parts.v1 === 'string' && parts.v1.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) )}import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool: parts = dict(part.split("=", 1) for part in header.split(",")) signed = parts["t"].encode() + b"." + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() fresh = abs(time.time() - int(parts["t"])) <= tolerance return fresh and hmac.compare_digest(parts.get("v1", ""), expected)Delivery
Section titled “Delivery”- Answer
2xxwithin 10 seconds. Anything else — another status, a timeout, no answer — is tried again: after 30 seconds, then doubling, never more than an hour apart, for 24 hours. Then the delivery is marked failed and is not tried again. - Redirects are not followed. Register the final address.
- At least once. A delivery can arrive twice — say, when your answer was lost on the way back.
id(also in thePubrivo-Event-Idheader) is the same each time: dedupe on it. - Order is not guaranteed. Read
createdAt, or fetch the Post, when order matters. - Deliveries are kept for 30 days after they succeed or give up, then deleted.