Polling events for automations

Poll GET /v1/events to react when a post goes out, a post fails, a draft is ready or a post takes off in your niche.

Postel has no webhooks. Your automation polls GET /v1/events every few minutes and acts on events it hasn't seen before. This is how the Zapier, n8n and Make apps work too.

ParameterTypeDefaultDescription
typestringrequiredThe event type. One type per request.
sincestringISO 8601 time with a timezone. Only events after it.
beforestringISO 8601 time with a timezone. Only events at or before it, to page back.
limitinteger25Events per page, 1 to 100.

Events come newest first.

Event types

TypeFires whenPayloadPlans
post.publishedA post went out. Replies Postel sends for you are left out.postEvery plan
post.failedA network refused a post, or part of a thread.post, with failure setEvery plan
draft.readyPostel wrote a draft for you that is waiting in Review.draftProfessional, Team and Enterprise
viral.newPostel found a post that took off in your niche, in For you.viralPostProfessional, Team and Enterprise

On plans without For you, draft.ready and viral.new return an empty list. They don't return an error.

The event object

FieldTypeDescription
idstringUnique per occurrence. Dedupe on it.
typestringOne of the types above.
occurredAtstringWhen it happened, UTC ISO 8601.
postobject or nullFor post.published and post.failed. A post.
draftobject or nullFor draft.ready. See below.
viralPostobject or nullFor viral.new. A viral post with an extra fit field.

draft has id, platform, text, writtenAt, plannedFor (the time Postel suggests, or null) and reviewUrl, the link to review, edit and schedule it in Postel.

viralPost.fit is a number from 0 to 1: how well the post fits your niche, as Postel scored it.

post.published
{
  "events": [
    {
      "id": "post.published:cmhjz4w2p0001l708xq3kd9vb",
      "type": "post.published",
      "occurredAt": "2026-12-01T08:30:03.000Z",
      "post": {
        "id": "cmhjz4w2p0001l708xq3kd9vb",
        "platform": "linkedin",
        "status": "published",
        "text": "We shipped our public API today. Three things we learned writing the docs first.",
        "thread": [
          "We shipped our public API today. Three things we learned writing the docs first."
        ],
        "firstComment": null,
        "mediaCount": 0,
        "scheduledAt": "2026-12-01T08:30:00.000Z",
        "publishedAt": "2026-12-01T08:30:03.000Z",
        "url": "https://www.linkedin.com/feed/update/urn:li:share:7268000123456789012/",
        "failure": null,
        "createdAt": "2026-11-04T13:05:12.481Z",
        "updatedAt": "2026-12-01T08:30:03.000Z"
      },
      "draft": null,
      "viralPost": null
    }
  ]
}

Ids are stable

The same occurrence always has the same id, so you can store ids you've handled and skip them next time.

  • post.published fires once per post.
  • post.failed fires once per failure. A post that fails, gets fixed and fails again has two events with different ids.
  • draft.ready fires again when Postel rewrites a draft.
  • viral.new fires once per post Postel found for you.

Don't parse ids. Their format can change.

A poll loop

Every few minutes:

  1. Ask for events since a minute before the newest occurredAt you've handled. The overlap catches events that land late.
  2. If the page is full, page back with before set to the oldest occurredAt on the page, until a page comes back short.
  3. Skip every id you've already handled. Handle the rest, oldest first.
  4. Remember the newest occurredAt and the ids you handled.
const API = "https://www.postel.app/api/v1/events";
const headers = { Authorization: `Bearer ${process.env.POSTEL_API_KEY}` };
const LIMIT = 100;

// Load these from your own storage between runs
let newest = null;
const seen = new Set();

async function fetchPage(type, since, before) {
  const url = new URL(API);
  url.searchParams.set("type", type);
  url.searchParams.set("limit", String(LIMIT));
  if (since) url.searchParams.set("since", since);
  if (before) url.searchParams.set("before", before);
  const res = await fetch(url, { headers });
  if (!res.ok) throw new Error(`${res.status} ${(await res.json()).code}`);
  return (await res.json()).events;
}

async function poll(type, handle) {
  const since = newest
    ? new Date(new Date(newest).getTime() - 60_000).toISOString()
    : undefined;
  const events = [];
  let before;
  for (let page = 0; page < 10; page++) {
    const batch = await fetchPage(type, since, before);
    events.push(...batch);
    if (batch.length < LIMIT) break;
    before = batch.at(-1).occurredAt;
  }

  const fresh = events
    .filter((event) => !seen.has(event.id))
    .filter((event, i, all) => all.findIndex((e) => e.id === event.id) === i)
    .reverse();
  for (const event of fresh) {
    await handle(event);
    seen.add(event.id);
    if (!newest || event.occurredAt > newest) newest = event.occurredAt;
  }
}

await poll("post.published", async (event) => {
  console.log("Published:", event.post.url);
});

Keep the set of seen ids bounded, for example the last 1,000. With the one-minute overlap, older ids never come back.

How often to poll

Every 1 to 15 minutes is plenty. Each poll is one request per event type, and every key has 60 requests a minute.

React to failures

A post.failed event carries the post with its failure. Most failures put the post back in drafts. Fix the cause, often an expired connection, then schedule it again.

If failure.partlyPublished is true, part of a thread is already live. Don't resend it. See A partly published thread.

On this page