Webhooks
Get a signed JSON request at your own endpoint when posts publish, fail or need review.
Webhooks are part of the Pro and Advanced plans. Owners and admins manage them.
Add an endpoint
Under Settings → Webhooks, choose Add endpoint:
- Payload URL: an HTTPS endpoint you control, such as
https://example.com/webhooks/orbit. - Events: which events to send.
- Only for posts tagged (optional): limit post events to one tag.
The signing secret is shown once, when the endpoint is created. Copy it then; Regenerate replaces it. Pause an endpoint by turning off Enabled.
Endpoints can also be managed over the API (/api/v1/webhook-endpoints), which is how an integration registers its own
receiver.
Events
| Event | Sent when |
|---|---|
post_published | A post goes live on a channel. |
post_failed | A post couldn't be published. |
approval_requested | Someone asks for a post to be reviewed. |
approval_decided | A review is approved or sent back. |
account_needs_reconnect | A channel stops accepting our sign-in. |
daily_digest | Each morning, with the day's scheduled posts. |
The request
A POST with a JSON body:
{
"event": "post_published",
"organizationId": "org_123",
"title": "Posted to LinkedIn",
"body": "We shipped the new docs…",
"url": "https://orbitwrite.com/brightloop/schedule?post=grp_123",
"composerGroupIds": ["grp_123"],
"post": {
"platform": "linkedin_page",
"platformLabel": "LinkedIn Page",
"handle": "Brightloop",
"platformUrl": "https://www.linkedin.com/feed/update/urn:li:share:…",
"appUrl": "https://orbitwrite.com/brightloop/…",
"excerpt": "We shipped the new docs…"
},
"timestamp": 1792400000000
}
post is included on post_published only. timestamp is milliseconds since the epoch.
The headers:
Content-Type: application/json
User-Agent: Orbit-Webhook/1
X-Orbit-Timestamp: 1792400000000
X-Orbit-Signature: sha256=5d41402abc4b2a76b9719d911017c592…
Verify the signature
The signature is an HMAC-SHA256, keyed with your signing secret, over the timestamp, a full stop, and the raw body. Check it before trusting the request, and reject old timestamps to stop replays.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyOrbitwrite(rawBody: string, headers: Headers, secret: string): boolean {
const timestamp = headers.get("x-orbit-timestamp") ?? "";
const signature = headers.get("x-orbit-signature") ?? "";
if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60_000) return false;
const expected = "sha256=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}
Use the body exactly as received: parsing and re-serialising the JSON changes the bytes and breaks the signature.
Delivery
- Each event is sent once, with a 10-second timeout. Respond with any 2xx quickly and do the work afterwards.
- Failed deliveries aren't retried, so treat webhooks as a prompt and read the current state from the API when it matters.