letmepost / api surfaces / Webhooks
POST /v1/webhook-endpoints

Stop polling .

HMAC-signed delivery for every state transition.

Subscribe to 8 lifecycle events with one POST. Webhooks land on your endpoint within median 800ms of each transition. HMAC-SHA256 signatures with per-endpoint secrets. Replay-safe with explicit retry budget and a dead-letter queue. Same envelope shape as the HTTP API.

8 event types · HMAC-SHA256 · exponential retry · API reference →
POST /v1/webhook-endpoints ·
{
  "url": "https:x0
  "events": [
    "post.queued",
    "post.published",
    "post.failed",
    "token.expiring"
  ]
}

Why webhooks vs polling?

Polling GET /v1/posts/:id

  • You eat your own rate limit just to check status
  • Latency = whatever your poll interval is
  • Wastes compute on the 99% of polls that are no-change
  • No signal for token expiry, version deprecations, quota walls
  • You write the backoff logic yourself
  • Multi-target posts: N status calls per publish

letmepost Webhooks

  • Push delivery, median 800ms from event to your endpoint
  • Zero polling overhead, zero rate-limit hits
  • HMAC-signed payloads with per-endpoint secret rotation
  • Lifecycle, token, version, quota events all on one channel
  • Exponential retry built in, then DLQ as last resort
  • One event per state transition, no duplicate-busy work

✓ Same envelope as the HTTP API

Webhook payloads carry the same fields you'd get from a synchronous response: id, status, error, request_id. You can grep webhooks the same way you grep logs.


EVENT TYPES

8 lifecycle signals across post + auth + platform
post.queued
post.validated
post.published
post.failed
post.rejected
token.expiring
token.revoked
version.deprecated

DELIVERY MODEL

how a webhook lands · retries on failure · dlq as last resort

Event fires

Payload built, signature computed with your per-endpoint secret. Stamped with X-LMP-Signature + X-LMP-Timestamp headers. Median latency before first POST: ~50ms.

Delivery attempt 1

We POST to your endpoint. Success = HTTP 2xx within 10s. Anything else is a failure: timeout, 4xx, 5xx, connection refused.

Retry on failure

Exponential backoff: 30s, 2min, 10min, 30min, 2h, 6h. Six attempts over ~9 hours. Each carries the same payload, signature, and request id.

DLQ after exhaustion

Six failed attempts: delivery moves to your endpoint's dead-letter queue. Inspect via the dashboard or POST /v1/webhook-endpoints/:id/replay/:event_id.


FEATURES

things you don't have to build

8 event types

post.queued, post.validated, post.published, post.failed, post.rejected, token.expiring, token.revoked, version.deprecated.

HMAC-SHA256 signatures

Per-endpoint secret. Signature computed over timestamp.body. Verify with timingSafeEqual to avoid timing attacks. Replay-window enforcement built in.

Exponential retry

Six attempts over ~9h on failure: 30s, 2min, 10min, 30min, 2h, 6h. Each retry carries the same payload + signature.

Dead-letter queue

After retries exhaust, delivery lands in DLQ. 7-day inspection on Pro, 30-day on Business. Replay any event manually.

Per-endpoint secret rotation

POST /v1/webhook-endpoints/:id/rotate-secret. Old secret stays valid for 12 hours so you can roll your handler without dropping deliveries.

Verify helper in the SDK

The TS + Python SDKs ship a verifyWebhook() helper that checks signature + timestamp window. Drop into Express, Fastify, FastAPI in two lines.

Webhooks fire on every Publishing API write. Pair them up.
Publishing API →

CODE EXAMPLE

verify a webhook · typescript
webhook-handler.ts ·
import { createHmac, timingSafeEqual } from 'node:crypto';

// Verify HMAC-SHA256 over `timestamp.body` against the X-LMP-Signature
// header. Use timingSafeEqual to avoid timing-attack signal leakage.

function verifyLetmepostWebhook(req, secret) {
  const sig = req.headers['x-lmp-signature'] as string;
  const ts  = req.headers['x-lmp-timestamp'] as string;

  // Replay window — reject anything older than 5 min
  const age = Math.abs(Date.now() / 1000 - parseInt(ts, 10));
  if (age > 300) return false;

  const expected = createHmac('sha256', secret)
    .update(`${ts}.${req.rawBody}`)
    .digest('hex');

  return timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}

app.post('/lmp-webhook', (req, res) => {
  if (!verifyLetmepostWebhook(req, process.env.LMP_WEBHOOK_SECRET)) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.rawBody);
  switch (event.type) {
    case 'post.published': /* update your UI */ break;
    case 'post.failed':    /* notify the user */ break;
    case 'token.expiring': /* prompt re-auth */  break;
  }
  res.status(200).end();
});


COMMON QUESTIONS

about webhook delivery

How fast do webhooks arrive?

Median 800ms from event firing to your endpoint receiving the POST. P95 under 3s. Long-tail bounded by upstream platform latencies (e.g. Meta video transcoding).

What if my endpoint is down?

We retry on exponential backoff: 30s, 2min, 10min, 30min, 2h, 6h. Six attempts over ~9h. Then the delivery lands in DLQ where you can replay it manually.

Do I need a separate endpoint per event?

No. One endpoint can subscribe to all 8 events. You dispatch on event.type in your handler. But you can subscribe multiple endpoints if you want to route events to different services.

How do I rotate a webhook secret?

POST /v1/webhook-endpoints/:id/rotate-secret. Old secret stays valid for 12 hours after rotation so you can roll your handler without dropping deliveries.

Are webhooks counted against my quota?

No. Webhook deliveries are free, unlimited, and don't consume any post-quota units. We only meter writes you initiate.

Can I replay events from the past?

Yes on Pro and Business: replay any event from the last 7 days (Pro) or 30 days (Business) via the dashboard or POST /v1/webhook-endpoints/:id/replay.

How big can payloads get?

Typical payload is 2–4 KB. Maximum 16 KB. Embedded raw upstream responses are truncated past the cap; the full body is always available via GET /v1/posts/:id.


OTHER SURFACES

the rest of the api

LEARN MORE


READY TO SUBSCRIBE?

One POST. Eight event types. Stop polling, start listening.

* * * PUSH · NOT POLL * * *
SURFACE · WEBHOOKS · /v1/webhook-endpoints
→ START FREE