Guides

Agent safety

Let an AI agent run your newsletter without handing it the keys. Agents write and prepare; anything that goes out to subscribers or can't be undone waits for your approval.

Agents propose, you approve

An AI agent can do everything up to the point of no return: read your numbers, write drafts, prepare a send. Before anything goes out to subscribers or can't be undone, it stops and asks you. You see what it wants to do, the exact input it will use and who asked, and you approve or reject it on the Approvals page. Nothing happens in between.

Three things make that safe. Each API key and each connected app has a level that says what it may do. The sends and deletes of a send with approval key are stored as requests instead of being run. And everything an agent changes is in a log you can read.

Levels

Pick a level when you make an API key, and when you connect an app such as ChatGPT or Claude (the consent page offers them). A key that already existed keeps its meaning: full is full, and a key from before levels is signups only.

LevelIn the APIWhat it can doWhat it can't
Read onlyread_onlyRead everything, including subscribers.Change anything.
Drafts onlydraftsRead everything. Create and edit draft campaigns, templates, snippets, segments and automations, and send test emails to you.Send, schedule, publish, delete, import, change settings or billing.
Send with approvalsend_with_approvalEverything drafts can, plus settings, pages, contacts and tags. Sends, schedules, deletes and large imports wait for your approval.Create keys, open billing, change prices or payments, or approve its own requests.
Full accessfullEverything.Approve requests (only you can, in the dashboard).
Signups onlysignupsAdd contacts.Read or change anything else.

What each level allows

Every action belongs to one class, and a level allows some classes. This is the table the check reads, so it is also the list of what a key can reach: the REST API answers 403 forbidden for the rest, and MCP only lists the tools a key can run.

ClassWhat it isExamplesRead onlyDrafts onlySend with approvalFull access
Read (93)Looks at data and changes nothing.get_overview, get_growth_report, compare_issues, get_site_report, and 89 moreYesYesYesYes
Write drafts (27)Creates and edits drafts: campaigns, templates, segments, automations that don't go out.create_campaign, update_campaign, duplicate_campaign, send_test_email, and 23 moreNoYesYesYes
Change settings and content (44)Changes what is live but sends nothing: settings, pages, contacts, tags.update_newsletter, update_navigation, update_link_page, unschedule_campaign, and 40 moreNoNoYesYes
Send (9)Starts or schedules emails to subscribers. Can't be undone once they go out.schedule_campaign, send_campaign, send_campaign_to_more, resend_campaign, and 5 moreNoNoAsks you firstYes
Delete or replace (21)Deletes things or changes how the newsletter sends, such as domains. Can't be undone.delete_campaign, bulk_update_campaigns, delete_page, delete_template, and 17 moreNoNoAsks you firstYes
Keys, billing and money (45)API keys, connected apps, webhooks, billing, exports of everything, prices and payments.create_newsletter, create_api_key, revoke_api_key, list_api_keys, and 41 moreNoNoNoYes

Publishing a page is more than writing a draft, so a drafts key can save a page as a draft but not publish one. A series that sends by itself counts as a send. add_contact is the one action a signups only key can run. The API's reference says on each endpoint which level it needs.

Limit a key

A key can be limited to one newsletter: it reaches nothing of the account's others, lists show only that newsletter, and anything that reaches the whole account (creating newsletters, domains, billing) is refused. A key can have a cap on calls a day, counted per UTC day with reads included, past which it answers 429 daily_cap_reached until midnight UTC. And it can expire: after its time it answers 401 unauthorized, like a revoked key. The API keys page shows each key's level, newsletter, calls today and expiry.

A drafting key for one newsletter, capped, for 30 days
curl -X POST "https://flockstack.io/api/v1/api-keys" \
  -H "Authorization: Bearer $FLOCKSTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Drafting assistant","level":"drafts","newsletter":"weekly","dailyCap":500,"expiresInDays":30}'

The approval flow

A send with approval key does not run the actions in the send and destructive classes (sending, scheduling, resuming, activating an automation, deleting, changing domains). It stores them as a request and answers at once. The owner is emailed and sees a banner in the dashboard.

The owner reads the request on the Approvals page: the subject, who it goes to and how many, the exact input. Approve runs the stored action once, as the key that asked, with that input; reject drops it and the agent reads the owner's note. Nobody decided in 24 hours: it expires and nothing runs.

The request can't be edited after it is made: a hash covers the action and its input, and the action that runs is built from what was stored. If the campaign or automation was edited since, approving asks the owner to look again before it runs. Asking twice with the same input is the same request. A key has at most 10 waiting at once.

