Webhooks
Register an https endpoint and yeetpost posts events to it as posts go out and come down and as accounts are connected and disconnected, scheduled posts 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"
]
}'{
"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"
}
}events if you only want some of them.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
post.sent: A post was publishedpost.failed: A post could not be publishedpost.deleted: A post was deleted on its platformpost.metrics: A day's reading of a post's numberspost.rescheduled: The system moved a pending post's own timeconnection.created: An account was connectedconnection.removed: An account was disconnected
The two connection events carry the profileId the account was connected into, which is how a platform built on profiles keeps its own records in step. Every event body below has one too.
post.metrics fires once a day, on the first read of each UTC day, for as long as a post is still being refreshed: one delivery per post per day for 30 days, not one per hourly read. A number the platform does not report back is null rather than 0.
post.rescheduled fires whenever a pending post's own time changes, with why in reason: missed_slot (a queued post missed its slot and moved to the next free one), fair_use (a fair-use deferral pushed it to the next UTC day), requeued (a failed post was requeued by hand), or catch_up (a hand-scheduled post was spaced out behind a backlog draining on its connection).
delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
{
"eventId": "8d3f2b7c-4e1a-4c6d-9b0f-1a2b3c4d5e6f",
"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: 7 attempts in all, waiting 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours between them, so the last attempt lands about 21 hours after the first and a receiver that is down over a night still gets the event. After that the delivery becomes a dead letter, status dead, and stays in the log to be replayed.
x-yeetpost-event-id is the id of the event itself: every attempt carries the same value, and so does a replay. Store it and drop what you have already seen and one event cannot be handled twice. x-yeetpost-mode says whether the post was live or made with a test key, so a test receiver can tell them apart.
#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
tis more than about 5 minutes from your clock, so a captured delivery cannot be replayed at you later.
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"{
"deliveries": [
{
"id": "8d3f2b7c-4e1a-4c6d-9b0f-1a2b3c4d5e6f",
"eventId": "8d3f2b7c-4e1a-4c6d-9b0f-1a2b3c4d5e6f",
"event": "post.sent",
"status": "delivered",
"attempts": 1,
"createdAt": "2026-08-26T09:24:11.412Z",
"nextAttemptAt": "2026-08-26T09:24:11.412Z",
"deliveredAt": "2026-08-26T09:24:12.004Z",
"deadAt": null,
"lastStatusCode": 200,
"lastError": null
},
{
"id": "1c4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f60",
"eventId": "1c4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f60",
"event": "post.failed",
"status": "dead",
"attempts": 7,
"createdAt": "2026-08-25T18:02:44.118Z",
"nextAttemptAt": "2026-08-26T14:39:12.882Z",
"deliveredAt": null,
"deadAt": "2026-08-26T14:39:13.204Z",
"lastStatusCode": 500,
"lastError": "response status 500"
}
],
"limit": 25,
"hasMore": false
}pending is waiting for its next attempt, processing is being sent right now, failed means the webhook was deleted under it, dead ran out of attempts and is only sent again by a replay.
Narrow it with status (pending, delivered, failed, dead) and page with limit (1 to 100, default 25) and startingAfter, the id of the last delivery on the previous page. New deliveries land at the top of this list while you page it, so the next page is the deliveries older than that one rather than the ones past an offset. hasMore tells you when to stop.
Deliveries are kept for 30 days. A dead letter's 30 days run from when it died rather than from when it was queued, so a delivery that spent its last day retrying is still there to replay.
#Replaying a delivery
Send one again. It is a new attempt chain on the same event: the same eventId, the same body, the same signature rules, so a receiver that deduplicates on the event id sees the event it missed rather than a new one.
curl -X POST "https://api.yeetpost.com/api/v2/webhooks/2c9a4f3b-77d1-4b0e-9f2a-8c3e1a5b7d90/deliveries/1c4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f60/replay" \
-H "x-api-key: $YEETPOST_API_KEY"A dead, failed or delivered delivery can be replayed. One that is still pending or processing is refused: it is already on its way.
#Testing a receiver
Queue a post.sent delivery with a fixed, obviously fake post in it and "test": true at the top level of the body, so you can check a receiver and its signature check without posting anything.
curl -X POST "https://api.yeetpost.com/api/v2/webhooks/2c9a4f3b-77d1-4b0e-9f2a-8c3e1a5b7d90/test" \
-H "x-api-key: $YEETPOST_API_KEY"It goes out through the normal worker, signed and retried like any other delivery, and it shows up in the delivery log. The webhook gets it whatever it is subscribed to, because you asked for it by id.
#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.