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

EventFires when
post.completedA target successfully publishes to its platform (status: published).
post.failedA 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.

AttemptDelay 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_id and target.platform, not arrival order.
  • Make your handler idempotent — retries mean the same event can be delivered more than once.
  • Respond 2xx as soon as you've durably queued the event; do slow processing (sending emails, etc.) asynchronously rather than inside the request handler.