Only a signed-in person can approve, in the dashboard. No API key, no connected app and no MCP tool can, and there is no endpoint for it: an agent can never approve its own request, even with full access.

Poll every retryAfter seconds, or subscribe to the approval.decided webhook and continue when it arrives. status is pending, approved, rejected or expired; executed is true once the approved action ran, and result is what it answered. If it ran and failed, execution.error says why. list_approvals lists a key's own requests.

REST: 202 Accepted, Retry-After: 30
POST https://flockstack.io/api/v1/campaigns/6650f1c2e9b1d4a0123456bb/send

{
  "status": "approval_required",
  "approvalId": "6650f1c2e9b1d4a0123456a1",
  "action": "send_campaign",
  "summary": "Send a campaign now (id: 6650f1c2e9b1d4a0123456bb)",
  "statusUrl": "https://flockstack.io/api/v1/approvals/6650f1c2e9b1d4a0123456a1",
  "retryAfter": 30,
  "expiresAt": "2026-10-22T09:00:00.000Z",
  "message": "A person has to approve this before it runs. It is waiting on FlockStack's Approvals page and has not been done. Do not send it again: poll get_approval_status (GET statusUrl) every retryAfter seconds or more, or wait for the approval.decided webhook, and continue once it says approved."
}
MCP: a tool result, not an error
{
  "content": [
    {
      "type": "text",
      "text": "{\"status\":\"approval_required\",\"approvalId\":\"6650f1c2e9b1d4a0123456a1\",\"action\":\"send_campaign\",\"summary\":\"Send a campaign now (id: 6650f1c2e9b1d4a0123456bb)\",\"statusUrl\":\"https://flockstack.io/api/v1/approvals/6650f1c2e9b1d4a0123456a1\",\"retryAfter\":30,\"expiresAt\":\"2026-10-22T09:00:00.000Z\",\"message\":\"A person has to approve this before it runs. It is waiting on FlockStack's Approvals page and has not been done. Do not send it again: poll get_approval_status (GET statusUrl) every retryAfter seconds or more, or wait for the approval.decided webhook, and continue once it says approved.\"}"
    }
  ],
  "structuredContent": {
    "status": "approval_required",
    "approvalId": "6650f1c2e9b1d4a0123456a1",
    "action": "send_campaign",
    "summary": "Send a campaign now (id: 6650f1c2e9b1d4a0123456bb)",
    "statusUrl": "https://flockstack.io/api/v1/approvals/6650f1c2e9b1d4a0123456a1",
    "retryAfter": 30,
    "expiresAt": "2026-10-22T09:00:00.000Z",
    "message": "A person has to approve this before it runs. It is waiting on FlockStack's Approvals page and has not been done. Do not send it again: poll get_approval_status (GET statusUrl) every retryAfter seconds or more, or wait for the approval.decided webhook, and continue once it says approved."
  }
}
Poll: GET /api/v1/approvals/{id}, or the get_approval_status tool
{
  "id": "6650f1c2e9b1d4a0123456a1",
  "status": "approved",
  "action": "send_campaign",
  "className": "send",
  "summary": "Send a campaign now (id: 6650f1c2e9b1d4a0123456bb)",
  "preview": {
    "title": "Send a campaign now",
    "lines": [
      [
        "Subject",
        "Issue 12: the long weekend"
      ],
      [
        "Goes to",
        "4,210 contacts"
      ],
      [
        "Segment",
        "No segment: the whole audience"
      ],
      [
        "When",
        "Now"
      ]
    ],
    "links": [
      {
        "label": "Open the draft",
        "href": "https://flockstack.io/campaigns/6650f1c2e9b1d4a0123456bb"
      }
    ]
  },
  "newsletterId": "6650f1c2e9b1d4a0123456aa",
  "actor": {
    "type": "key",
    "id": "6650f1c2e9b1d4a0123456b2",
    "name": "Newsletter assistant"
  },
  "via": "api",
  "reason": "key",
  "createdAt": "2026-10-21T08:00:00.000Z",
  "expiresAt": "2026-10-22T09:00:00.000Z",
  "decidedAt": "2026-10-21T08:12:00.000Z",
  "note": "Looks good, go ahead",
  "executed": true,
  "execution": {
    "state": "done",
    "error": null
  },
  "result": {
    "id": "6650f1c2e9b1d4a0123456bb",
    "status": "sending"
  },
  "statusUrl": "https://flockstack.io/api/v1/approvals/6650f1c2e9b1d4a0123456a1",
  "retryAfter": null
}

