Retries and polling
A delivery counts as successful when your endpoint answers with any 2xx status. Anything else, including a timeout or a connection error, schedules a retry.
Schedule
Section titled “Schedule”Up to 6 attempts over about 15 hours:
| Attempt | Delay after the previous one |
|---|---|
| 1 | immediately |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 12 hours |
After the sixth failure the event is marked undelivered and not sent again. It stays readable through GET /v1/events.
Respond fast. Verify the signature, write the event somewhere durable, return 200, and process afterwards. A handler that calls Meta, sends email and renders a PDF before answering will time out under load and be retried, which brings the next point.
Idempotency
Section titled “Idempotency”Every retry carries the same event id in the body and in X-MissLess-Delivery. Your handler will see the same event more than once, both from retries and from the occasional duplicate. Treat the id as the unit of work:
async function handle(event) { const fresh = await db.insertIgnore('missless_events', { id: event.id, type: event.type, received_at: new Date() }); if (!fresh) return; // already processed switch (event.type) { case 'post.published': case 'post.partial': case 'post.failed': await updatePost(event.data.post); break; // ... }}Keep processed ids for at least a day; 15 hours is the retry horizon.
Ordering
Section titled “Ordering”Deliveries are not ordered. Two events for the same post can arrive out of sequence when one is retried. Use the timestamps inside data (updated on posts, last_message_at on conversations) to decide which state is newer, not the arrival order.
Polling with GET /v1/events
Section titled “Polling with GET /v1/events”GET /v1/events lists the same events the webhook delivers, whether or not you have a webhook configured. Use it as your only integration if you prefer polling, or as a catch-up after an outage.
| Query | Notes |
|---|---|
workspace |
One workspace |
type |
One event type |
since |
ISO-8601. Events with created_at after this instant |
page, limit |
Pagination, default 50, max 200. Oldest first |
curl "https://social.missless.tel/v1/events?workspace=ws7k2m9p4q1r8t3&type=post.published&since=2026-08-25T00:00:00Z" \ -H "Authorization: Bearer $MISSLESS_API_KEY"{ "data": [ { "id": "evt1a3c5e7g9i2k", "type": "post.published", "created_at": "2026-08-25T09:20:11Z", "partner": "acme", "workspace": "ws7k2m9p4q1r8t3", "external_id": "user_123", "data": { "id": "pst5g7j9l2n4q6s", "external_ref": "acme:post:1234", "status": "published" } } ], "page": 1, "limit": 50, "total": 1}Each item is exactly the body a webhook delivery would carry. Run the same handler over it.
A polling loop that behaves:
- Store the
created_atof the newest event you processed as a cursor. - Every minute,
GET /v1/events?since=<cursor>and page through untildatais empty. - Process each event through the same idempotent handler, so overlap with webhooks is harmless.
- Move the cursor forward only after the page is processed.
Polling costs one request per minute per poller against the 300 per minute per key limit. Poll per partner, not per workspace, unless you have a reason.
Catch-up after an outage
Section titled “Catch-up after an outage”If your endpoint was down for longer than the retry horizon, some events were never delivered. Fetch GET /v1/events?since=<last good timestamp> once and run them through your handler. Because it de-duplicates on id, the events that did arrive are skipped and the missing ones are filled in.