Approvals, limits and activity

Make posts wait for a person, give each API key and AI app its own limits, and see who did what.

View as MarkdownOpenAPIBase URL https://postwire.io

Approvals, per brand. A brand's rules decide which posts wait for a person before they go out: by where they come from (the dashboard, an API key, an AI agent over MCP such as Claude, ChatGPT or Cursor, or n8n/Make/Zapier), optionally only on some networks, and optionally only when the post contains a link, mentions a currency amount or uses a word from the brand's list. Plain rules, checked on PostWire's server before anything is sent; no AI is involved. A post that waits is answered with 202 and status: "pending_approval" and is not published. The account owner approves it in the dashboard (Approvals) or from the emailed link; on Pro and above, extra approvers (an agency's client) get their own link and need no PostWire account. Each link is signed, expires (72 hours by default) and works once. The approval page shows every network's version (account, text, media, length against the network's limit) and lets the approver approve all, approve some networks, edit a network's text, pick another time, or reject with a reason. A post whose scheduled time passed while it waited goes out right after approval. Reminders go out after 12 hours by default (up to 3), and the owner can also have the link posted in the brand's Telegram or Discord.

Nobody but a person approves. An API key or an AI connector cannot approve or reject a post, nor change the rules or the limits: those endpoints answer 403 approval_requires_human to anything but a signed-in dashboard session. GET /api/post/status with the returned id says whether a post was approved.

Limits per API key and AI app. A monthly post cap (a post counts once per network; a network that fails is given back), the networks and brands it may post to, and "every post from it waits for approval". Refusals: 403 key_network_not_allowed, key_brand_not_allowed, key_monthly_cap_reached.

Activity log. Every account: posts published, failed and canceled, approvals, keys created and revoked, settings changed and the X credits each X post used, each with its source (the dashboard, an API key by name, an AI app, n8n, an approver's email). Filter it and export CSV. Free keeps 7 days, Starter 30, Pro 90, Agency and Scale 365.

Plans. Approvals by the owner and key limits: every paid plan. Client approvers: Pro (5 per brand), Agency (25), Scale (100). Free can switch the rules off but not on.

Approvals: posts waiting for a person, and the ones decided

GET/api/approvals

Newest first, each with every network's preview (account, text, title, media, length against the network's limit and, for Instagram and LinkedIn, about where the feed folds it), why it waits, who was notified, the decision and the post's state. counts = how many per status. can_decide = whether this caller may decide (only a signed-in dashboard session).

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
statusquery, stringpending | approved | rejected | canceled; omit for all
brand_idquery, string (uuid)Only this brand
limitquery, integer1 to 200, default 50
beforequery, string (date-time)ISO time: only approvals created before it (paging)

Responses

