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

EventSent when
post_publishedA post goes live on a channel.
post_failedA post couldn't be published.
approval_requestedSomeone asks for a post to be reviewed.
approval_decidedA review is approved or sent back.
account_needs_reconnectA channel stops accepting our sign-in.
daily_digestEach 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.