Client-approval gated publishing for agency SEO ops
Gate every publish and outbound action behind explicit client approval — drafts move at agency speed, publishes wait for client trust, all powered by durable approval primitives.
Drafts move at agency speed. Publishes happen at client trust speed. Every publish (and every outbound action) pauses for the client to approve, render in their portal, edit if needed, and click go.
| Outcome | Every publish + outreach action gated by the client's explicit approval |
| Time | Free; uses existing approval primitives |
| Cost | Free; only the underlying invocation costs apply |
| Prereqs | An approval surface (Slack bot, client portal, or email) that can POST to CitationBench's approval endpoints |
What it does
agent (any skill that publishes / sends / submits)
↓ reaches the outside-world step
↓ workspace policy: require_approval
↓ invocation moves to WAITING_APPROVAL
↓ approval.created webhook fires
↓
your portal / Slack renders the preview
↓ client clicks approve / approve-with-edits / reject
↓ POST to /v1/agent/invocations/{id}/approve or /reject
↓ invocation resumes; publish actually firesStep 1 — Set the workspace policy
curl -X PATCH .../v1/workspaces/ws_acme -d '{
"settings": {
"approvalPolicy": {
"publish": "require_approval",
"outreach_send": "require_approval",
"indexing_submit": "auto"
}
}
}'This is the per-workspace default. Individual invocations can tighten further but not loosen.
Step 2 — Wire the approval webhook
curl -X POST .../v1/webhooks -d '{
"url": "https://hooks.our-portal.com/approvals",
"events": ["agent.invocation.awaiting_approval"]
}'Webhook payload:
{
"approvalId": "appr_***",
"invocationId": "inv_***",
"skill": "produce.publish",
"step": "publish_to_wordpress",
"workspace": "ws_acme",
"preview": {
"platform": "wordpress",
"title": "How engineering teams track capacity",
"slug": "engineering-team-capacity-tracking",
"url": "https://acme.com/blog/engineering-team-capacity-tracking",
"categories": ["Engineering"],
"featuredImageUrl": "https://cdn.citationbench.com/.../hero.png",
"scheduledAt": "2026-05-25T09:00:00-04:00"
},
"raisedAt": "2026-05-24T08:14:32Z",
"timeoutAt": "2026-05-31T08:14:32Z"
}Step 3 — Build the client-facing preview
Your portal renders the preview payload to the client with three actions: Approve, Edit + Approve, Reject. On click, POST back:
# Approve
curl -X POST .../v1/agent/invocations/inv_***/approve \
-H "Authorization: Bearer sk_live_***" \
-d '{ "approvalId": "appr_***", "note": "Looks good, ship it." }'
# Approve with edits
curl -X POST .../v1/agent/invocations/inv_***/approve \
-d '{
"approvalId": "appr_***",
"edits": { "title": "How engineering teams actually track capacity" },
"note": "Tightened title"
}'
# Reject
curl -X POST .../v1/agent/invocations/inv_***/reject \
-d '{ "approvalId": "appr_***", "note": "Wrong angle — rewrite with X" }'Step 4 — Slack as the approval surface (fast path)
If you don't have a portal yet:
curl -X POST .../v1/integrations/slack -d '{
"channel": "#acme-approvals",
"approvalRouting": "post-and-buttons"
}'Approvals post to Slack with Approve / Reject buttons. Click → the integration calls the API for you.
Step 5 — Time-limited approvals
For time-sensitive workflows (responding to inbound within the business day):
curl -X POST .../v1/agent/invoke -d '{
"skill": "content_factory",
"input": { "...": "..." },
"approval": {
"required": true,
"timeoutMinutes": 240,
"onTimeout": "CANCEL"
}
}'After 4 hours with no decision, the invocation cancels.
Step 6 — Trust-once patterns
Approve a particular send "and trust henceforth":
curl -X POST .../v1/agent/invocations/inv_***/approve -d '{
"approvalId": "appr_***",
"trustHenceforth": "contact"
}'Future actions to the same contact auto-approve. Useful for established partners.
One-shot setup
#!/usr/bin/env bash
set -euo pipefail
KEY="${CITATIONBENCH_API_KEY:?}"
WS="${WORKSPACE_ID:?}"
PORTAL="${PORTAL_HOOK_URL:?}"
BASE="https://api.citationbench.com/v1"
# 1. Tighten the workspace
curl -sf -X PATCH $BASE/workspaces/$WS \
-H "Authorization: Bearer $KEY" \
-d '{
"settings": {
"approvalPolicy": {
"publish": "require_approval",
"outreach_send": "require_approval",
"indexing_submit": "auto"
}
}
}'
# 2. Wire the webhook
curl -sf -X POST $BASE/webhooks \
-H "Authorization: Bearer $KEY" \
-d "{ \"url\": \"$PORTAL\", \"events\": [\"agent.invocation.awaiting_approval\"] }"
echo "Client-approval gating active for $WS. All publishes + outreach require explicit approval."Gotchas
- Auto-fire indexing. Indexing submissions are typically
autoeven when publish is gated — there's no risk to the client there. - Approval queue overload. If your client doesn't review for a week, the queue fills up. Set a
timeoutMinutesto prevent stale work. - Trust-once is per resource type. Trusting
contactdoesn't trustaccount; trustingstepdoesn't trust the whole skill. Each is its own scope. - Don't override per-invocation when workspace says approve. The
nonescope on individual invocations only works if workspace policy permits it. Otherwise the policy wins. - Slack approvals are public to the channel. Use private channels or DMs for client-sensitive previews.
Related
- API: Agent · approval
- API: Agent · invoke
- API: Production · publish
- Concept: Approval Workflows
- Concept: Eval Gates
- Concept: Workspaces
100 landing pages overnight
Feed in a keyword list, get 100 brand-aligned landing pages back the next morning — programmatic SEO at scale using a 4-step pipeline with optional auto-publish and indexing.
Competitor URL → content plan
Drop a competitor URL, get back an 8–12-item prioritized content plan targeting their winnable keywords, content gaps, and missing angles — ready to feed into bulk blog production.