StatusMeans
200{ approvals: [{ id, status, schedule_id, brand_id, mode: now|scheduled, run_at, platforms, targets, reasons: [{ code, text }], source: { kind, label }, notified, decision, decided_by, decided_via: link|dashboard|requester, decided_at, reminders, next_reminder_at, preview: [{ platform, name, account, text, title, chars, max, over, fold, media }], post: { status, run_at, results, last_error } }], counts: { pending, approved, … }, can_decide }
503governance_unavailable
curl
curl "https://postwire.io/api/approvals?status=pending" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
const res = await fetch("https://postwire.io/api/approvals?status=pending", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.get(
    "https://postwire.io/api/approvals?status=pending",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())

Example response

JSON
{
  "approvals": [
    {
      "id": "7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
      "status": "pending",
      "mode": "now",
      "platforms": [
        "linkedin",
        "x"
      ],
      "source": {
        "kind": "agents",
        "label": "Claude (AI connector)"
      },
      "reasons": [
        {
          "code": "source_agents",
          "text": "Posts from AI agents (Claude, ChatGPT, Cursor and other MCP clients) need approval"
        },
        {
          "code": "link",
          "text": "Contains a link: bakery.com/rye"
        }
      ],
      "notified": [
        {
          "to": "o***@bakery.com",
          "role": "owner",
          "via": "email",
          "sent": true
        },
        {
          "to": "m***@client.com",
          "role": "external",
          "via": "email",
          "sent": true
        }
      ],
      "preview": [
        {
          "platform": "x",
          "name": "X",
          "account": "@bakery (Bakery)",
          "text": "Our new rye is out: bakery.com/rye",
          "chars": 37,
          "max": 280,
          "over": false,
          "media": []
        }
      ]
    }
  ],
  "counts": {
    "pending": 1,
    "approved": 12,
    "rejected": 2
  },
  "can_decide": true
}

Abridged: each approval also carries its targets, decision, reminders and the post's state.

One approval, with its preview

GET/api/approvals/{id}

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
id requiredpath, string (uuid)

Responses

StatusMeans
200{ approval: { …, review_url, post }, can_decide }
404not_found
curl
curl "https://postwire.io/api/approvals/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
const res = await fetch("https://postwire.io/api/approvals/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.get(
    "https://postwire.io/api/approvals/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())

Approve or reject a waiting post (dashboard session only)

POST/api/approvals/{id}/decision

Only a person signed in to the PostWire dashboard can call this: an API key made in API & MCP, an AI connector or any other source gets 403 approval_requires_human — an agent can never approve its own post. Approve all networks, or only `networks`; `edits` replaces a network's text before it goes out; `run_at` sets a new time. A post whose time is now or has passed is published at once and the response carries each network's result; a post approved before its time goes out at that time. Reject keeps `reason`, shown to whoever sent it. One decision wins: a second one answers 409 not_pending.

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
id requiredpath, string (uuid)

Body (JSON)

FieldTypeDescription
action requiredstringOne of: approve, reject.
networksarray of stringApprove only these networks of the post; the others are dropped from it. Default: all.
editsobject{ "<network>": { "text": "…", "title": "…" } } — the approved text for that network.
run_atstring (date-time)Publish at this time instead (not in the past, up to 365 days ahead).
reasonstringWhy it was rejected (or a note with an approval). Up to 500 characters.

Responses

StatusMeans
200{ ok, decision: approved|rejected, approval, late (its time had passed), post: { id, status, run_at, platforms }, results? (when it was published now), message }
400bad_request (no network chosen, an emptied text, a past run_at)
403approval_requires_human
404not_found
409not_pending: already decided, or withdrawn
curl
curl -X POST "https://postwire.io/api/approvals/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f/decision" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "action": "approve",
  "networks": [
    "linkedin"
  ],
  "edits": {
    "linkedin": {
      "text": "Our new rye is out — 12 € a loaf. bakery.com/rye"
    }
  }
}'
Node
const res = await fetch("https://postwire.io/api/approvals/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f/decision", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "action": "approve",
    "networks": [
      "linkedin"
    ],
    "edits": {
      "linkedin": {
        "text": "Our new rye is out — 12 € a loaf. bakery.com/rye"
      }
    }
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.post(
    "https://postwire.io/api/approvals/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f/decision",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
    json={
        "action": "approve",
        "networks": ["linkedin"],
        "edits": {
            "linkedin": {
                "text": "Our new rye is out — 12 € a loaf. bakery.com/rye",
            },
        },
    },
)
print(r.status_code, r.json())

Example response

JSON
{
  "ok": true,
  "decision": "approved",
  "late": false,
  "post": {
    "id": "5d0c…",
    "status": "done",
    "platforms": [
      "linkedin"
    ]
  },
  "results": [
    {
      "ok": true,
      "platform": "linkedin",
      "url": "https://www.linkedin.com/feed/update/urn:li:share:72…"
    }
  ],
  "message": "Approved and published to Bakery on LinkedIn."
}

What an approval link shows (no API key: the link is the credential)

POST/api/approvals/link

The page at /approve/#t=<token> calls this. Each approver gets their own link: HMAC-signed, expiring (72 hours by default, set per brand) and good for one decision. It never carries key ids.

No API key needed.

Body (JSON)

FieldTypeDescription
t requiredstringThe token from the link's #t= fragment.

Responses

StatusMeans
200{ approval (with preview), brand, requested_by (masked), you: { role: owner|external|chat, approver }, link: { expires_at, used, expired }, can_decide, post }
400link_invalid
404link_invalid
410link_expired
curl
curl -X POST "https://postwire.io/api/approvals/link" \
  -H "Content-Type: application/json" \
  -d '{
  "t": "apr1.eyJh…"
}'
Node
const res = await fetch("https://postwire.io/api/approvals/link", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    "t": "apr1.eyJh…"
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.post(
    "https://postwire.io/api/approvals/link",
    json={
        "t": "apr1.eyJh…",
    },
)
print(r.status_code, r.json())

Approve or reject through an approval link

POST/api/approvals/link/decision

Same decision as POST /api/approvals/{id}/decision, by the person the link was sent to (the account owner, a client approver on Pro and above, or the brand's Telegram/Discord chat when switched on). The link is spent by the decision.

No API key needed.

Body (JSON)

FieldTypeDescription
t requiredstring
action requiredstringOne of: approve, reject.
networksarray of string
editsobject
run_atstring (date-time)
reasonstringUp to 500 characters.

Responses

StatusMeans
200as POST /api/approvals/{id}/decision
409link_used or not_pending
410link_expired
curl
curl -X POST "https://postwire.io/api/approvals/link/decision" \
  -H "Content-Type: application/json" \
  -d '{
  "t": "apr1.eyJh…",
  "action": "reject",
  "reason": "The price is 13 € since October."
}'
Node
const res = await fetch("https://postwire.io/api/approvals/link/decision", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    "t": "apr1.eyJh…",
    "action": "reject",
    "reason": "The price is 13 € since October."
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.post(
    "https://postwire.io/api/approvals/link/decision",
    json={
        "t": "apr1.eyJh…",
        "action": "reject",
        "reason": "The price is 13 € since October.",
    },
)
print(r.status_code, r.json())

Each brand's approval rules, and what the plan includes

GET/api/approvals/settings

Needs an API key: Authorization: Bearer pw_live_….

Responses

StatusMeans
200{ plan: { name, approvals, approvers (extra approvers per brand), guardrails, activity_days }, locked: { approvals?|approvers?|guardrails?: { message, plan, upgrade_url } }, sources, defaults, can_edit, brands: [{ id, name, networks, chats: { telegram, discord }, policy, saved }] }
curl
curl "https://postwire.io/api/approvals/settings" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
const res = await fetch("https://postwire.io/api/approvals/settings", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.get(
    "https://postwire.io/api/approvals/settings",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())

Set a brand's approval rules (dashboard session only)

PUT/api/brands/{id}/approvals

Plain rules checked on PostWire's server before anything is sent; no AI is involved. Only a signed-in dashboard session can change them (403 approval_requires_human otherwise), so an agent cannot switch its own approvals off.

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
id requiredpath, string (uuid)

Body (JSON)

FieldTypeDescription
enabledbooleanSwitching the rules off is allowed on every plan; on needs a paid plan (Starter and above).
sourcesobjectWhich sources' posts wait. Defaults: api, agents and automations true; dashboard false.
sources.dashboardboolean
sources.apiboolean
sources.agentsbooleanAI agents over MCP: Claude, ChatGPT, Cursor, the local postwire-mcp.
sources.automationsbooleann8n, Make, Zapier.
networksarray of stringOnly posts that include one of these networks wait (the whole post waits). Empty = every network.
only_ifobjectOnly posts that match one of these wait. All off = every post from the sources.
only_if.linkbooleanContains a link (URLs, www., bare domains like shop.com/sale).
only_if.pricebooleanMentions a currency amount ($49, 19.99 EUR, S/ 120, 300 soles).
only_if.wordsarray of stringWords or phrases (up to 200), case- and accent-insensitive, whole words; a trailing * matches the start of a word.
approversarray of string (email)Extra approvers who approve from their own link without a PostWire account. Pro: 5 per brand, Agency 25, Scale 100; not on Starter.
notifyobject
notify.emailbooleanEmail the owner (default true).
notify.telegrambooleanPost the approval link in the brand's connected Telegram (anyone in that chat can approve with it).
notify.discordboolean
remind_every_hoursintegerRemind with fresh links after this many hours, at most 3 times (0 = never). Default 12. 0 to 168.
link_hoursintegerHow long each link works. Default 72. 1 to 336.

Responses

StatusMeans
200{ ok, brand_id, policy }
400bad_request
403approval_requires_human, plan_feature or approvers_limit (with upgrade)
503governance_unavailable
curl
curl -X PUT "https://postwire.io/api/brands/b3f1c2d4-8a9e-4f6b-a1c2-3d4e5f6a7b8c/approvals" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "enabled": true,
  "sources": {
    "agents": true,
    "api": true,
    "automations": true,
    "dashboard": false
  },
  "only_if": {
    "link": true,
    "price": true,
    "words": [
      "guarant*"
    ]
  },
  "approvers": [
    "maria@client.com"
  ],
  "remind_every_hours": 12
}'
Node
const res = await fetch("https://postwire.io/api/brands/b3f1c2d4-8a9e-4f6b-a1c2-3d4e5f6a7b8c/approvals", {
  method: "PUT",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "enabled": true,
    "sources": {
      "agents": true,
      "api": true,
      "automations": true,
      "dashboard": false
    },
    "only_if": {
      "link": true,
      "price": true,
      "words": [
        "guarant*"
      ]
    },
    "approvers": [
      "maria@client.com"
    ],
    "remind_every_hours": 12
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.put(
    "https://postwire.io/api/brands/b3f1c2d4-8a9e-4f6b-a1c2-3d4e5f6a7b8c/approvals",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
    json={
        "enabled": True,
        "sources": {
            "agents": True,
            "api": True,
            "automations": True,
            "dashboard": False,
        },
        "only_if": {
            "link": True,
            "price": True,
            "words": ["guarant*"],
        },
        "approvers": ["maria@client.com"],
        "remind_every_hours": 12,
    },
)
print(r.status_code, r.json())

Limits per API key and connected AI app, with this month's use

GET/api/guardrails

Needs an API key: Authorization: Bearer pw_live_….

Responses

StatusMeans
200{ period, available, can_edit, networks, keys: [{ id, name, prefix, session, guard: { monthly_cap, networks, brand_ids, require_approval, used_this_month } | null }], apps: [{ id, name, guard }] }
curl
curl "https://postwire.io/api/guardrails" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
const res = await fetch("https://postwire.io/api/guardrails", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.get(
    "https://postwire.io/api/guardrails",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())

Set the limits of one API key or connected AI app (dashboard session only)

PUT/api/guardrails/{kind}/{id}

kind = key (an API key) or app (a connected AI app, as listed by GET /api/connected-apps). monthly_cap counts each network a post is sent or queued to through it, per UTC month; a network that fails is given back. networks / brand_ids: what it may post to (null = all). require_approval: every post from it waits for approval, whatever the brand's rules. An empty body removes the limits. Paid plans.

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
kind requiredpath, string
id requiredpath, string (uuid)

Body (JSON)

FieldTypeDescription
monthly_capinteger
networksarray of string
brand_idsarray of string (uuid)
require_approvalboolean

Responses

StatusMeans
200{ ok, subject: key|app, id, guard }
403approval_requires_human or plan_feature
404not_found
curl
curl -X PUT "https://postwire.io/api/guardrails/key/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "monthly_cap": 300,
  "networks": [
    "linkedin",
    "bluesky"
  ],
  "require_approval": false
}'
Node
const res = await fetch("https://postwire.io/api/guardrails/key/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f", {
  method: "PUT",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "monthly_cap": 300,
    "networks": [
      "linkedin",
      "bluesky"
    ],
    "require_approval": false
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.put(
    "https://postwire.io/api/guardrails/key/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
    json={
        "monthly_cap": 300,
        "networks": ["linkedin", "bluesky"],
        "require_approval": False,
    },
)
print(r.status_code, r.json())

The account's activity log

GET/api/activity

Who or what did what: post.published / post.failed / post.deferred / post.canceled (per network), x.credits_spent, approval.requested / approved / rejected / edited / withdrawn, key.created / key.revoked, settings.approvals_changed / settings.guardrails_changed. Each row says its source (actor_kind, actor_label: the dashboard, an API key by name, an AI connector by app, n8n/Make/Zapier, an approver's email) and the X credits it used.

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
sincequery, string (date-time)ISO time (the plan keeps 7, 30, 90 or 365 days)
untilquery, string (date-time)ISO time
actionquery, stringAn action (post.published) or a prefix ending in a dot (post., approval., key., settings.); x = rows that used X credits
actorquery, stringdashboard | api | agents | automations | approver | scheduler | system
brand_idquery, string (uuid)Only this brand
key_idquery, string (uuid)Only this API key or AI app (its id)
limitquery, integer1 to 500, default 100
before_idquery, integerPaging: rows older than this id

Responses

StatusMeans
200{ activity: [{ id, at, action, actor_kind, actor_label, key_id, grant_id, brand_id, platform, target_id, x_credits, summary, detail }], days, since, next_before_id? }
curl
curl "https://postwire.io/api/activity?action=approval.&actor=agents" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
const res = await fetch("https://postwire.io/api/activity?action=approval.&actor=agents", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.get(
    "https://postwire.io/api/activity?action=approval.&actor=agents",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())

The activity log as CSV (same filters, up to 5,000 rows)

GET/api/activity.csv

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
sincequery, string (date-time)ISO time (the plan keeps 7, 30, 90 or 365 days)
untilquery, string (date-time)ISO time
actionquery, stringAn action (post.published) or a prefix ending in a dot (post., approval., key., settings.); x = rows that used X credits
actorquery, stringdashboard | api | agents | automations | approver | scheduler | system
brand_idquery, string (uuid)Only this brand
key_idquery, string (uuid)Only this API key or AI app (its id)

Responses

StatusMeans
200text/csv: at, action, actor_kind, actor_label, platform, brand_id, key_id, grant_id, target_id, x_credits, summary
curl
curl "https://postwire.io/api/activity.csv?since=2026-10-01T00%3A00%3A00Z" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
const res = await fetch("https://postwire.io/api/activity.csv?since=2026-10-01T00%3A00%3A00Z", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.get(
    "https://postwire.io/api/activity.csv?since=2026-10-01T00%3A00%3A00Z",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())