Webhooks
Webhooks tell your own systems what happened to a post without polling. Add an HTTPS endpoint under Settings → Webhooks, pick the events you want, and TimeToPost sends a signed JSON POST for each one. Org owners and admins manage endpoints; every member can read the delivery history.
Events
post.scheduled: a post was queued for a future time.post.published: the post went out on at least one platform.data.partialis true when some platforms failed and will be retried.post.failed: the post failed for good (retries exhausted, skipped as overdue, or abandoned mid-publish).draft.approved: an engine/API draft was approved and scheduled.draft.rejected: a draft was rejected.
You can also send a webhook.test event from the dashboard or the API at any time.
Payload
{
"id": "evt_3f9c1b7a5d2e48c0a1b2c3d4e5f60718",
"type": "post.published",
"created": "2026-10-24T18:30:02.114Z",
"orgId": "org_...",
"data": {
"postId": "post_...",
"status": "published",
"partial": false,
"platforms": ["twitter"],
"publishedAt": "2026-10-24T18:30:01.870Z"
}
}Headers on every request:
Content-Type: application/json
User-Agent: TimeToPost-Webhooks/1.0
TimeToPost-Event: post.published
TimeToPost-Event-Id: evt_3f9c... # idempotency key, stable across retries
TimeToPost-Delivery: <delivery id>
TimeToPost-Attempt: 1 # 1..8
TimeToPost-Signature: t=1792866602,v1=<hex hmac-sha256>Verify the signature
The signature is HMAC-SHA256(secret, "<t>.<raw body>"), hex encoded. Verify it against the raw request body, compare in constant time, and reject timestamps more than 5 minutes old to block replays. Your signing secret (whsec_...) is shown once when you create or rotate an endpoint.
import crypto from 'node:crypto';
// rawBody MUST be the exact bytes you received (e.g. express.raw), not re-serialized JSON.
export function verifyTimeToPost(secret, signatureHeader, rawBody, toleranceSeconds = 300) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => [p.slice(0, p.indexOf('=')), p.slice(p.indexOf('=') + 1)])
);
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false; // replay guard
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
// The header can carry more than one v1= value; accept if any matches.
return signatureHeader
.split(',')
.filter((p) => p.startsWith('v1='))
.some((p) => {
const got = Buffer.from(p.slice(3), 'hex');
return got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
}import hmac, hashlib, time
def verify_timetopost(secret: str, signature_header: str, raw_body: bytes, tolerance=300) -> bool:
parts = [p.split("=", 1) for p in signature_header.split(",")]
t = next((int(v) for k, v in parts if k == "t"), None)
if t is None or abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(v, expected) for k, v in parts if k == "v1")Delivery, retries and idempotency
- Respond with any
2xxwithin 10 seconds. Anything else, a timeout or a connection error counts as a failure. Redirects are not followed. - Failed deliveries retry up to 8 attempts total, waiting roughly 30s, 1m, 2m, 4m, 8m, 16m, 32m (±20% jitter).
- An event can arrive more than once. Deduplicate on
TimeToPost-Event-Id(the payloadid); the same logical event always has the same id. - Ordering is not guaranteed. Use
createdand fetch current state from the API when it matters. - After 10 consecutive deliveries fail permanently, the endpoint is auto-disabled and the dashboard shows why. Re-enable it once fixed.
- Delivery history is kept for 30 days.
Endpoint requirements
URLs must be https on port 443 and resolve to a public address. Private, loopback, link-local and cloud-metadata addresses are refused, the address is checked at delivery time (not just when you save it) and the connection is pinned to the checked address.
API
GET/api/webhooks
POST/api/webhooks
curl -X POST https://timetopost.co/api/webhooks \
-H "Authorization: Bearer $TTP_TOKEN" -H "Content-Type: application/json" \
-d '{"url":"https://your.app/hooks/timetopost","events":["post.published","post.failed"]}'
# { "success": true, "endpoint": { "id": "...", ... }, "secret": "whsec_..." } <- secret shown oncePATCH /api/webhooks/:id updates an endpoint with any of url, events, description, enabled.
DELETE/api/webhooks/:id
POST/api/webhooks/:id/rotate-secret
POST/api/webhooks/:id/test
GET/api/webhooks/:id/deliveries