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:
- Tell the author why. Surface
commentand stop. User edits and resubmits as a new post. - 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,
scheduledAtsecond. - 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.