The approval.decided webhook

The owner approved or rejected something an API key or connected app asked to do, or it expired after 24 hours. status says which. executed is true once an approved action has run; if it failed, error says why. note is what the owner wrote. Get the result with get_approval_status.

approval.decided
{
  "type": "approval.decided",
  "timestamp": "2026-10-21T08:12:00.000Z",
  "data": {
    "approval": {
      "id": "6650f1c2e9b1d4a0123456a1",
      "newsletterId": "6650f1c2e9b1d4a0123456aa",
      "action": "send_campaign",
      "summary": "Send a campaign now (id: 6650f1c2e9b1d4a0123456bb)",
      "status": "approved",
      "note": "Looks good",
      "decidedAt": "2026-10-21T09:00:00.000Z",
      "executed": true,
      "error": null,
      "actor": {
        "type": "key",
        "id": "6650f1c2e9b1d4a0123456b2",
        "name": "Newsletter assistant"
      }
    }
  }
}

Ask me before an agent sends

On the Approvals page each newsletter has a switch. With it on, anything an API key or connected app does in the send and delete classes waits for you, whatever the key's level, full access included. Only you can switch it, in the dashboard.

A send with approval key also sends large changes of contacts to you: an import of more than 25 contacts in one call, or more than 100 contacts changed (imported or unsubscribed) in a day, because a loop of small changes is the same as one big one.

An agent can only change a campaign while it is a draft once it asks first: a scheduled campaign was approved as it was, so a send with approval key must unschedule it, edit it and ask again. The same goes for an active automation.

Agent activity

Every call from an API key or a connected app that changes something or shows contact data is in the log: the action, which key or app, which newsletter, the time, how it ended (ok, failed, denied or waiting for approval), a short summary and the request's ID (the X-Request-Id header of the REST answer). Every step of an approval is there too. Calls that were refused are recorded, so you see what an agent tried.

The log keeps ids, option names and counts, never the text of an email, a subject, a body or an address. It is kept 90 days, then deleted. Calls of every kind, reads included, are also counted per key and per day. See it on the Agent activity page, with filters for the key or app, the action, the newsletter and the result, and a page for each key, or read it with list_agent_activity: a key or app reads its own entries.

An entry of list_agent_activity
{
  "id": "6650f1c2e9b1d4a0123456c3",
  "at": "2026-10-21T08:00:03.000Z",
  "kind": "call",
  "actor": {
    "type": "key",
    "id": "6650f1c2e9b1d4a0123456b2",
    "name": "Newsletter assistant"
  },
  "via": "api",
  "action": "schedule_campaign",
  "className": "send",
  "newsletterId": "6650f1c2e9b1d4a0123456aa",
  "status": "approval_required",
  "errorCode": null,
  "summary": "Schedule a campaign (id: 6650f1c2e9b1d4a0123456bb)",
  "requestId": "req_8f2c61d0a94b37e1c5d2",
  "approvalId": "6650f1c2e9b1d4a0123456a1"
}

Connected apps

ChatGPT, Claude and Grok Bot connect with OAuth. The consent page lists the levels up to what the app asked for, in plain words, and starts on send with approval when the app asked for full access. What you pick is what the app may do, and the API keys page lists it for each app. An app can ask for less with the OAuth scopes flockstack:read, flockstack:drafts, flockstack:send (send with approval) and flockstack:full, and never gets more than it asked for. Apps connected before levels existed keep full access until you disconnect them.

Setups that work

Read only analyst. A key with level read_only, limited to the newsletter it reports on. It can read campaigns, reports and subscribers, and change nothing. Give it a daily cap and an expiry if it is a one-off analysis.

Drafting assistant. A key with level drafts. It writes campaigns, templates, segments and automations and sends test emails to you, and you send them yourself from the dashboard. It can't schedule, publish, import or delete, so there is nothing to approve.

Send with approval operator. A key with level send_with_approval, or ChatGPT or Claude connected on that level. It drafts, edits settings and contacts, then asks you to send or schedule. You read the request on the Approvals page, or let your own automation read approval.decided. This is the setup to start with for an agent that runs your newsletter.

Full access is for your own scripts on a server you control. If an agent has it, turn on Ask me before an agent sends for the newsletter so its sends still wait for you.

What no key can do

Deciding an approval and turning on Ask me before an agent sends are done by a signed-in person in the dashboard only. They are not in the REST API, the OpenAPI document or the MCP tools. A key below full access also can't create keys, open billing, change prices or payments, manage webhooks or export the account.