Webhooks for Post Events
Subscribe to events to get real-time notifications when posts are published, scheduled, or fail. Webhooks are the right choice when you want to know about state changes without polling the API.
Creating a webhook
Code
Response:
Code
The id is a UUID and the secret is a 64-character hex string returned exactly once on creation. Store the secret. You will need it to verify incoming webhook signatures.
Available events
| Event | When it fires |
|---|---|
post.scheduled | A post moves into the scheduled state |
post.published | A post publishes successfully to at least one platform |
post.failed | A post fails to publish on every selected platform |
These are the only three valid events. Creating or updating a webhook with any other value (including a "*" wildcard) returns 400. There is no Social Inbox event (a new DM, comment, or mention) yet — poll GET /inbox/conversations instead.
Partial publishes fire post.published, not a separate event. When a post succeeds on some platforms and fails on others, the payload's data.status is "warning" and the per-platform outcomes are in data.targets (each with its own status). Inspect targets to see exactly which platforms succeeded or failed.
Payload shape
Code
data.targets holds one entry per selected platform. native_post_id is the platform's own post ID (present when that platform succeeded); error appears only on a non-successful target. data.status is the overall post status (posted, warning for a partial success, or failed). Always branch on the top-level type before accessing data.
Verifying signatures
Every outbound webhook call is signed with HMAC-SHA256 using your webhook's secret. Each delivery carries these headers:
| Header | Value |
|---|---|
X-OmniSocials-Signature | t=<unix_timestamp>,v1=<hex_signature> |
X-OmniSocials-Timestamp | The same unix timestamp, on its own |
X-OmniSocials-Event | The event type (e.g. post.published) |
X-OmniSocials-Webhook-Id | The webhook's ID |
X-OmniSocials-Delivery | A unique ID per event, stable across retry attempts. Use it to deduplicate. |
The signature is computed over `${timestamp}.${rawBody}` (the timestamp, a literal dot, then the raw JSON body), the same scheme Stripe uses. To verify: parse t and v1 out of the header, recompute the HMAC over timestamp.rawBody, and compare against v1. A mismatch means the request is not from OmniSocials. Reject it.
Code
Verify against the raw request body, before any JSON parsing or reserialization. Always compare with a constant-time function like timingSafeEqual. Optionally, reject deliveries whose t timestamp is more than a few minutes old to guard against replay. Keep that window at 5 minutes or more: the timestamp is set when the event is created and reused on retries, so a retried delivery arrives with the original timestamp.
Delivery and retries
- Respond with any
2xxwithin 10 seconds. That counts as delivered. Anything else (including a timeout after 10 seconds) counts as a failure. Do your processing after you respond, not before. - Failed deliveries retry twice: the first retry after 2 seconds, the second after 5 more. Only
5xx,429, and network errors are retried; any other4xxfails immediately without a retry. - Delivery is at-least-once. If your endpoint processed the event but the response timed out, the retry delivers a duplicate. Deduplicate on
X-OmniSocials-Delivery; it stays the same across retries of one event. - Ordering is not guaranteed. Deliveries fire asynchronously and concurrently; a retrying delivery can arrive after a newer event. Order on the payload's
created_at. - After the final failed attempt the event is dropped. It is not re-sent later, and there is no replay endpoint. The webhook records the failure:
failure_countincrements andlast_failure(time, status, error) is stored; read both viaGET /webhooks/:id. A webhook is never disabled automatically, so a recovered endpoint receives the next event normally. Reconcile missed events by pollingGET /posts?status=. - Requests come with
User-Agent: OmniSocials-Webhooks/1.0and follow up to 3 redirects.
Local testing. Webhook URLs must be publicly reachable. For local development, expose your dev server with a tunnel (for example cloudflared tunnel or ngrok http 3000) and register the tunnel URL as the webhook.
Managing webhooks
All webhook endpoints require the webhooks:manage scope on your API key.
| Endpoint | Description |
|---|---|
GET /webhooks | List all webhooks for this key |
GET /webhooks/:id | Get a single webhook |
PATCH /webhooks/:id | Update URL, events, or is_active (re-enabling resets failure_count) |
DELETE /webhooks/:id | Delete a webhook |
POST /webhooks/:id/rotate-secret | Rotate the signing secret |
Rotating a secret immediately invalidates the old one. Update your verification code to use the new secret before rotating, or accept a short window of failed verifications.