Webhooks
Webhooks let OmniPost push post-completion events to your backend instead of you polling GET /v1/posts/:id. Register an endpoint once with POST /v1/webhooks and OmniPost delivers a signed JSON payload every time a target finishes.
Event types
| Event | Fires when |
|---|---|
| post.completed | A target successfully publishes to its platform (status: published). |
| post.failed | A target fails permanently (status: failed), including sandbox simulate_error targets. |
Events are per-target, not per-post. Publishing to three platforms in one POST /v1/upload call delivers up to three separate webhook events, one per target, as each resolves independently.
Registering an endpoint
curl -X POST https://api.omnipost.dev/v1/webhooks \
-H "Authorization: Bearer $OMNIPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/omnipost",
"events": ["post.completed", "post.failed"]
}'{
"id": "wh_3a7c9f1b2e8d4056",
"url": "https://yourapp.com/webhooks/omnipost",
"events": ["post.completed", "post.failed"],
"secret": "whsec_5f1a9c2b7e4d4083ae1b9c7f2d4e6a80",
"created_at": "2026-08-02T14:00:00Z"
}The secret is shown once
Store secretimmediately — it's used to verify incoming deliveries and cannot be retrieved again. If you lose it, delete and recreate the webhook.
Payload shape
Every delivery is a POST with a JSON body shaped like this:
{
"event": "post.completed",
"post_id": "post_8f3d1a9c2b7e4f01",
"target": {
"platform": "instagram",
"status": "published",
"external_url": "https://www.instagram.com/p/Cz1a2B3cD4e/"
},
"timestamp": "2026-08-02T14:03:13Z"
}For post.failed, target includes error_code and error_message instead of external_url — see Errors for the full list of target-level error codes.
Verifying signatures
Every delivery includes an X-OmniPost-Signature header: sha256=<hex-digest>, an HMAC-SHA256 of the raw request body using your webhook's secret. Always verify it before trusting a payload — anyone can otherwise POST a fake completion event to a guessable URL.
import crypto from "node:crypto";
import express from "express";
const app = express();
// Use the raw body, not a parsed JSON object — the signature is computed
// over the exact bytes OmniPost sent.
app.post(
"/webhooks/omnipost",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.header("X-OmniPost-Signature") ?? "";
const expected =
"sha256=" +
crypto
.createHmac("sha256", process.env.OMNIPOST_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const isValid =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!isValid) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body.toString("utf8"));
// ... handle event.event === "post.completed" | "post.failed"
res.status(200).send("ok");
}
);Use a constant-time comparison (crypto.timingSafeEqual / hmac.compare_digest) rather than === or ==, and compute the HMAC over the raw request bytes before any JSON parsing or body-transforming middleware touches them.
Retry behavior
A delivery is considered successful if your endpoint responds with a 2xx status within 10 seconds. Anything else — a non-2xx status, a timeout, or a connection error — is retried up to 5 times with exponential backoff.
| Attempt | Delay after previous attempt |
|---|---|
| 1 (initial) | — |
| 2 | ~1 minute |
| 3 | ~5 minutes |
| 4 | ~30 minutes |
| 5 | ~2 hours |
| 6 (final) | ~6 hours |
After the final attempt fails, the delivery is marked failedand no further retries are made for that event. The underlying post and target data is never lost — it's always retrievable via GET /v1/posts/:id, so a webhook outage on your side is recoverable by reconciling against the API afterward.
- Deliveries for the same event are not guaranteed to arrive in order relative to other events; always key off
post_idandtarget.platform, not arrival order. - Make your handler idempotent — retries mean the same event can be delivered more than once.
- Respond
2xxas soon as you've durably queued the event; do slow processing (sending emails, etc.) asynchronously rather than inside the request handler.