Errors & blockers

How Many Posts Can I Schedule at Once?

PostGate has no dedicated bulk endpoint, so here's what scheduling in bulk actually means: looping POST /api/posts with per-row idempotency keys, realistic client-side concurrency limits, and why async per-target delivery keeps a large batch fast.

Quick answer

There is no hard cap baked into a single call — PostGate has no bulk endpoint, so bulk scheduling means calling POST /api/posts once per post in a loop your own client controls. Use a distinct idempotency key per row, cap concurrency at roughly 5–10 in-flight requests, and don't wait on delivery: the response is acceptance, and per-target publishing happens asynchronously.

Short answer first, honestly: PostGate has no dedicated bulk or batch endpoint, so there isn't a hard number baked into a single API call. "Scheduling in bulk" means calling POST /api/posts once per post, the same endpoint you'd use for one, in a loop your own client controls. That's not a limitation to work around quietly — it's a design that puts the concurrency, ordering, and retry behavior in your hands instead of hiding them inside an opaque batch job you can't inspect mid-run. Here's how to do that loop safely, at whatever volume you actually need.

PostGate handles OAuth, publishing and delivery evidence for every network through one API. Start free or read the docs.

01

Why is there no /api/posts/batch endpoint?

Every post — whether you're creating one or four hundred — goes through the same create-post endpoint, accepting one post body, a list of target profiles, and optional media. A dedicated batch endpoint would need its own partial-failure semantics, its own way to report which of two hundred rows succeeded, and its own idempotency story distinct from the one the single-post endpoint already has. Looping over the existing endpoint reuses all of that instead of duplicating it — every post you create in a bulk run gets the exact same per-target delivery tracking, retries, and idempotency guarantees as a post created one at a time from a UI.

02

Why a distinct idempotency key per row, not per batch?

The risk in any loop that fires many writes is a partial failure: your process crashes, a network blip drops a response, or a deploy interrupts the run at row 140 of 300. Retrying the whole batch naively would re-publish the 139 rows that already succeeded. The fix is the same one that protects a single post — a stable Idempotency-Key — applied per row instead of per batch. PostGate honors each key for 72 hours: rerun the exact same request with the same key and you get the original result back, not a second post; reuse the key with different content and you get a 409 conflict instead of a silent overwrite. Derive the key from something stable in your own data — a CSV row's unique ID, a queue message ID — never from a fresh random value generated at request time, or a retry of the same logical row looks like a brand-new one to the API.

A key that survives a retried run without duplicating rowsPostGate API
const idempotencyKey = `bulk-import-2026-08-26:${row.id}`;

await fetch('https://postgate.studio/api/posts?profile_group_id=grp_acme123', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${POSTGATE_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': idempotencyKey,
  },
  body: JSON.stringify({
    post: { body: row.caption },
    profiles: row.profileIds,
    media: row.mediaUrl ? [row.mediaUrl] : [],
    ...(row.scheduledAt ? { scheduled_at: row.scheduledAt } : {}),
  }),
});
03

How do you rate-limit yourself before a platform does?

Firing three hundred requests with Promise.all and no concurrency cap is the fastest way to trip a 429 — not from PostGate's API itself, but from the upstream platform a given post targets once delivery actually reaches it. Cap how many create-post calls run concurrently, and treat a rate_limited or platform_transient error — the two codes PostGate's error envelope marks retryable: true — with exponential backoff and jitter, reusing the same idempotency key on every retry of that row. A concurrency limit of somewhere around 5–10 in-flight requests is a reasonable default for a bulk run against a single workspace; there's no universal number because it depends on how many distinct platforms and accounts the batch is spread across, not on a limit PostGate itself imposes on the number of posts you can create.

A bounded-concurrency loop with per-row retry on retryable errorsPostGate API
async function createPostWithRetry(row, { maxAttempts = 4 } = {}) {
  const key = `bulk-import-2026-08-26:${row.id}`;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await fetch('https://postgate.studio/api/posts?profile_group_id=grp_acme123', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.POSTGATE_API_KEY}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': key,
      },
      body: JSON.stringify({ post: { body: row.caption }, profiles: row.profileIds, media: [] }),
    });
    if (res.ok) return res.json();

    const { error } = await res.json();
    if (!error.retryable || attempt === maxAttempts) throw new Error(`${row.id}: ${error.code}`);
    await new Promise((r) => setTimeout(r, 2 ** attempt * 500 + Math.random() * 250));
  }
}

// Cap concurrency instead of firing every row at once.
async function runBulk(rows, concurrency = 8) {
  const queue = [...rows];
  const results = [];
  async function worker() {
    let row;
    while ((row = queue.shift())) results.push(await createPostWithRetry(row));
  }
  await Promise.all(Array.from({ length: concurrency }, worker));
  return results;
}
04

Does firing the next post wait for the last one to finish?

A create-post response is acceptance, not proof of publication — the request returns as soon as PostGate has validated and queued the post, before any provider has actually been called. That matters directly for throughput: your loop doesn't need to block on a platform's own publish latency (which can run seconds for a simple text post to well over a minute for a video that needs provider-side processing) before moving to the next row. Delivery to each platform target happens asynchronously behind the response, tracked through the per-target status model — pending, publishing, then a terminal published, failed, or skipped — so a bulk run's real bottleneck is how fast your client can accept and queue requests, not how fast every platform gets around to actually publishing them. Subscribe to terminal webhooks to find out which rows in a large batch actually landed, rather than polling every post you just created.

Frequently asked questions

Does PostGate have a bulk or batch scheduling endpoint?

No — there is no /api/posts/batch, and that is a deliberate shape. A dedicated batch endpoint would need its own partial-failure semantics, its own way to report which of two hundred rows succeeded, and its own idempotency story. Looping over the single create-post endpoint reuses all of that: every post in a bulk run gets the same per-target delivery tracking, retries, and idempotency guarantees as a post created one at a time.

How do I safely retry a bulk run that failed halfway through?

Give each row its own stable Idempotency-Key derived from your own data — a CSV row's unique ID or a queue message ID, never a fresh random value generated at request time. PostGate honors each key for 72 hours: rerunning the same request with the same key returns the original result instead of a second post, and reusing a key with different content returns a 409 conflict instead of a silent overwrite. A crashed run at row 140 of 300 can then be rerun from the top without re-publishing the rows that already succeeded.

How many concurrent scheduling requests should I send?

Cap concurrency at somewhere around 5-10 in-flight requests for a bulk run against a single workspace. There is no universal number because the real constraint is the upstream platforms your posts target, not a limit PostGate imposes on how many posts you can create — firing three hundred requests with no cap is the fastest way to trip a 429 downstream. Treat rate_limited and platform_transient errors with exponential backoff and jitter, reusing the same idempotency key per row.

Do I have to wait for each post to publish before creating the next?

No. The create-post response is acceptance, not proof of publication — it returns as soon as the post is validated and queued, before any provider is called. Delivery to each platform target happens asynchronously behind the response through the per-target status model, so a bulk run's real bottleneck is how fast your client can queue requests. Subscribe to terminal webhooks to learn which rows actually landed instead of polling every post you just created.

Build it with PostGate

One API for every social platform your customers care about — sandboxed, documented, and ready today.

Start building Read the docs