yeetpost

Posting

Webhooks

Register an https endpoint and yeetpost posts post.sent and post.failed events to it as posts go out, scheduled ones included. That is the whole point: by then nobody is waiting on an HTTP response.

#Register an endpoint

curl -X POST "https://api.yeetpost.com/api/v2/webhooks" \
  -H "x-api-key: $YEETPOST_API_KEY" \
  -H "content-type: application/json" \
  -d '{
  "url": "https://example.com/hooks/yeetpost",
  "events": [
    "post.sent",
    "post.failed"
  ]
}'
response
{
  "webhook": {
    "id": "2c9a4f3b-77d1-4b0e-9f2a-8c3e1a5b7d90",
    "url": "https://example.com/hooks/yeetpost",
    "events": [
      "post.sent",
      "post.failed"
    ],
    "secret": "yp_whsec_3f1a...",
    "createdAt": "2026-08-26T09:24:11.412Z"
  }
}
The secret is shown exactly once. Store it now. There is no way to read it back, only to delete the webhook and register a new one. Pass events if you only want one of the two.

The URL has to be https and has to point at a public host: private, loopback, link-local and metadata addresses are rejected with 400 invalid_request. Deliveries do not follow redirects, so register the final URL.

#What a delivery looks like

delivery headers

x-yeetpost-eventstringrequired

The event name, the same value as event in the body.

x-yeetpost-deliverystring <uuid>required

Id of this delivery attempt's delivery row. Stable across retries of the same event, so it works as a deduplication key.

x-yeetpost-signaturestring, matches ^t=[0-9]+,v1=[0-9a-f]{64}$required

t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.

post.sent
{
  "event": "post.sent",
  "timestamp": "2026-08-26T09:24:12.004Z",
  "post": {
    "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",
    "connectionSlug": "linkedin",
    "text": "shipped the new api docs today",
    "status": "sent",
    "url": "https://www.linkedin.com/feed/update/urn:li:share:1234567890",
    "scheduledFor": null,
    "createdAt": "2026-08-26T09:24:11.412Z",
    "error": null
  }
}

Answer with any 2xx within 5 seconds. Anything else, or a timeout, is retried up to 5 attempts with exponential backoff starting at 10 seconds, after which the delivery is marked failed.

x-yeetpost-delivery is stable across retries of the same event, so it works as a deduplication key.

#Verifying the signature

Each delivery carries x-yeetpost-signature: t=<unix seconds>,v1=<hex hmac>. v1 is HMAC-SHA256("<t>.<raw request body>") keyed by your webhook secret.

  • Sign the raw body bytes, before any JSON parsing. Re-serialising changes them.
  • Compare with a constant-time comparison, not ===.
  • Reject anything whose t is more than about 5 minutes from your clock, so a captured delivery cannot be replayed at you later.
express receiver
import crypto from "crypto";
import express from "express";

const app = express();

app.post(
  "/hooks/yeetpost",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const header = String(req.headers["x-yeetpost-signature"]);
    const timestamp = header.match(/t=(\d+)/)?.[1] ?? "";
    const signature = header.match(/v1=([0-9a-f]{64})/)?.[1] ?? "";
    const rawBody = req.body.toString("utf8");

    const expected = crypto
      .createHmac("sha256", process.env.YEETPOST_WEBHOOK_SECRET)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");

    const isSignatureValid =
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
    const isFresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;

    if (!isSignatureValid || !isFresh) {
      res.status(400).send("bad signature");
      return;
    }

    const event = JSON.parse(rawBody);
    console.log(event.event, event.post.id);
    res.sendStatus(200);
  },
);

app.listen(3000);

#The delivery log

When a receiver is not getting events, read the log instead of guessing: what was sent, how it went, and the last error when it did not land.

curl "https://api.yeetpost.com/api/v2/webhooks/2c9a4f3b-77d1-4b0e-9f2a-8c3e1a5b7d90/deliveries" \
  -H "x-api-key: $YEETPOST_API_KEY"
response
{
  "deliveries": [
    {
      "id": "8d3f2b7c-4e1a-4c6d-9b0f-1a2b3c4d5e6f",
      "event": "post.sent",
      "status": "delivered",
      "attempts": 1,
      "createdAt": "2026-08-26T09:24:11.412Z",
      "deliveredAt": "2026-08-26T09:24:12.004Z",
      "lastError": null
    },
    {
      "id": "1c4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f60",
      "event": "post.failed",
      "status": "failed",
      "attempts": 5,
      "createdAt": "2026-08-25T18:02:44.118Z",
      "deliveredAt": null,
      "lastError": "response status 500"
    }
  ],
  "limit": 25
}

pending is waiting for its next attempt, processing is being sent right now, failed ran out of attempts.

Deliveries are kept for 30 days, and limit is 1 to 100, default 25.

#Listing and deleting

Your webhooks, newest first. Secrets are never included.

curl "https://api.yeetpost.com/api/v2/webhooks" \
  -H "x-api-key: $YEETPOST_API_KEY"

Deleting stops delivery. Deliveries still queued for that webhook are marked failed.

curl -X DELETE "https://api.yeetpost.com/api/v2/webhooks/2c9a4f3b-77d1-4b0e-9f2a-8c3e1a5b7d90" \
  -H "x-api-key: $YEETPOST_API_KEY"

#Full schemas

Both event bodies and every webhook endpoint are on the API reference.