Postato Docs
GuidesCookbook

Approval flow

Submit a post, wait for review, react to the outcome.

Approval flow

When a workspace has an approval policy, posts created by agents (or specific users) land in a review queue before delivery. This recipe shows how to submit a post, track the decision, and handle both outcomes cleanly.

Submit

Nothing changes on the submit side; you still call POST /posts with status: "publish". If the policy matches, the response returns status: "pending_approval" instead of queued:

const r = await fetch(
  `https://api.postato.com.br/v1/workspaces/${WORKSPACE_ID}/posts`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': crypto.randomUUID(),
    },
    body: JSON.stringify({
      platform: 'linkedin',
      accountId: LINKEDIN_ACCOUNT_ID,
      status: 'publish',
      postType: 'post',
      content: "Our Q2 results are in — read the recap.",
    }),
  }
);
const { id: postId, status } = await r.json();

if (status === 'pending_approval') {
  console.log(`Post ${postId} awaiting reviewer`);
}

Option 1: wait via webhook

The production-grade path. Register a webhook for approval.decided:

// In your webhook handler (see Webhook handler recipe for signature verification):
if (event.type === 'approval.decided') {
  const { postId, decision, reviewerId, comment } = event.data;
  if (decision === 'approved') {
    // Post is now queued for delivery automatically. Nothing else to do.
    console.log(`Post ${postId} approved by ${reviewerId}`);
  } else {
    // Rejected or expired. Notify the author, surface the comment.
    console.log(`Post ${postId} rejected: ${comment}`);
    await notifyAuthor(postId, comment);
  }
}

No polling, no waiting. Reviewer action fires the webhook within seconds.

Option 2: poll from an agent

When the agent is user-facing and the user is waiting:

async function waitForDecision(postId: string, timeoutMs = 60_000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const r = await fetch(
      `https://api.postato.com.br/v1/workspaces/${WORKSPACE_ID}/posts/${postId}`,
      { headers: { Authorization: `Bearer ${API_KEY}` } }
    );
    const { status, approval } = await r.json();
    if (status === 'queued' || status === 'published') {
      return { outcome: 'approved' };
    }
    if (status === 'failed' && approval?.decision === 'rejected') {
      return { outcome: 'rejected', reason: approval.comment };
    }
    await new Promise((res) => setTimeout(res, 3000));
  }
  return { outcome: 'timeout' };
}

Use for interactive flows where the user is actively watching (a Slack bot, a chat agent), not for background services.

Handling rejection

When rejected, you have two UX choices:

  1. Tell the author why. Surface comment and stop. User edits and resubmits as a new post.
  2. Auto-retry with updates. If the rejection is predictable ("too long", "missing disclaimer"), have the agent rewrite and resubmit. Do NOT re-use the original Idempotency-Key; generate a fresh one, the intent changed.

Never auto-retry rejected posts without understanding the reason. Rejections often signal content policy, legal risk, or brand-voice issues that a blind retry won't fix.

Scheduled + approvals

If the post was submitted as scheduled AND the policy requires approval:

  • Approval first, scheduledAt second.
  • Approved before scheduledAt → fires at the scheduled time.
  • Approved after scheduledAt → fires immediately.
  • Rejected at any point → terminal, regardless of scheduledAt.

See the Approvals guide for the full state machine.

Who can approve?

Users with the posts.approve permission in the workspace. Typically assigned to managers or content leads. Check with GET /v1/workspaces/{id}/members if you need to know programmatically who will receive the approval request.

On this page