# ApresMail API and MCP reference The ApresMail REST API and MCP server share the actions listed below. Both enforce account limits, credential permissions and sending checks. Audience v2 has one Subscriber profile per address and independent NewsletterSubscription consent for each newsletter. Groups belong to a newsletter. A legacy contact ID is a subscription ID on v2; legacy recipient actions return conflict there. Adding is organizational by default. On ready v2 accounts, POST /contacts with subscribe:true records explicit owner authorization for an unrestricted newsletter subscription without recipient confirmation. Check subscriptionMode: owner-authorized acknowledges the mode while status reports any preserved restriction. It never restores withdrawals or starts welcome/sequence mail. Imports do not imply consent. The CSV preview changes nothing, and the existing CSV importer is not the later durable CSV importer or a qualified MailerLite Classic migration. ## Authentication Create an API key on the dashboard's API keys page and send it as `Authorization: Bearer ` to http://localhost:3000/api/v1 or to the MCP server at http://localhost:3000/mcp. - New scoped keys select content:read, content:write, messages:send, audience:read, audience:write and settings:manage independently. The dashboard defaults to content read/write and a 30-day lifetime; 7 or 90 days are also available. Scoped keys cannot manage credentials. Legacy full keys keep their existing access. - Signups only keys can only add contacts (`POST /api/v1/contacts`, MCP tool `add_contact`), for signup forms in website code. That endpoint also answers browsers (CORS). - Every key gets 300 requests a minute across REST and MCP, and some actions have their own limit, for each key and for the account: its keys, connected apps and dashboard share the account's, so more keys don't raise it. Over a limit, the answer is 429 rate_limited. - Keys are stored hashed and shown once. Expired or revoked keys answer 401 unauthorized. An account has up to 20 active keys. - The MCP server also takes OAuth access tokens, for apps such as ChatGPT, Claude and Grok Bot: the user signs in to ApresMail and selects requested permissions. Read the supported scope identifiers from the server’s OAuth discovery metadata. Existing full grants continue to work. New connections request individual permissions. Discovery starts at http://localhost:3000/.well-known/oauth-protected-resource/mcp. ## Errors Every error answers `{ "error": { "code": "…", "message": "…" } }`. MCP tools answer the same object in an error result. Codes never change meaning: - `unauthorized` (401): The API key is missing, malformed, unknown or revoked. - `forbidden` (403): The key's level doesn't allow this action: a signups only key can only add contacts. - `not_found` (404): The newsletter, campaign, contact, segment, domain or key isn't in this account. - `validation_failed` (400): The input doesn't match the schema or breaks a rule. The message names the field. - `conflict` (409): The resource's state doesn't allow it, e.g. sending a campaign that is already sending or sent. Retrying won't help. - `rate_limited` (429): Too many requests with this key, or too many of one action for the account, whose keys, apps and dashboard share its limits. Wait and retry. - `subscription_required` (402): The account needs an active Apresly subscription. - `plan_limit_emails` (402): The send needs more emails than the account has left this month. - `plan_limit_subscribers` (402): The account has more active subscribers than its assigned limit allows. - `account_blocked` (403): The account is blocked after spam signals (bounce or complaint rates, Gmail's spam rate in Google Postmaster Tools, a spam trap, the email provider) or by an admin. Nothing is sent, the tracked links in its emails stop redirecting, and only reads, pausing, unscheduling, revoking access work until an admin unblocks it. - `sender_not_allowed` (422): The from address is neither the newsletter's shared address nor on a verified domain of this account. - `send_check_failed` (422): A pre-send check failed: no postal address, subject or content, the email is over 100 KB, or its segment was deleted. - `no_recipients` (422): Nobody is left to send to. - `sending_disabled` (503): Sending is turned off on this server. - `unavailable` (503): A service the action needs isn't configured or didn't answer. Retry later. - `internal_error` (500): Something failed on ApresMail's side. - `SMS_AMOUNT_INVALID` (400): SMS operation could not complete (SMS_AMOUNT_INVALID). - `SMS_REQUEST_INVALID` (400): SMS operation could not complete (SMS_REQUEST_INVALID). - `SMS_SENDER_INVALID` (400): SMS operation could not complete (SMS_SENDER_INVALID). - `SMS_PHONE_INVALID` (400): SMS operation could not complete (SMS_PHONE_INVALID). - `SMS_CONTENT_INVALID` (400): SMS operation could not complete (SMS_CONTENT_INVALID). - `SMS_CONTENT_TOO_LONG` (400): SMS operation could not complete (SMS_CONTENT_TOO_LONG). - `SMS_CODE_INVALID` (400): SMS operation could not complete (SMS_CODE_INVALID). - `SMS_BUDGET_INSUFFICIENT` (409): SMS operation could not complete (SMS_BUDGET_INSUFFICIENT). - `SMS_IDEMPOTENCY_CONFLICT` (409): SMS operation could not complete (SMS_IDEMPOTENCY_CONFLICT). - `SMS_VERSION_CONFLICT` (409): SMS operation could not complete (SMS_VERSION_CONFLICT). - `SMS_LIMIT_BELOW_USAGE` (409): SMS operation could not complete (SMS_LIMIT_BELOW_USAGE). - `SMS_QUALIFICATION_MISMATCH` (409): SMS operation could not complete (SMS_QUALIFICATION_MISMATCH). - `SMS_OUTSIDE_WINDOW` (409): SMS operation could not complete (SMS_OUTSIDE_WINDOW). - `SMS_AUDIENCE_LIMIT` (409): SMS operation could not complete (SMS_AUDIENCE_LIMIT). - `SMS_PHONE_UNAVAILABLE` (409): SMS operation could not complete (SMS_PHONE_UNAVAILABLE). - `SMS_OPERATION_EXPIRED` (410): SMS operation could not complete (SMS_OPERATION_EXPIRED). - `SMS_CODE_EXPIRED` (410): SMS operation could not complete (SMS_CODE_EXPIRED). - `SMS_RATE_LIMITED` (429): SMS operation could not complete (SMS_RATE_LIMITED). - `SMS_CONFIGURATION_UNAVAILABLE` (503): SMS operation could not complete (SMS_CONFIGURATION_UNAVAILABLE). - `SMS_DISABLED` (503): SMS operation could not complete (SMS_DISABLED). - `SMS_ACCOUNT_UNAVAILABLE` (503): SMS operation could not complete (SMS_ACCOUNT_UNAVAILABLE). - `SMS_NOT_QUALIFIED` (503): SMS operation could not complete (SMS_NOT_QUALIFIED). - `SMS_RATE_CARD_INVALID` (503): SMS operation could not complete (SMS_RATE_CARD_INVALID). - `SMS_SENDER_NOT_READY` (503): SMS operation could not complete (SMS_SENDER_NOT_READY). - `SMS_SCHEMA_UNAVAILABLE` (503): SMS operation could not complete (SMS_SCHEMA_UNAVAILABLE). - `SMS_COST_CHANGED` (409): SMS operation could not complete (SMS_COST_CHANGED). - `SMS_RATE_EXPIRED` (409): SMS operation could not complete (SMS_RATE_EXPIRED). - `SMS_BUILD_EXPIRED` (409): SMS operation could not complete (SMS_BUILD_EXPIRED). - `SMS_SOURCE_UNAVAILABLE` (503): SMS operation could not complete (SMS_SOURCE_UNAVAILABLE). Sending a campaign that is already sending or sent answers `conflict`, so retrying a send never sends twice. ## MCP The MCP server speaks Streamable HTTP at http://localhost:3000/mcp, without sessions. Each endpoint below is one tool with the same name, input and output; its annotations tell clients whether it's read-only, can't be undone, is safe to repeat or reaches outside ApresMail. The send tools can't be undone: ask the user before calling them. ### ChatGPT 1. Go to Plugins (chatgpt.com/plugins), click + and pick Add custom MCP server. 2. Name it ApresMail, enter http://localhost:3000/mcp as the server URL and pick OAuth, with no client ID or secret. If ChatGPT asks how to register, pick Client ID Metadata Document. 3. Check I understand and want to continue, then click Create as a plugin. 4. Install the plugin, sign in to ApresMail when ChatGPT asks, choose permissions and click Allow. 5. In a chat, type @ and pick ApresMail. Custom MCP servers are on ChatGPT on the web, for Business, Enterprise and Edu workspaces: on Business, admins and owners add them; on Enterprise and Edu, so can the members an admin allows. On Pro, ChatGPT only uses the read-only tools for now. ### Claude 1. Go to Customize › Connectors and click Add custom connector. 2. Name it ApresMail and enter http://localhost:3000/mcp. If Claude asks how it identifies itself, keep Use Claude's published identity. Click Add. 3. Click Connect, sign in to ApresMail, choose permissions and click Allow. 4. The connector works in claude.ai, Claude Desktop and the mobile apps. Turn it on or off in a chat from + › Connectors. On Team and Enterprise plans, an owner adds it once in Organization settings › Connectors › Add › Custom › Web, then each member clicks Connect under Customize › Connectors. The Free plan has one custom connector. ### Grok Bot 1. In a chat, ask Grok Bot to add the custom MCP server http://localhost:3000/mcp. 2. Sign in to ApresMail when it asks, choose permissions and click Allow. ### Claude Code Terminal: ``` claude mcp add --transport http --scope user apresmail http://localhost:3000/mcp \ --header "Authorization: Bearer $APRESMAIL_API_KEY" ``` The shell puts the key in Claude Code's settings when the command runs. --scope user makes the server available in every project; run /mcp in Claude Code to check it's connected. ### Codex ~/.codex/config.toml: ``` [mcp_servers.apresmail] url = "http://localhost:3000/mcp" bearer_token_env_var = "APRESMAIL_API_KEY" ``` Codex reads the key from APRESMAIL_API_KEY each time it starts, so export it in your shell profile. The same server from the terminal: codex mcp add apresmail --url http://localhost:3000/mcp --bearer-token-env-var APRESMAIL_API_KEY ### Cursor ~/.cursor/mcp.json: ``` { "mcpServers": { "apresmail": { "url": "http://localhost:3000/mcp", "headers": { "Authorization": "Bearer ${env:APRESMAIL_API_KEY}" } } } } ``` Cursor reads APRESMAIL_API_KEY from the environment it was opened in. If it isn't set there, put the key itself after Bearer, and keep the file out of version control. ## Endpoints ### Campaigns Drafts, test emails, scheduling, sending, sending in steps and reports. #### List countdown campaigns `GET /api/v1/native-campaigns` · MCP tool `list_native_campaigns` · content:read · read-only, safe to repeat, reaches outside ApresMail Lists owned published Apresly campaigns for native email countdown blocks. No recipient is registered. No parameters. ```bash curl "http://localhost:3000/api/v1/native-campaigns" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Validate a countdown destination `POST /api/v1/native-campaigns/validate` · MCP tool `validate_native_countdown` · content:read · read-only, safe to repeat, reaches outside ApresMail Checks an owned campaign and optional published checkout destination without registering a recipient. - `capability` (body, string, required): Owned campaign capability returned by list_native_campaigns - `checkoutPublicId` (body, string | null, required): Published Commerce checkout UUID, or null for an image without a link ```bash curl -X POST "http://localhost:3000/api/v1/native-campaigns/validate" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"capability":"…","checkoutPublicId":"…"}' ``` #### List campaigns `GET /api/v1/campaigns` · MCP tool `list_campaigns` · content:read · read-only, safe to repeat Lists campaigns, newest activity first: sent issues by send date, scheduled ones by send time, drafts by last edit. Email content is left out; get_campaign has it. Filter channel sms to list pilot campaigns without an email plan. - `newsletter` (query, string): Only this newsletter's: its ID or slug. Default: every newsletter. - `channel` (query, "email" | "sms"): Filter by channel; channel sms is available without an email plan - `status` (query, "draft" | "scheduled" | "sending" | "paused" | "sent"): Only campaigns with this status - `limit` (query, integer): How many, 50 by default, 200 at most ```bash curl "http://localhost:3000/api/v1/campaigns?status=sent&limit=10" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Get a campaign `GET /api/v1/campaigns/{id}` · MCP tool `get_campaign` · content:read · read-only, safe to repeat Gets a campaign with its content, stats and, for drafts, what would block sending it now. - `id` (path, string, required): The campaign's ID ```bash curl "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Get a campaign's report `GET /api/v1/campaigns/{id}/report` · MCP tool `get_campaign_report` · content:read · read-only, safe to repeat Delivery, open, click, bounce, complaint and unsubscribe rates of a sent campaign, its list cleanup emails, results by inbox provider, and remaining contacts. SMS returns delivery status counts and reserved, committed and released USD caps without email engagement metrics. - `id` (path, string, required): The campaign's ID ```bash curl "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/report" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Create a draft campaign `POST /api/v1/campaigns` · MCP tool `create_campaign` · content:write Creates an email draft by default, or an SMS draft with channel sms, name, sms.senderId, sms.template and a stable requestId. SMS rejects email content, archive and segments; at most 100 newsletter groups. Prepare and approve a review before sending. - `channel` (body, "email" | "sms"): Campaign channel; omitted means email. SMS uses name, sms and requestId instead of email content. - `name` (body, string): Private SMS campaign name, at most 200 characters - `sms` (body, object): SMS sender and plain text; only {{first_name}} personalization is supported - `sms.senderId` (body, string, required) - `sms.template` (body, string, required) - `requestId` (body, string): Required for SMS: stable UUID reused with identical input after a timeout - `newsletter` (body, string): The newsletter's ID or slug. Default: the account's first newsletter, the one it has had longest. - `groupIds` (body, string[]): SMS: union of up to 100 newsletter groups; empty selects the newsletter - `subject` (body, string): Subject line, 200 characters at most - `previewText` (body, string): Text inbox apps show after the subject, 300 characters at most - `format` (body, "markdown" | "rich" | "html"): markdown (the default when content is given), rich or html, as in update_campaign - `content` (body, string): The body in that format, 500 KB at most - `archiveSlug` (body, string): The issue's page path once sent. Default: made from the subject; a taken slug gets a number. - `segment` (body, string | null): A segment's ID (list_segments): only the active contacts it picks when the campaign goes out get it. null: every active contact. - `audienceRules` (body, object | null): Additional campaign-specific OR sets of AND conditions; null removes these rules. Ready v2 email audiences only. ```bash curl -X POST "http://localhost:3000/api/v1/campaigns" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject":"This week","previewText":"Three links worth your time","content":"# This week\n\nThree links worth your time."}' ``` #### Update a draft campaign `PATCH /api/v1/campaigns/{id}` · MCP tool `update_campaign` · content:write · safe to repeat Changes an unapproved draft using its expectedRevision. Content, segment and organizational group selection share one revision. Stale edits conflict without overwriting text. Scheduled or approved campaigns cannot be edited. - `channel` (body, "email" | "sms"): Campaign channel; omitted means email. SMS uses name, sms and requestId instead of email content. - `name` (body, string): Private SMS campaign name, at most 200 characters - `sms` (body, object): SMS sender and plain text; only {{first_name}} personalization is supported - `sms.senderId` (body, string, required) - `sms.template` (body, string, required) - `requestId` (body, string): Required for SMS: stable UUID reused with identical input after a timeout - `id` (path, string, required): The campaign's ID - `expectedRevision` (body, integer, required): The revision last read by this editor; stale edits return conflict - `groupIds` (body, string[]): SMS: union of up to 100 newsletter groups; empty selects the newsletter - `audienceLimit` (body, integer | null): Maximum reviewed recipients, 1 to 25000; null restores the default maximum of 25000 - `subject` (body, string): Subject line, 200 characters at most - `previewText` (body, string): Text inbox apps show after the subject, 300 characters at most - `format` (body, "markdown" | "rich" | "html"): With content: the format it's in. Default: the campaign's current format. - `content` (body, string): The body in that format, 500 KB at most - `segment` (body, string | null): A segment's ID (list_segments): only the active contacts it picks when the campaign goes out get it. null: every active contact. - `audienceRules` (body, object | null): Additional campaign-specific OR sets of AND conditions; null removes these rules. Ready v2 email audiences only. ```bash curl -X PATCH "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"expectedRevision":"…","subject":"This week, and one more link"}' ``` #### Delete a draft campaign `DELETE /api/v1/campaigns/{id}` · MCP tool `delete_campaign` · content:write · can't be undone, safe to repeat Deletes a draft or scheduled campaign: it leaves every list and a scheduled send never goes out. Sent campaigns can't be deleted. - `id` (path, string, required): The campaign's ID ```bash curl -X DELETE "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Duplicate a campaign `POST /api/v1/campaigns/{id}/duplicate` · MCP tool `duplicate_campaign` · content:write Copies a campaign's subject, preview text and content into a new draft. - `id` (path, string, required): The campaign's ID ```bash curl -X POST "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/duplicate" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Send a test email `POST /api/v1/campaigns/{id}/test` · MCP tool `send_test_email` · messages:send · reaches outside ApresMail Sends the saved campaign to the specified test address, or the signed-in user/account owner when omitted, with [Test] in the subject. It counts toward the account's email allowance. - `id` (path, string, required): The campaign's ID - `to` (body, string): The test email destination; omitted uses the signed-in user or account owner ```bash curl -X POST "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/test" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Schedule a reviewed campaign `POST /api/v1/campaigns/{id}/schedule` · MCP tool `schedule_campaign` · messages:send · can't be undone, safe to repeat, reaches outside ApresMail Approves a retained review for a future schedule. Sending can't be undone: messages can't be recalled once they go out. Ask the user to confirm first. Requires review ID, revision, digest and stable request UUID; no live audience is selected here. - `id` (path, string, required): The campaign's ID - `expectedCapMicrousd` (body, string): SMS only: exact maximum micro-USD cost returned by the ready review - `reviewId` (body, string): Retained ready review ID belonging to this campaign - `expectedRevision` (body, integer): Draft revision captured by this review - `snapshotDigest` (body, string): Exact snapshot digest returned by the ready review - `requestId` (body, string): Stable approval UUID; reuse the same value and input after a timeout - `validityHours` (body, integer): Execution validity after its due time, 1 to 72 hours; default 24 - `audienceLimit` (body, integer): Maximum contacts to include in this preview - `segment` (body, string | null): A segment's ID (list_segments): only the active contacts it picks when the campaign goes out get it. null: every active contact. - `scheduledAt` (body, string): Scheduled ISO instant - `timezone` (body, string): IANA timezone - `localDateTime` (body, string): Local wall-clock time ```bash curl -X POST "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/schedule" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Cancel a reviewed schedule `POST /api/v1/campaigns/{id}/unschedule` · MCP tool `unschedule_campaign` · content:write · safe to repeat Changes only the specified approved execution. Requires its current lifecycle revision and a stable request UUID. Unknown deliveries are never retried. - `id` (path, string, required): The campaign's ID - `reviewId` (body, string, required): The review's ID - `requestId` (body, string, required): Stable UUID for this lifecycle request; reuse after a timeout - `expectedLifecycleRevision` (body, integer, required): Lifecycle revision returned by the latest review read - `expectedUnknownGeneration` (body, integer): Unknown-outcome generation explicitly acknowledged on resume; zero for a clean execution ```bash curl -X POST "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/unschedule" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"reviewId":"…","requestId":"…","expectedLifecycleRevision":"…"}' ``` #### Send a reviewed campaign now `POST /api/v1/campaigns/{id}/send` · MCP tool `send_campaign` · messages:send · can't be undone, safe to repeat, reaches outside ApresMail Approves exactly the retained review for sending now. Sending can't be undone: messages can't be recalled once they go out. Ask the user to confirm first. Requires review ID, revision, digest and stable request UUID; no live audience is selected here. - `id` (path, string, required): The campaign's ID - `expectedCapMicrousd` (body, string): SMS only: exact maximum micro-USD cost returned by the ready review - `reviewId` (body, string): Retained ready review ID belonging to this campaign - `expectedRevision` (body, integer): Draft revision captured by this review - `snapshotDigest` (body, string): Exact snapshot digest returned by the ready review - `requestId` (body, string): Stable approval UUID; reuse the same value and input after a timeout - `validityHours` (body, integer): Execution validity after its due time, 1 to 72 hours; default 24 - `audienceLimit` (body, integer): Maximum contacts to include in this preview - `segment` (body, string | null): A segment's ID (list_segments): only the active contacts it picks when the campaign goes out get it. null: every active contact. ```bash curl -X POST "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/send" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Send a campaign to more contacts `POST /api/v1/campaigns/{id}/send-more` · MCP tool `send_campaign_to_more` · messages:send · can't be undone, reaches outside ApresMail Disabled: duplicate the campaign and prepare a new review for another intentional send. Sending can't be undone: messages can't be recalled once they go out. Ask the user to confirm first. - `id` (path, string, required): The campaign's ID - `additional` (body, integer): Legacy input; this operation always conflicts ```bash curl -X POST "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/send-more" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Pause a reviewed campaign `POST /api/v1/campaigns/{id}/pause` · MCP tool `pause_campaign` · content:write · safe to repeat Changes only the specified approved execution. Requires its current lifecycle revision and a stable request UUID. Unknown deliveries are never retried. - `id` (path, string, required): The campaign's ID - `reviewId` (body, string, required): The review's ID - `requestId` (body, string, required): Stable UUID for this lifecycle request; reuse after a timeout - `expectedLifecycleRevision` (body, integer, required): Lifecycle revision returned by the latest review read - `expectedUnknownGeneration` (body, integer): Unknown-outcome generation explicitly acknowledged on resume; zero for a clean execution ```bash curl -X POST "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/pause" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"reviewId":"…","requestId":"…","expectedLifecycleRevision":"…"}' ``` #### Resume a reviewed campaign `POST /api/v1/campaigns/{id}/resume` · MCP tool `resume_campaign` · messages:send · can't be undone, safe to repeat, reaches outside ApresMail Changes only the specified approved execution. Sending can't be undone: messages can't be recalled once they go out. Ask the user to confirm first. Requires its current lifecycle revision and a stable request UUID. Unknown deliveries are never retried. - `id` (path, string, required): The campaign's ID - `reviewId` (body, string, required): The review's ID - `requestId` (body, string, required): Stable UUID for this lifecycle request; reuse after a timeout - `expectedLifecycleRevision` (body, integer, required): Lifecycle revision returned by the latest review read - `expectedUnknownGeneration` (body, integer): Unknown-outcome generation explicitly acknowledged on resume; zero for a clean execution ```bash curl -X POST "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/resume" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"reviewId":"…","requestId":"…","expectedLifecycleRevision":"…"}' ``` #### Cancel an approved campaign `POST /api/v1/campaigns/{id}/cancel` · MCP tool `cancel_campaign` · content:write · can't be undone, safe to repeat Permanently cancels this approved execution and suppresses unstarted queued/held work in bounded batches. Provider-started and unknown deliveries remain unchanged. - `id` (path, string, required): The campaign's ID - `reviewId` (body, string, required): The review's ID - `requestId` (body, string, required): Stable UUID for this lifecycle request; reuse after a timeout - `expectedLifecycleRevision` (body, integer, required): Lifecycle revision returned by the latest review read - `expectedUnknownGeneration` (body, integer): Unknown-outcome generation explicitly acknowledged on resume; zero for a clean execution ```bash curl -X POST "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/cancel" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"reviewId":"…","requestId":"…","expectedLifecycleRevision":"…"}' ``` #### Update a campaign's archive page `PATCH /api/v1/campaigns/{id}/archive` · MCP tool `update_campaign_archive` · settings:manage · safe to repeat Changes the path of a campaign's archive page (/p/), or pins a sent issue to the top of the archive. Works on sent campaigns too. - `id` (path, string, required): The campaign's ID - `expectedRevision` (body, integer): Required when changing the archive slug; the revision last read by this editor - `slug` (body, string): Lowercase letters, digits and dashes. Empty removes it from a draft. - `pinned` (body, boolean): Pinned issues lead the archive ```bash curl -X PATCH "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/archive" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"pinned":true}' ``` #### Preview the list cleanup of a send `GET /api/v1/campaigns/{id}/cleanup-preview` · MCP tool `preview_campaign_cleanup` · content:read · read-only, safe to repeat Counts what the newsletter's list cleanup would do if the campaign went out now: issues sent, last-chance emails instead, contacts still waiting to answer one, and contacts made dormant. Reads the whole list, so it can take a few seconds. - `id` (path, string, required): The campaign's ID - `audienceLimit` (query, integer): Count for a send capped at this many contacts - `includeRanks` (query, boolean): Also return, for each cold contact, how many issue recipients come before it, to recount any limit without asking again ```bash curl "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/cleanup-preview" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Preview campaign audience `GET /api/v1/campaigns/{id}/audience-preview` · MCP tool `preview_campaign_audience` · content:read + audience:read · read-only, safe to repeat Counts current eligible recipients in a saved v2 email draft using its groups, segment and additional conditions. The dashboard can preview validated unsaved selection changes without writing them. Read-only; no review, schedule or email is created. Counts are a live observation and can change before sending. - `id` (path, string, required): The campaign's ID - `expectedRevision` (query, integer, required): The revision last read by this editor; stale edits return conflict ```bash curl "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/audience-preview" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Prepare a campaign review `POST /api/v1/campaigns/{id}/reviews` · MCP tool `prepare_campaign_review` · content:write · safe to repeat Freezes this draft's content and audience selection for a bounded background review. Does not approve or send messages. The same request ID and input returns its original review, including a terminal result; different input conflicts. - `id` (path, string, required): Campaign ID in this account - `expectedRevision` (body, integer, required): Draft revision that the operator reviewed - `requestId` (body, string, required): Stable UUID for this preparation request; reuse it after a timeout ```bash curl -X POST "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/reviews" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"expectedRevision":"…","requestId":"…"}' ``` #### Get a campaign review `GET /api/v1/campaigns/{id}/reviews/{reviewId}` · MCP tool `get_campaign_review` · content:read · read-only, safe to repeat Reads an explicit summary of retained campaign review content, selection, counters and lifecycle. Preparation diagnostics may be stale and do not grant permission to send. Historical reads remain available while review preparation is disabled. - `id` (path, string, required): Campaign ID in this account - `reviewId` (path, string, required): Review ID belonging to this campaign and account ```bash curl "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/reviews/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### List campaign review recipients `GET /api/v1/campaigns/{id}/reviews/{reviewId}/recipients` · MCP tool `list_campaign_review_recipients` · audience:read · read-only, safe to repeat Reads a bounded retained audience snapshot, not current sending permission. Building reviews conflict. Erased or missing canonical recipients are omitted immediately; an empty page may still have nextCursor. Historical reads remain available while preparation is disabled. - `id` (path, string, required): Campaign ID in this account - `reviewId` (path, string, required): Review ID belonging to this campaign and account - `cursor` (query, string): Previous page nextCursor for this exact review - `limit` (query, integer): Raw snapshot rows per page, 1 to 100; defaults to 50 ```bash curl "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/reviews/6650f1c2e9b1d4a012345678/recipients" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Preview frozen campaign content `GET /api/v1/campaigns/{id}/reviews/{reviewId}/preview` · MCP tool `get_campaign_review_preview` · content:read · read-only, safe to repeat Reads the retained email preview or a synthetic SMS example including its complete unsubscribe-link length. Links are disabled and no tracking or unsubscribe credentials are generated. Never renders mutable newsletter defaults. - `id` (path, string, required): Campaign ID in this account - `reviewId` (path, string, required): Review ID belonging to this campaign and account ```bash curl "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/reviews/6650f1c2e9b1d4a012345678/preview" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Approve a campaign review `POST /api/v1/campaigns/{id}/reviews/{reviewId}/approve` · MCP tool `approve_campaign_review` · messages:send · can't be undone, safe to repeat, reaches outside ApresMail Approves exactly the retained content and audience snapshot. Sending can't be undone. Reuse the same request UUID and input after a timeout; changed input conflicts. - `expectedCapMicrousd` (body, string): SMS only: exact maximum micro-USD cost returned by the ready review - `reviewId` (path, string, required): Retained ready review ID belonging to this campaign - `expectedRevision` (body, integer, required): Draft revision captured by this review - `snapshotDigest` (body, string, required): Exact snapshot digest returned by the ready review - `requestId` (body, string, required): Stable approval UUID; reuse the same value and input after a timeout - `validityHours` (body, integer): Execution validity after its due time, 1 to 72 hours; default 24 - `mode` (body, "now" | "scheduled", required): Send now or approve a future schedule - `scheduledAt` (body, string): Scheduled ISO instant; required only for scheduled mode - `timezone` (body, string): IANA timezone of the schedule - `localDateTime` (body, string): Local wall-clock time represented by scheduledAt - `id` (path, string, required): Campaign ID in this account ```bash curl -X POST "http://localhost:3000/api/v1/campaigns/6650f1c2e9b1d4a012345678/reviews/6650f1c2e9b1d4a012345678/approve" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"expectedRevision":"…","snapshotDigest":"…","requestId":"…","mode":"…"}' ``` ### Overview Subscribers, email results and sends per hour, as on the dashboard's first page. #### Get the overview `GET /api/v1/overview` · MCP tool `get_overview` · content:read · read-only, safe to repeat The dashboard's overview for one newsletter: subscribers each day of the last 30 days, email results of the last 30 days against the 30 before, emails sent per hour today and yesterday, and the plan's usage this month, which the account's newsletters share. - `newsletter` (query, string): The newsletter's ID or slug. Default: the account's first newsletter, the one it has had longest. - `timeZone` (query, string): IANA time zone for days and hours, e.g. Europe/Paris. Default: UTC. ```bash curl "http://localhost:3000/api/v1/overview?timeZone=Europe%2FParis" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` ### Newsletters Sender, postal address, welcome and confirmation emails, list cleanup, archive and theme. #### List newsletters `GET /api/v1/newsletters` · MCP tool `list_newsletters` · content:read · read-only, safe to repeat Lists the account's newsletters, 3 at most, first created first, with their settings and active contacts. Actions that take a newsletter use the first one when it's left out. No parameters. ```bash curl "http://localhost:3000/api/v1/newsletters" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Get a newsletter `GET /api/v1/newsletters/{id}` · MCP tool `get_newsletter` · content:read · read-only, safe to repeat Gets a newsletter's settings: sender, postal address, welcome and confirmation emails, list cleanup, archive and theme. - `id` (path, string, required): The newsletter's ID or slug ```bash curl "http://localhost:3000/api/v1/newsletters/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Create a newsletter `POST /api/v1/newsletters` · MCP tool `create_newsletter` · settings:manage + messages:send Creates a newsletter. An account has up to 3, sharing its account's assigned email and subscriber limits; one more answers conflict. Without a fromEmail it sends from the shared address hello@., which ApresMail sets up in a few minutes. List cleanup is on unless cleanup.enabled is false. Without a postalAddress, its emails print the server's default if it has one: the administrator-configured address. - `name` (body, string, required): The newsletter's name, shown in emails and on the archive - `slug` (body, string): Lowercase letters, digits and dashes. Default: made from the name, numbered when another newsletter has it. - `imageUrl` (body, string): Square logo, 128×128 or larger: an https:// link - `fromName` (body, string): Name in the From field - `fromEmail` (body, string): The shared address (hello@., set automatically) or an address on a domain added to this account - `replyTo` (body, string): Where replies go. Empty: replies to the shared address go to the account owner. - `postalAddress` (body, string): Printed in the footer of every email. Never set, it uses the configured default; saved empty, there is none and sending waits for one. - `archiveEnabled` (body, boolean): A public page for each sent issue, with a signup form. On for new newsletters: the website goes live after the first issue. - `description` (body, string): One sentence under the name on the archive, 200 characters at most - `author` (body, object): The archive's author page; the author's name is fromName - `author.bio` (body, string): Under the author's name on the archive, 160 characters at most - `author.imageUrl` (body, string): Author photo: an https:// link. The newsletter image otherwise. - `socialLinks` (body, string[]): https:// links shown on the archive, 8 at most - `requireConfirmation` (body, boolean): Confirmed opt-in: new signups get an email with a button and only join once they click it. On for new newsletters. New subscribers get the confirmation email or the welcome email: turning it on while the welcome email is on needs welcome.enabled false in the same request. - `cleanup` (body, object): List cleanup at each send: contacts who ignored several issues get a last-chance email instead, and become dormant if they don't answer - `cleanup.enabled` (body, boolean): On for new newsletters - `cleanup.issues` (body, number | null): Ignored issues in a row before the last-chance email, 2 to 100. Default 6. - `cleanup.days` (body, number | null): Days without an open, click or signup, 30 to 3650. Default 180. - `welcome` (body, object): Email sent to each new subscriber, while signups don't need confirming: turning it on while requireConfirmation is on needs requireConfirmation false in the same request. - `welcome.enabled` (body, boolean) - `welcome.subject` (body, string) - `welcome.previewText` (body, string) - `welcome.markdown` (body, string): Markdown - `confirmation` (body, object): The confirmation email for confirmed opt-in. Sent back unchanged, the built-in one stays built-in and follows the name and description. - `confirmation.subject` (body, string) - `confirmation.previewText` (body, string) - `confirmation.markdown` (body, string): Markdown that keeps the button: an HTML link like Confirm - `theme` (body, object): Its look on its website and in its emails: a preset from list_themes and what's changed on top of it. Send only what changes; null takes a choice back to the preset's. - `theme.preset` (body, "mailcheap" | "broadsheet" | "garden" | "midnight" | "poster" | "swiss" | "typewriter" | "sunset"): A preset from list_themes. Another preset starts over from it: the changes made to the one before go, unless this call sends new ones. - `theme.colors` (body, object | null): Hex colors. null takes one back to the preset's; the other shades derive from these three. - `theme.fonts` (body, object | null): Fonts from list_themes. Mail clients without web fonts, like Gmail, show a close system font. - `theme.headingWeight` (body, integer | null): Fonts with fewer weights use their closest - `theme.radius` (body, string | null): Corners of buttons, cards and pictures - `theme.shadow` (body, string | null): Cards and forms on the website. Emails have none. - `theme.spacing` (body, string | null): Line height and space between paragraphs ```bash curl -X POST "http://localhost:3000/api/v1/newsletters" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Field notes","postalAddress":"Field Notes Ltd, 1 Main St, Paris, France"}' ``` #### Update a newsletter `PATCH /api/v1/newsletters/{id}` · MCP tool `update_newsletter` · settings:manage + messages:send · safe to repeat Changes a newsletter's settings. Send only the fields to change; nested groups (welcome, confirmation, cleanup, author) keep the fields you leave out. An account changes slugs 3 times a day at most: one more answers rate_limited. - `id` (path, string, required): The newsletter's ID or slug - `name` (body, string): The newsletter's name, shown in emails and on the archive - `slug` (body, string): Lowercase letters, digits and dashes, not a name ApresMail keeps like www, api or mail. The shared from address (hello@.) and the archive subdomain follow it. - `imageUrl` (body, string): Square logo, 128×128 or larger: an https:// link - `fromName` (body, string): Name in the From field - `fromEmail` (body, string): The shared address (hello@., set automatically) or an address on a domain added to this account - `replyTo` (body, string): Where replies go. Empty: replies to the shared address go to the account owner. - `postalAddress` (body, string): Printed in the footer of every email. Never set, it uses the configured default; saved empty, there is none and sending waits for one. - `archiveEnabled` (body, boolean): A public page for each sent issue, with a signup form. On for new newsletters: the website goes live after the first issue. - `description` (body, string): One sentence under the name on the archive, 200 characters at most - `author` (body, object): The archive's author page; the author's name is fromName - `author.bio` (body, string): Under the author's name on the archive, 160 characters at most - `author.imageUrl` (body, string): Author photo: an https:// link. The newsletter image otherwise. - `socialLinks` (body, string[]): https:// links shown on the archive, 8 at most - `requireConfirmation` (body, boolean): Confirmed opt-in: new signups get an email with a button and only join once they click it. On for new newsletters. New subscribers get the confirmation email or the welcome email: turning it on while the welcome email is on needs welcome.enabled false in the same request. - `cleanup` (body, object): List cleanup at each send: contacts who ignored several issues get a last-chance email instead, and become dormant if they don't answer - `cleanup.enabled` (body, boolean): On for new newsletters - `cleanup.issues` (body, number | null): Ignored issues in a row before the last-chance email, 2 to 100. Default 6. - `cleanup.days` (body, number | null): Days without an open, click or signup, 30 to 3650. Default 180. - `welcome` (body, object): Email sent to each new subscriber, while signups don't need confirming: turning it on while requireConfirmation is on needs requireConfirmation false in the same request. - `welcome.enabled` (body, boolean) - `welcome.subject` (body, string) - `welcome.previewText` (body, string) - `welcome.markdown` (body, string): Markdown - `confirmation` (body, object): The confirmation email for confirmed opt-in. Sent back unchanged, the built-in one stays built-in and follows the name and description. - `confirmation.subject` (body, string) - `confirmation.previewText` (body, string) - `confirmation.markdown` (body, string): Markdown that keeps the button: an HTML link like Confirm - `theme` (body, object): Its look on its website and in its emails: a preset from list_themes and what's changed on top of it. Send only what changes; null takes a choice back to the preset's. - `theme.preset` (body, "mailcheap" | "broadsheet" | "garden" | "midnight" | "poster" | "swiss" | "typewriter" | "sunset"): A preset from list_themes. Another preset starts over from it: the changes made to the one before go, unless this call sends new ones. - `theme.colors` (body, object | null): Hex colors. null takes one back to the preset's; the other shades derive from these three. - `theme.fonts` (body, object | null): Fonts from list_themes. Mail clients without web fonts, like Gmail, show a close system font. - `theme.headingWeight` (body, integer | null): Fonts with fewer weights use their closest - `theme.radius` (body, string | null): Corners of buttons, cards and pictures - `theme.shadow` (body, string | null): Cards and forms on the website. Emails have none. - `theme.spacing` (body, string | null): Line height and space between paragraphs ```bash curl -X PATCH "http://localhost:3000/api/v1/newsletters/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"replyTo":"jane@example.com","requireConfirmation":true}' ``` #### List themes `GET /api/v1/themes` · MCP tool `list_themes` · content:read · read-only, safe to repeat Lists the theme presets a newsletter can take, with their colors, fonts and choices, and the options to change them with: fonts, heading weights, radius, shadow and spacing. update_newsletter's theme sets them. No parameters. ```bash curl "http://localhost:3000/api/v1/themes" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Check a slug `GET /api/v1/newsletters/slugs/{slug}` · MCP tool `check_newsletter_slug` · content:read · read-only, safe to repeat Says whether a newsletter can take a slug: available, or taken by another newsletter, reserved by ApresMail or invalid, with the message saving it would answer. Checking doesn't count toward the 3 slug changes a day. - `slug` (path, string, required): The slug to check - `id` (query, string): The newsletter that would take it, by ID or slug: its own slug is available to it ```bash curl "http://localhost:3000/api/v1/newsletters/slugs/6650f1c2e9b1d4a012345678?slug=field-notes" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Send a test of the welcome email `POST /api/v1/newsletters/{id}/welcome-test` · MCP tool `send_welcome_test_email` · messages:send · reaches outside ApresMail Sends the saved welcome email to the account owner's address, switched on or not. It counts toward the plan's emails. - `id` (path, string, required): The newsletter's ID or slug ```bash curl -X POST "http://localhost:3000/api/v1/newsletters/6650f1c2e9b1d4a012345678/welcome-test" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` ### SMS #### Get SMS settings `GET /api/v1/sms/settings` · MCP tool `get_sms_settings` · content:read · read-only, safe to repeat Shows the account's controlled SMS pilot limit. This is a spending cap, not a bill or retail price. No parameters. ```bash curl "http://localhost:3000/api/v1/sms/settings" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### List SMS senders `GET /api/v1/sms/senders` · MCP tool `list_sms_senders` · content:read · read-only, safe to repeat Lists this account's approved or pending brand senders without AWS resource details. - `cursor` (query, string): The resource ID in this account - `limit` (query, integer): Number of senders per page, up to 100 ```bash curl "http://localhost:3000/api/v1/sms/senders" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Update SMS consent wording `PUT /api/v1/newsletters/{id}/sms-consent` · MCP tool `update_newsletter_sms_consent` · settings:manage + messages:send · safe to repeat Sets this newsletter's separate SMS wording and approved confirmation sender. It never grants SMS consent. - `id` (path, string, required): The resource ID in this account - `expectedRevision` (body, integer, required): The current version; stale changes are rejected - `requestId` (body, string, required): A unique request ID; reuse unchanged after an uncertain result - `senderId` (body, string, required): The resource ID in this account - `wordingText` (body, string, required): Separate SMS consent wording shown to subscribers - `privacyUrl` (body, string, required): Published HTTPS privacy policy URL for this newsletter ```bash curl -X PUT "http://localhost:3000/api/v1/newsletters/6650f1c2e9b1d4a012345678/sms-consent" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"expectedRevision":"…","requestId":"…","senderId":"…","wordingText":"…","privacyUrl":"…"}' ``` #### Get subscriber SMS preferences `GET /api/v1/subscribers/{id}/sms` · MCP tool `get_subscriber_sms` · audience:read · read-only, safe to repeat Shows a masked verified phone and independent SMS preferences; never exposes a full phone number. - `id` (path, string, required): The resource ID in this account ```bash curl "http://localhost:3000/api/v1/subscribers/6650f1c2e9b1d4a012345678/sms" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Remove subscriber SMS phone `DELETE /api/v1/subscribers/{id}/sms/phone` · MCP tool `remove_subscriber_sms_phone` · audience:write + audience:read · can't be undone, safe to repeat Removes the phone and withdraws all its SMS consents. Email subscriptions remain unchanged. - `id` (path, string, required): The resource ID in this account - `expectedPhoneVersion` (query, integer, required): The current version; stale changes are rejected - `requestId` (query, string, required): A unique request ID; reuse unchanged after an uncertain result ```bash curl -X DELETE "http://localhost:3000/api/v1/subscribers/6650f1c2e9b1d4a012345678/sms/phone" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Withdraw subscriber SMS consent `POST /api/v1/subscribers/{id}/sms/subscriptions/{subscriptionId}/withdraw` · MCP tool `withdraw_subscriber_sms` · audience:write + audience:read · safe to repeat Withdraws SMS permission for one newsletter, cancels pending confirmation and keeps other preferences unchanged. Cannot grant consent. - `id` (path, string, required): The resource ID in this account - `subscriptionId` (path, string, required): The resource ID in this account - `expectedVersion` (body, integer, required): The current version; stale changes are rejected - `requestId` (body, string, required): A unique request ID; reuse unchanged after an uncertain result ```bash curl -X POST "http://localhost:3000/api/v1/subscribers/6650f1c2e9b1d4a012345678/sms/subscriptions/6650f1c2e9b1d4a012345678/withdraw" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"expectedVersion":"…","requestId":"…"}' ``` ### Contacts Newsletter-specific compatibility actions. On v2, a contact ID is a NewsletterSubscription ID; add is organizational by default; explicit subscribe:true records owner authorization for unrestricted subscriptions without recipient confirmation. Imports do not imply consent, no welcome/sequence mail starts, and delete removes only that subscription. #### Inspect proposed import recipients `POST /api/v1/contacts/import-preview` · MCP tool `inspect_import_recipients` · audience:read · read-only, safe to repeat Preview only. No contacts or consent were changed. Reads consent for the selected newsletter on v2 accounts, current account-wide consent on v1 accounts, and independent restrictions for up to 200 addresses in one request snapshot. Different requests do not share a snapshot; these diagnostics never authorize delivery. - `newsletter` (body, string, required): Newsletter ID or slug in this account - `records` (body, object[], required): One through 200 distinct normalized addresses to inspect; no records are written - `records[].ordinal` (body, integer, required): Unique address ordinal in the local analysis, 1 through 25000 - `records[].email` (body, string, required): Normalized email; lowercase, no surrounding whitespace, preserving dots and plus suffixes ```bash curl -X POST "http://localhost:3000/api/v1/contacts/import-preview" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"newsletter":"weekly","records":"…"}' ``` #### Create subscriber import `POST /api/v1/subscriber-imports` · MCP tool `create_subscriber_import` · audience:write + audience:read · safe to repeat Bounded durable CSV import for one newsletter. Simple csv requires one profiles file and an existing target group, without migration evidence. New simple CSV runs authorize eligible blank/active subscriptions after exact-plan approval; existing restrictions and older runs retain their meaning. Owner-authorized approval and apply also require sending permission for scoped agents. Classic source active status alone never grants consent. No email, DOI or sequence is started. Verify your export columns and statuses; real Classic export qualification remains separate. - `newsletterId` (body, string, required): The one existing newsletter receiving this import. - `sourceKind` (body, "mailerlite-classic-csv" | "csv", required): csv for a simple owner-authorized group import, or mailerlite-classic-csv for the advanced migration preset. - `requestKey` (body, string, required): Stable idempotency key; retry exactly the same payload with this key. - `files` (body, object[], required): Confirmed file roles, headers and owner-declared source byte hashes; no raw file is stored. - `files[].fileRole` (body, "profiles" | "statuses" | "memberships", required) - `files[].header` (body, string[], required) - `files[].delimiter` (body, "comma" | "semicolon" | "tab", required) - `files[].declaredRows` (body, integer, required) - `files[].byteCount` (body, integer, required) - `files[].ownerDeclaredFileSha256` (body, string, required) - `mapping` (body, object, required): Explicit header, source status, group delimiter and timestamp mappings. - `mapping.profiles` (body, object, required) - `mapping.statuses` (body, object) - `mapping.memberships` (body, object) - `mapping.timestampFormat` (body, "iso-utc" | "classic-utc") - `mapping.timezone` (body, string) - `mapping.groupSeparator` (body, string) - `mapping.statusValues` (body, object) - `sourceContext` (body, object): Required only for Classic: source account/newsletter identity and UTC extraction time. Omit for csv. - `sourceContext.sourceAccountId` (body, string, required) - `sourceContext.sourceNewsletterId` (body, string, required) - `sourceContext.extractedAt` (body, string, required) - `groupMappings` (body, object[]): Explicit source group to newsletter-scoped existing or new group mapping. - `groupMappings[].sourceGroupId` (body, string, required) - `groupMappings[].target` (body, object, required) - `constantGroup` (body, object): For csv, one required existing group in the target newsletter. Optional for Classic. Membership never grants consent. - `evidencePolicy` (body, object): csv requires only version csv_facts_v1; new simple runs activate eligible subscriptions through owner authorization, without claiming recipient confirmation. Classic retains its closed facts-only or reviewed source policy. - `evidencePolicy.version` (body, "classic_csv_facts_v1" | "classic_confirmation_record_v1" | "csv_facts_v1", required) - `evidencePolicy.sourceNewsletterId` (body, string) - `evidencePolicy.artifactSha256` (body, string) - `evidencePolicy.acknowledgedSourceAssertion` (body, boolean) ```bash curl -X POST "http://localhost:3000/api/v1/subscriber-imports" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"newsletterId":"…","sourceKind":"…","requestKey":"…","files":"…","mapping":"…"}' ``` #### Upload subscriber import chunk `POST /api/v1/subscriber-imports/{id}/chunks` · MCP tool `upload_subscriber_import_chunk` · audience:write + audience:read · safe to repeat Bounded durable CSV import for one newsletter. Simple csv requires one profiles file and an existing target group, without migration evidence. New simple CSV runs authorize eligible blank/active subscriptions after exact-plan approval; existing restrictions and older runs retain their meaning. Owner-authorized approval and apply also require sending permission for scoped agents. Classic source active status alone never grants consent. No email, DOI or sequence is started. Verify your export columns and statuses; real Classic export qualification remains separate. - `id` (path, string, required): The opaque import run ID in this account. - `revision` (body, integer, required): The latest run revision; stale writes return a conflict. - `requestKey` (body, string, required): Stable idempotency key; retry exactly the same payload with this key. - `fileRole` (body, "profiles" | "statuses" | "memberships", required): The declared file role for this chunk. - `chunkIndex` (body, integer, required): Contiguous zero-based chunk index for this role. - `rows` (body, object[], required): Up to 200 raw cell arrays with contiguous one-based source ordinals. - `rows[].ordinal` (body, integer, required) - `rows[].cells` (body, string[], required) - `declaredDigest` (body, string, required): SHA256 of JSON.stringify(rows), verified independently by the server. ```bash curl -X POST "http://localhost:3000/api/v1/subscriber-imports/6650f1c2e9b1d4a012345678/chunks" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"revision":"…","requestKey":"…","fileRole":"…","chunkIndex":"…","rows":"…","declaredDigest":"…"}' ``` #### Stage subscriber import `POST /api/v1/subscriber-imports/{id}/stage` · MCP tool `stage_subscriber_import` · audience:write + audience:read · safe to repeat Bounded durable CSV import for one newsletter. Simple csv requires one profiles file and an existing target group, without migration evidence. New simple CSV runs authorize eligible blank/active subscriptions after exact-plan approval; existing restrictions and older runs retain their meaning. Owner-authorized approval and apply also require sending permission for scoped agents. Classic source active status alone never grants consent. No email, DOI or sequence is started. Verify your export columns and statuses; real Classic export qualification remains separate. - `id` (path, string, required): The opaque import run ID in this account. - `revision` (body, integer, required): The latest run revision; stale writes return a conflict. - `requestKey` (body, string, required): Stable idempotency key; retry exactly the same payload with this key. - `uploadedCounts` (body, object, required): Exact uploaded row counts per role. - `canonicalDigest` (body, string, required): Authoritative canonical digest returned by the last upload. ```bash curl -X POST "http://localhost:3000/api/v1/subscriber-imports/6650f1c2e9b1d4a012345678/stage" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"revision":"…","requestKey":"…","uploadedCounts":"…","canonicalDigest":"…"}' ``` #### Preview subscriber import `POST /api/v1/subscriber-imports/{id}/preview` · MCP tool `preview_subscriber_import` · audience:write + audience:read · safe to repeat Bounded durable CSV import for one newsletter. Simple csv requires one profiles file and an existing target group, without migration evidence. New simple CSV runs authorize eligible blank/active subscriptions after exact-plan approval; existing restrictions and older runs retain their meaning. Owner-authorized approval and apply also require sending permission for scoped agents. Classic source active status alone never grants consent. No email, DOI or sequence is started. Verify your export columns and statuses; real Classic export qualification remains separate. - `id` (path, string, required): The opaque import run ID in this account. - `revision` (body, integer, required): The latest run revision; stale writes return a conflict. - `requestKey` (body, string, required): Stable idempotency key; retry exactly the same payload with this key. ```bash curl -X POST "http://localhost:3000/api/v1/subscriber-imports/6650f1c2e9b1d4a012345678/preview" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"revision":"…","requestKey":"…"}' ``` #### Get subscriber import `GET /api/v1/subscriber-imports/{id}` · MCP tool `get_subscriber_import` · audience:read · read-only, safe to repeat Bounded durable CSV import for one newsletter. Simple csv requires one profiles file and an existing target group, without migration evidence. New simple CSV runs authorize eligible blank/active subscriptions after exact-plan approval; existing restrictions and older runs retain their meaning. Owner-authorized approval and apply also require sending permission for scoped agents. Classic source active status alone never grants consent. No email, DOI or sequence is started. Verify your export columns and statuses; real Classic export qualification remains separate. - `id` (path, string, required): The opaque import run ID in this account. ```bash curl "http://localhost:3000/api/v1/subscriber-imports/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### List subscriber imports `GET /api/v1/subscriber-imports` · MCP tool `list_subscriber_imports` · audience:read · read-only, safe to repeat Bounded durable CSV import for one newsletter. Simple csv requires one profiles file and an existing target group, without migration evidence. New simple CSV runs authorize eligible blank/active subscriptions after exact-plan approval; existing restrictions and older runs retain their meaning. Owner-authorized approval and apply also require sending permission for scoped agents. Classic source active status alone never grants consent. No email, DOI or sequence is started. Verify your export columns and statuses; real Classic export qualification remains separate. - `cursor` (query, string): Opaque signed continuation cursor from the preceding page. - `limit` (query, integer): Maximum bounded page size. ```bash curl "http://localhost:3000/api/v1/subscriber-imports" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Get subscriber import rows `GET /api/v1/subscriber-imports/{id}/rows` · MCP tool `get_subscriber_import_rows` · audience:read · read-only, safe to repeat Bounded durable CSV import for one newsletter. Simple csv requires one profiles file and an existing target group, without migration evidence. New simple CSV runs authorize eligible blank/active subscriptions after exact-plan approval; existing restrictions and older runs retain their meaning. Owner-authorized approval and apply also require sending permission for scoped agents. Classic source active status alone never grants consent. No email, DOI or sequence is started. Verify your export columns and statuses; real Classic export qualification remains separate. - `id` (path, string, required): The opaque import run ID in this account. - `cursor` (query, string): Opaque signed continuation cursor from the preceding page. - `limit` (query, integer): Maximum bounded page size. - `resultCode` (query, string): Optional exact safe result classification filter. ```bash curl "http://localhost:3000/api/v1/subscriber-imports/6650f1c2e9b1d4a012345678/rows" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Approve subscriber import `POST /api/v1/subscriber-imports/{id}/approve` · MCP tool `approve_subscriber_import` · audience:write + audience:read · safe to repeat Bounded durable CSV import for one newsletter. Simple csv requires one profiles file and an existing target group, without migration evidence. New simple CSV runs authorize eligible blank/active subscriptions after exact-plan approval; existing restrictions and older runs retain their meaning. Owner-authorized approval and apply also require sending permission for scoped agents. Classic source active status alone never grants consent. No email, DOI or sequence is started. Verify your export columns and statuses; real Classic export qualification remains separate. - `id` (path, string, required): The opaque import run ID in this account. - `revision` (body, integer, required): The latest run revision; stale writes return a conflict. - `requestKey` (body, string, required): Stable idempotency key; retry exactly the same payload with this key. - `planDigest` (body, string, required): The exact completed preview plan digest being approved or applied. - `evidenceDigest` (body, string, required): The exact retained source evidence digest reviewed by the owner. - `acknowledgement` (body, "approve-reviewed-import" | "approve-owner-authorized-import", required): Use approve-owner-authorized-import only for subscriptionMode owner-authorized to authorize eligible subscriptions; organizational and reviewed-evidence runs require approve-reviewed-import. This approves the exact plan, without claiming recipient confirmation. ```bash curl -X POST "http://localhost:3000/api/v1/subscriber-imports/6650f1c2e9b1d4a012345678/approve" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"revision":"…","requestKey":"…","planDigest":"…","evidenceDigest":"…","acknowledgement":"…"}' ``` #### Apply subscriber import `POST /api/v1/subscriber-imports/{id}/apply` · MCP tool `apply_subscriber_import` · audience:write + audience:read · safe to repeat Bounded durable CSV import for one newsletter. Simple csv requires one profiles file and an existing target group, without migration evidence. New simple CSV runs authorize eligible blank/active subscriptions after exact-plan approval; existing restrictions and older runs retain their meaning. Owner-authorized approval and apply also require sending permission for scoped agents. Classic source active status alone never grants consent. No email, DOI or sequence is started. Verify your export columns and statuses; real Classic export qualification remains separate. - `id` (path, string, required): The opaque import run ID in this account. - `revision` (body, integer, required): The latest run revision; stale writes return a conflict. - `requestKey` (body, string, required): Stable idempotency key; retry exactly the same payload with this key. - `planDigest` (body, string, required): The exact completed preview plan digest being approved or applied. ```bash curl -X POST "http://localhost:3000/api/v1/subscriber-imports/6650f1c2e9b1d4a012345678/apply" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"revision":"…","requestKey":"…","planDigest":"…"}' ``` #### Cancel subscriber import `POST /api/v1/subscriber-imports/{id}/cancel` · MCP tool `cancel_subscriber_import` · audience:write + audience:read · can't be undone, safe to repeat Bounded durable CSV import for one newsletter. Simple csv requires one profiles file and an existing target group, without migration evidence. New simple CSV runs authorize eligible blank/active subscriptions after exact-plan approval; existing restrictions and older runs retain their meaning. Owner-authorized approval and apply also require sending permission for scoped agents. Classic source active status alone never grants consent. No email, DOI or sequence is started. Verify your export columns and statuses; real Classic export qualification remains separate. - `id` (path, string, required): The opaque import run ID in this account. - `revision` (body, integer, required): The latest run revision; stale writes return a conflict. - `requestKey` (body, string, required): Stable idempotency key; retry exactly the same payload with this key. ```bash curl -X POST "http://localhost:3000/api/v1/subscriber-imports/6650f1c2e9b1d4a012345678/cancel" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"revision":"…","requestKey":"…"}' ``` #### Export subscriber import report `POST /api/v1/subscriber-imports/{id}/report` · MCP tool `export_subscriber_import_report` · audience:read · read-only, safe to repeat Bounded durable CSV import for one newsletter. Simple csv requires one profiles file and an existing target group, without migration evidence. New simple CSV runs authorize eligible blank/active subscriptions after exact-plan approval; existing restrictions and older runs retain their meaning. Owner-authorized approval and apply also require sending permission for scoped agents. Classic source active status alone never grants consent. No email, DOI or sequence is started. Verify your export columns and statuses; real Classic export qualification remains separate. - `id` (path, string, required): The opaque import run ID in this account. - `cursor` (body, string): Opaque signed continuation cursor from the preceding page. - `limit` (body, integer): Maximum bounded page size. - `resultCode` (body, string): Optional exact safe result classification filter. ```bash curl -X POST "http://localhost:3000/api/v1/subscriber-imports/6650f1c2e9b1d4a012345678/report" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### List contacts `GET /api/v1/contacts` · MCP tool `list_contacts` · audience:read · read-only, safe to repeat Lists newsletter contacts, newest signups first, 50 a page. On v2 each contact ID is a Subscription ID and email comes from its Subscriber profile. Filter by newsletter, status or the start of the email address; segment filtering remains a v1 option. Pass nextCursor back as cursor. counts has every status for the same filters. - `newsletter` (query, string): Only this newsletter's: its ID or slug. Default: every newsletter. - `status` (query, "active" | "pending" | "unsubscribed" | "bounced" | "complained" | "dormant"): Only contacts with this status - `segment` (query, string): Only the contacts a segment picks (its ID, from list_segments), whatever their status unless status is given. With newsletter, one of that newsletter's segments. - `search` (query, string): Contacts whose email starts with this - `limit` (query, integer): Contacts per page, 50 by default, 100 at most - `cursor` (query, string): nextCursor from the previous page ```bash curl "http://localhost:3000/api/v1/contacts?status=active&search=jane" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Get a contact `GET /api/v1/contacts/{id}` · MCP tool `get_contact` · audience:read · read-only, safe to repeat Gets one newsletter contact by Subscription ID on v2 (legacy Contact ID on v1), or by email when unambiguous. Status and engagement belong to that newsletter. - `id` (path, string, required): The contact's ID or email address - `newsletter` (query, string): The newsletter's ID or slug, needed for an email address that is a contact of several newsletters ```bash curl "http://localhost:3000/api/v1/contacts/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Add a contact `POST /api/v1/contacts` · MCP tool `add_contact` · audience:write + audience:read · reaches outside ApresMail Adds a contact to one newsletter. With subscribe:true on a ready unified audience, the owner authorizes a new unrestricted subscription to receive issues without recipient confirmation. Omit subscribe to add an organizational contact only. Existing withdrawals, delivery blocks and dormant states remain unchanged. Never sends a welcome or confirmation message. Requires full access. - `newsletter` (body, string): The newsletter's ID or slug. Default: the account's first newsletter, the one it has had longest. - `email` (body, string, required): The subscriber's email address - `source` (body, string): Where they signed up, e.g. website. Default: api. - `subscribe` (body, boolean): The owner authorizes this newsletter subscription without recipient confirmation. Unified audience and full access required. Never restores a withdrawal or removes delivery blocks. - `resubscribe` (body, boolean): Legacy option: true is rejected. Use a recipient-confirmed signup to renew consent. - `ip` (body, string): The visitor's IP address: one visitor gets 5 signups in 10 minutes ```bash curl -X POST "http://localhost:3000/api/v1/contacts" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email":"jane@example.com","source":"website"}' ``` #### Change a contact's status `PATCH /api/v1/contacts/{id}` · MCP tool `update_contact_status` · audience:write + audience:read · can't be undone, safe to repeat Changes engagement status for an already-consented newsletter contact, or withdraws only its newsletter consent on v2 (account-wide on v1). Operator input cannot activate pending or withdrawn consent. No email is sent. - `id` (path, string, required): The contact's ID or email address - `newsletter` (body, string): The newsletter's ID or slug, needed for an email address that is a contact of several newsletters - `status` (body, "active" | "dormant" | "unsubscribed", required): active, dormant or unsubscribed - `consent` (body, boolean): Legacy assertion; it never grants or restores marketing consent ```bash curl -X PATCH "http://localhost:3000/api/v1/contacts/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"status":"dormant"}' ``` #### Delete a contact `DELETE /api/v1/contacts/{id}` · MCP tool `delete_contact` · audience:write + audience:read · can't be undone, safe to repeat Deletes one newsletter contact. On v2 its ID is a Subscription ID: only that newsletter is withdrawn and soft-deleted, while the Subscriber and other newsletters remain. On v1, the legacy deletion and account-wide recipient rules apply. Reimporting never restores consent. - `id` (path, string, required): The contact's ID or email address - `newsletter` (query, string): The newsletter's ID or slug, needed for an email address that is a contact of several newsletters ```bash curl -X DELETE "http://localhost:3000/api/v1/contacts/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Import contacts `POST /api/v1/contacts/import` · MCP tool `import_contacts` · audience:write + audience:read · can't be undone, safe to repeat Adds up to 1000 contacts per call, with the same checks as the dashboard's CSV import: invalid, misspelled (gmial.com…), spam trap, role (info@, support@…), throwaway, duplicate and suppressed addresses, undeliverable ones (ApresMail knows they can't receive email: they hard bounced or hit a spam trap, from any sender on ApresMail), and addresses on domains that can't receive email, are left out and counted. consent must be true: "These people agreed to receive my emails". Existing contacts keep their status unless a row gives a stricter one (unsubscribed, bounced, complained), which is final. No welcome emails go out. For more rows, send the next chunk with the importId of the first. - `newsletter` (body, string): The newsletter's ID or slug. Default: the account's first newsletter, the one it has had longest. - `consent` (body, boolean, required): Must be true: These people agreed to receive my emails. Bought or scraped lists aren't allowed. - `contacts` (body, object[], required): The rows, 1000 at most - `contacts[].email` (body, string, required): Invalid addresses are left out and counted - `contacts[].status` (body, "active" | "dormant" | "unsubscribed" | "bounced" | "complained"): active (default), dormant, unsubscribed, bounced or complained - `contacts[].subscribedAt` (body, string): When they signed up, ISO 8601 - `contacts[].emailsReceived` (body, number): Emails they got on the previous platform - `contacts[].openRate` (body, number): 0 to 1, from the previous platform: sends reach the most engaged first - `contacts[].clickRate` (body, number): 0 to 1, from the previous platform - `importId` (body, string): The importId of the first chunk, to add rows to the same import - `duplicates` (body, integer): Duplicates you removed before sending, counted in the import's report ```bash curl -X POST "http://localhost:3000/api/v1/contacts/import" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"consent":true,"contacts":[{"email":"jane@example.com","subscribedAt":"2025-03-01T00:00:00Z"},{"email":"sam@example.com","status":"unsubscribed"}]}' ``` #### Export contacts `GET /api/v1/contacts/export` · MCP tool `export_contacts` · audience:read · read-only, safe to repeat Makes a CSV download link for the contacts matching the filters, valid for an hour, no API key needed to open it. The columns go back in through the import as they are. - `newsletter` (query, string): Only this newsletter's: its ID or slug. Default: every newsletter. - `status` (query, "active" | "pending" | "unsubscribed" | "bounced" | "complained" | "dormant"): Only contacts with this status - `segment` (query, string): Only the contacts a segment picks (its ID, from list_segments), whatever their status unless status is given. With newsletter, one of that newsletter's segments. - `search` (query, string): Contacts whose email starts with this ```bash curl "http://localhost:3000/api/v1/contacts/export?status=active" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` ### Subscribers Canonical v2 profiles and newsletter-specific subscriptions, groups and consent events. Writes are scoped and idempotent; no action grants consent. #### List subscribers `GET /api/v1/subscribers` · MCP tool `list_subscribers` · audience:read · read-only, safe to repeat Lists canonical profiles, fifty raw records per keyset page. Newsletter, consent and group filters never grant consent. - `email` (query, string): Exact email address to look up; cannot be combined with a cursor - `newsletterId` (query, string): An ID in this account - `consentState` (query, "none" | "active" | "withdrawn"): Consent state within the selected newsletter; requires newsletterId - `groupId` (query, string): An ID in this account - `cursor` (query, string): Opaque continuation cursor from the previous page; omit for the first page ```bash curl "http://localhost:3000/api/v1/subscribers" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Get a subscriber `GET /api/v1/subscribers/{id}` · MCP tool `get_subscriber` · audience:read · read-only, safe to repeat Gets one live canonical profile in this account. - `id` (path, string, required): An ID in this account ```bash curl "http://localhost:3000/api/v1/subscribers/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### List subscriber subscriptions `GET /api/v1/subscribers/{id}/subscriptions` · MCP tool `list_subscriptions` · audience:read · read-only, safe to repeat Lists a subscriber's newsletter-specific subscriptions, fifty raw records per page. - `id` (path, string, required): An ID in this account - `cursor` (query, string): Opaque continuation cursor from the previous page; omit for the first page ```bash curl "http://localhost:3000/api/v1/subscribers/6650f1c2e9b1d4a012345678/subscriptions" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### List subscription groups `GET /api/v1/subscriptions/{id}/groups` · MCP tool `list_subscription_groups` · audience:read · read-only, safe to repeat Lists only groups attached to this subscription's newsletter. - `id` (path, string, required): An ID in this account - `cursor` (query, string): Opaque continuation cursor from the previous page; omit for the first page ```bash curl "http://localhost:3000/api/v1/subscriptions/6650f1c2e9b1d4a012345678/groups" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### List subscription consent events `GET /api/v1/subscriptions/{id}/events` · MCP tool `list_subscription_events` · audience:read · read-only, safe to repeat Lists newsletter-scoped consent evidence without private references or legacy account grants. - `id` (path, string, required): An ID in this account - `cursor` (query, string): Opaque continuation cursor from the previous page; omit for the first page ```bash curl "http://localhost:3000/api/v1/subscriptions/6650f1c2e9b1d4a012345678/events" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Update subscriber profile `PATCH /api/v1/subscribers/{id}` · MCP tool `update_subscriber` · audience:write + audience:read · safe to repeat Changes only the first name with profile version compare-and-set. Never grants consent. - `id` (path, string, required): An ID in this account - `profileVersion` (body, integer, required): Current profile version for conflict detection - `firstName` (body, string | null, required): New first name, or null to clear it - `requestId` (body, string, required): Stable UUID for this exact operation ```bash curl -X PATCH "http://localhost:3000/api/v1/subscribers/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"profileVersion":"…","firstName":"…","requestId":"…"}' ``` #### Withdraw subscription `POST /api/v1/subscriptions/{id}/withdraw` · MCP tool `withdraw_subscription` · audience:write + audience:read · can't be undone, safe to repeat Withdraws only this newsletter's current consent epoch. Replays with the same requestId return the original result. - `id` (path, string, required): An ID in this account - `requestId` (body, string, required): Stable UUID for this exact operation ```bash curl -X POST "http://localhost:3000/api/v1/subscriptions/6650f1c2e9b1d4a012345678/withdraw" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"requestId":"…"}' ``` #### Delete a subscription `DELETE /api/v1/subscriptions/{id}` · MCP tool `delete_subscription` · audience:write + audience:read · can't be undone, safe to repeat Soft-deletes one newsletter subscription, withdraws consent and removes its organizational memberships. The profile and other newsletters remain. - `id` (path, string, required): An ID in this account - `requestId` (query, string, required): Stable UUID for this exact operation ```bash curl -X DELETE "http://localhost:3000/api/v1/subscriptions/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Erase subscriber `DELETE /api/v1/subscribers/{id}` · MCP tool `erase_subscriber` · audience:write + audience:read · can't be undone, safe to repeat Starts account-wide privacy erasure for this profile and all its newsletter subscriptions. Retains tombstone IDs. - `id` (path, string, required): An ID in this account - `requestId` (query, string, required): Stable UUID for this exact operation ```bash curl -X DELETE "http://localhost:3000/api/v1/subscribers/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` ### Recipients Deprecated v1 account-wide consent reads. V2 accounts use Subscribers actions and receive conflict for these legacy tools. #### List recipients through the deprecated endpoint `GET /api/v1/recipients` · MCP tool `list_recipients` · audience:read · read-only, safe to repeat Deprecated legacy account-wide consent read. On a v2 account this always returns conflict; use list_subscribers and list_subscriptions. On v1, exact email search only; erased records are omitted. - `state` (query, "none" | "active" | "withdrawn"): Only recipients with this canonical consent state; omitted means all states - `email` (query, string): Exact email match, preserving plus suffixes and dots; cannot be combined with cursor - `cursor` (query, string): The previous page nextCursor with the same filters; never combine with email ```bash curl "http://localhost:3000/api/v1/recipients" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Get a recipient through the deprecated endpoint `GET /api/v1/recipients/{id}` · MCP tool `get_recipient` · audience:read · read-only, safe to repeat Deprecated legacy account-wide consent read. On a v2 account this always returns conflict; use get_subscriber and list_subscriptions. On v1, erased and foreign records return not_found. - `id` (path, string, required): The canonical recipient ID in this account ```bash curl "http://localhost:3000/api/v1/recipients/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` ### Segments Saved conditions that pick some of a newsletter's contacts by engagement, signup date, source or address, to send campaigns to them only. #### List segments `GET /api/v1/segments` · MCP tool `list_segments` · audience:read · read-only, safe to repeat Lists segments, oldest first, with their conditions in words and how many active contacts each picks now. - `newsletter` (query, string): Only this newsletter's: its ID or slug. Default: every newsletter. ```bash curl "http://localhost:3000/api/v1/segments" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Get a segment `GET /api/v1/segments/{id}` · MCP tool `get_segment` · audience:read · read-only, safe to repeat Gets one segment with its conditions and how many active contacts it picks now. - `id` (path, string, required): The segment's ID ```bash curl "http://localhost:3000/api/v1/segments/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Count a segment's contacts `POST /api/v1/segments/count` · MCP tool `count_segment` · audience:read · read-only, safe to repeat Counts the active contacts some conditions would pick, without saving anything: try them before create_segment. - `newsletter` (body, string): The newsletter's ID or slug. Default: the account's first newsletter, the one it has had longest. - `match` (body, "all" | "any"): all: contacts meeting every condition (the default). any: contacts meeting at least one. - `conditions` (body, object[], required): The conditions, 10 at most. Dates count back from each send, so "within 30 days" moves with time. - `conditions[].field` (body, "opened" | "clicked" | "missed_issues" | "emails_opened" | "signed_up" | "source" | "email", required): opened, clicked: the last open or click (a click counts as an open). missed_issues: issues received in a row without opening or clicking any. emails_opened: emails opened in all. signed_up: the signup date. source: where the contact came from (website, import, dashboard, api, or a source an API caller gave). email: the address. - `conditions[].op` (body, "within" | "not_within" | "never" | "at_least" | "less_than" | "before" | "since" | "is" | "is_not" | "contains" | "ends_with", required): opened, clicked: within, not_within (days) or never. missed_issues, emails_opened: at_least or less_than (a number). signed_up: within, not_within (days), before or since (a date). source: is or is_not. email: contains or ends_with. - `conditions[].value` (body, number | string): Days (1 to 3650) for within and not_within, a number for at_least and less_than, YYYY-MM-DD (UTC) for before and since, text for the rest. None for never. ```bash curl -X POST "http://localhost:3000/api/v1/segments/count" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"conditions":[{"field":"opened","op":"within","value":30}]}' ``` #### Create a segment `POST /api/v1/segments` · MCP tool `create_segment` · audience:write + audience:read Saves conditions that pick some of a newsletter's contacts, to send campaigns to them only (segment on update_campaign, send_campaign or schedule_campaign). Contacts join and leave it as they meet the conditions. - `newsletter` (body, string): The newsletter's ID or slug. Default: the account's first newsletter, the one it has had longest. - `name` (body, string, required): The segment's name, 60 characters at most - `match` (body, "all" | "any"): all: contacts meeting every condition (the default). any: contacts meeting at least one. - `conditions` (body, object[], required): The conditions, 10 at most. Dates count back from each send, so "within 30 days" moves with time. - `conditions[].field` (body, "opened" | "clicked" | "missed_issues" | "emails_opened" | "signed_up" | "source" | "email", required): opened, clicked: the last open or click (a click counts as an open). missed_issues: issues received in a row without opening or clicking any. emails_opened: emails opened in all. signed_up: the signup date. source: where the contact came from (website, import, dashboard, api, or a source an API caller gave). email: the address. - `conditions[].op` (body, "within" | "not_within" | "never" | "at_least" | "less_than" | "before" | "since" | "is" | "is_not" | "contains" | "ends_with", required): opened, clicked: within, not_within (days) or never. missed_issues, emails_opened: at_least or less_than (a number). signed_up: within, not_within (days), before or since (a date). source: is or is_not. email: contains or ends_with. - `conditions[].value` (body, number | string): Days (1 to 3650) for within and not_within, a number for at_least and less_than, YYYY-MM-DD (UTC) for before and since, text for the rest. None for never. ```bash curl -X POST "http://localhost:3000/api/v1/segments" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Engaged","conditions":[{"field":"opened","op":"within","value":30}]}' ``` #### Update a segment `PATCH /api/v1/segments/{id}` · MCP tool `update_segment` · audience:write + audience:read · safe to repeat Renames a segment or changes its conditions. Send only what changes; conditions replace the old ones. Campaigns going to it, scheduled ones included, use the new conditions when they go out. - `id` (path, string, required): The segment's ID - `name` (body, string): The segment's name, 60 characters at most - `match` (body, "all" | "any"): all: contacts meeting every condition (the default). any: contacts meeting at least one. - `conditions` (body, object[]): The conditions, 10 at most. Dates count back from each send, so "within 30 days" moves with time. - `conditions[].field` (body, "opened" | "clicked" | "missed_issues" | "emails_opened" | "signed_up" | "source" | "email", required): opened, clicked: the last open or click (a click counts as an open). missed_issues: issues received in a row without opening or clicking any. emails_opened: emails opened in all. signed_up: the signup date. source: where the contact came from (website, import, dashboard, api, or a source an API caller gave). email: the address. - `conditions[].op` (body, "within" | "not_within" | "never" | "at_least" | "less_than" | "before" | "since" | "is" | "is_not" | "contains" | "ends_with", required): opened, clicked: within, not_within (days) or never. missed_issues, emails_opened: at_least or less_than (a number). signed_up: within, not_within (days), before or since (a date). source: is or is_not. email: contains or ends_with. - `conditions[].value` (body, number | string): Days (1 to 3650) for within and not_within, a number for at_least and less_than, YYYY-MM-DD (UTC) for before and since, text for the rest. None for never. ```bash curl -X PATCH "http://localhost:3000/api/v1/segments/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Engaged this quarter","conditions":[{"field":"opened","op":"within","value":90}]}' ``` #### Delete a segment `DELETE /api/v1/segments/{id}` · MCP tool `delete_segment` · audience:write + audience:read · can't be undone, safe to repeat Deletes a segment. Its contacts stay as they are. A scheduled, sending or paused campaign going to it answers conflict: change who it goes to first. Drafts going to it can't be sent until they get another segment or none. - `id` (path, string, required): The segment's ID ```bash curl -X DELETE "http://localhost:3000/api/v1/segments/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` ### Sequences #### Import a Classic sequence draft `POST /api/v1/sequences/import-classic` · MCP tool `import_classic_sequence` · content:write · safe to repeat Imports a reviewed MailerLite Classic migration bundle as a draft. Repeating the same source account, workflow and target returns the original draft; changed content conflicts. Does not publish, enroll participants or send. - `newsletterId` (body, string, required): The existing target newsletter in this account; its sender settings apply. - `bundle` (body, object, required): One explicitly authored Classic migration bundle, not a native automation export; at most 512 KiB. - `bundle.format` (body, string, required) - `bundle.version` (body, number, required) - `bundle.sourceAccount` (body, string, required) - `bundle.sourceSequenceId` (body, string, required) - `bundle.name` (body, string, required) - `bundle.trigger` (body, object, required) - `bundle.entryPolicy` (body, "once" | "on_new_join"): Once per subscriber by default. on_new_join requires a fresh group join after the previous run ends; current runs never overlap. - `bundle.schedule` (body, object, required) - `bundle.entryStepId` (body, string, required) - `bundle.steps` (body, any[], required) - `groupMappings` (body, object[]): Exact source group references mapped to groups in the target newsletter; omitted groups stay unresolved. - `groupMappings[].sourceGroupId` (body, string, required) - `groupMappings[].targetGroupId` (body, string, required) - `contentReviewed` (body, boolean, required): Confirm manual review of original unpersonalized content, retained external links/assets and timing; this does not grant consent or publish. ```bash curl -X POST "http://localhost:3000/api/v1/sequences/import-classic" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"newsletterId":"…","bundle":"…","contentReviewed":"…"}' ``` #### List sequences `GET /api/v1/sequences` · MCP tool `list_sequences` · content:read · read-only, safe to repeat Lists account-wide email sequences without enrolling or sending. - `page` (query, integer): One-based result page, fifty entries per page - `state` (query, "draft" | "active" | "paused" | "stopped" | "archived"): Only sequences in this lifecycle state ```bash curl "http://localhost:3000/api/v1/sequences" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Get a sequence `GET /api/v1/sequences/{id}` · MCP tool `get_sequence` · content:read · read-only, safe to repeat Reads the saved draft and published revision identity. - `id` (path, string, required): The resource ID in the authenticated account ```bash curl "http://localhost:3000/api/v1/sequences/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Create a sequence `POST /api/v1/sequences` · MCP tool `create_sequence` · content:write · safe to repeat Creates a draft only; reuse requestId after a lost response. Does not enroll or send. - `requestId` (body, string, required): A stable UUID for this exact operation; reuse it only to retry the same payload - `name` (body, string, required): A short name for the sequence - `newsletterId` (body, string, required): The resource ID in the authenticated account ```bash curl -X POST "http://localhost:3000/api/v1/sequences" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"requestId":"…","name":"…","newsletterId":"…"}' ``` #### Save a sequence `PATCH /api/v1/sequences/{id}` · MCP tool `update_sequence` · content:write · safe to repeat Saves the exact draft using its expected version and stable request identity; existing enrollments are unchanged. - `id` (path, string, required): The resource ID in the authenticated account - `requestId` (body, string, required): A stable UUID for this exact operation; reuse it only to retry the same payload - `expectedVersion` (body, integer, required): The expected version returned by the latest read of this resource - `name` (body, string, required): A short name for the sequence - `draft` (body, object, required): The complete saved trigger, graph and send schedule - `draft.trigger` (body, any, required) - `draft.entryPolicy` (body, "once" | "on_new_join"): Once per subscriber by default. on_new_join requires a fresh group join after the previous run ends; current runs never overlap. - `draft.entryNodeId` (body, string, required) - `draft.nodes` (body, any[], required) - `draft.schedule` (body, object, required) ```bash curl -X PATCH "http://localhost:3000/api/v1/sequences/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"requestId":"…","expectedVersion":"…","name":"…","draft":"…"}' ``` #### Preview a sequence `POST /api/v1/sequences/{id}/preview` · MCP tool `preview_sequence` · content:write · read-only, safe to repeat Validates and renders a saved draft. Simulates synthetic facts without creating recipients, jobs or sending. - `id` (path, string, required): The resource ID in the authenticated account - `expectedVersion` (body, integer, required): The expected version returned by the latest read of this resource - `simulation` (body, object): Synthetic clock, memberships and origin form for a read-only path simulation - `simulation.at` (body, string, required) - `simulation.memberGroupIds` (body, string[], required) - `simulation.originFormId` (body, string | null, required) ```bash curl -X POST "http://localhost:3000/api/v1/sequences/6650f1c2e9b1d4a012345678/preview" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"expectedVersion":"…"}' ``` #### Publish a sequence `POST /api/v1/sequences/{id}/publish` · MCP tool `publish_sequence` · content:write + messages:send · safe to repeat Approves the exact preview as an immutable revision for future trigger events. A paused sequence remains paused. - `id` (path, string, required): The resource ID in the authenticated account - `requestId` (body, string, required): A stable UUID for this exact operation; reuse it only to retry the same payload - `expectedVersion` (body, integer, required): The expected version returned by the latest read of this resource - `previewDigest` (body, string, required): The digest from previewing this exact saved version ```bash curl -X POST "http://localhost:3000/api/v1/sequences/6650f1c2e9b1d4a012345678/publish" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"requestId":"…","expectedVersion":"…","previewDigest":"…"}' ``` #### Change sequence state `POST /api/v1/sequences/{id}/lifecycle` · MCP tool `sequence_lifecycle` · content:write · safe to repeat Pauses, resumes, permanently stops or archives a sequence. Resume never replays historical trigger events. - `id` (path, string, required): The resource ID in the authenticated account - `requestId` (body, string, required): A stable UUID for this exact operation; reuse it only to retry the same payload - `expectedLifecycleVersion` (body, integer, required): The expected version returned by the latest read of this resource - `action` (body, "pause" | "resume" | "stop" | "archive", required): The lifecycle transition to apply ```bash curl -X POST "http://localhost:3000/api/v1/sequences/6650f1c2e9b1d4a012345678/lifecycle" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"requestId":"…","expectedLifecycleVersion":"…","action":"…"}' ``` #### List sequence participants `GET /api/v1/sequences/{id}/participants` · MCP tool `list_sequence_participants` · audience:read · read-only, safe to repeat Lists participant versions, status and next due times within this account. - `id` (path, string, required): The resource ID in the authenticated account - `page` (query, integer): One-based result page, fifty entries per page - `state` (query, "waiting" | "active" | "unknown" | "completed" | "stopped" | "expired" | "failed"): Only participants in this execution state ```bash curl "http://localhost:3000/api/v1/sequences/6650f1c2e9b1d4a012345678/participants" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Get sequence participant `GET /api/v1/sequences/{id}/participants/{enrollmentId}` · MCP tool `get_sequence_participant` · audience:read · read-only, safe to repeat Reads bounded execution history including chosen branches and safe stop reasons. - `id` (path, string, required): The resource ID in the authenticated account - `enrollmentId` (path, string, required): The resource ID in the authenticated account ```bash curl "http://localhost:3000/api/v1/sequences/6650f1c2e9b1d4a012345678/participants/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Change participant state `POST /api/v1/sequences/{id}/participants/{enrollmentId}/lifecycle` · MCP tool `sequence_participant_lifecycle` · audience:write + audience:read · safe to repeat Pauses, resumes or stops one enrollment without reentry or replaying an unknown provider attempt. - `id` (path, string, required): The resource ID in the authenticated account - `enrollmentId` (path, string, required): The resource ID in the authenticated account - `requestId` (body, string, required): A stable UUID for this exact operation; reuse it only to retry the same payload - `expectedCursorVersion` (body, integer, required): The expected version returned by the latest read of this resource - `action` (body, "pause" | "resume" | "stop", required): The participant lifecycle transition; unknown outcomes cannot resume ```bash curl -X POST "http://localhost:3000/api/v1/sequences/6650f1c2e9b1d4a012345678/participants/6650f1c2e9b1d4a012345678/lifecycle" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"requestId":"…","expectedCursorVersion":"…","action":"…"}' ``` ### Groups On v2 every group belongs to one newsletter: list/create require newsletterId, and membership uses an existing Subscriber in that newsletter. Membership never grants consent. V1 account-wide groups remain available during maintenance. #### List groups `GET /api/v1/groups` · MCP tool `list_groups` · content:read · read-only, safe to repeat Lists organizational groups and member counts. On v2 newsletterId is required and only groups of that newsletter are returned. Membership does not grant permission to send email. - `newsletterId` (query, string): The owning newsletter ID; required for v2 group listing and creation - `archived` (query, boolean): List archived groups instead of active groups - `query` (query, string): Case-insensitive literal text in the group name - `page` (query, integer): Page number, starting at 1; 50 results per page ```bash curl "http://localhost:3000/api/v1/groups" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Get a group `GET /api/v1/groups/{id}` · MCP tool `get_group` · content:read · read-only, safe to repeat Reads one organizational group, including an archived group. On v2 the DTO includes its immutable newsletterId. - `id` (path, string, required): The group's ID ```bash curl "http://localhost:3000/api/v1/groups/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Create a group `POST /api/v1/groups` · MCP tool `create_group` · audience:write Creates an empty organizational group. On v2 newsletterId is required and cannot later change. Does not create contacts, consent or email. - `name` (body, string, required): The group name, 1 to 100 characters - `newsletterId` (body, string): The owning newsletter ID; required for v2 group listing and creation ```bash curl -X POST "http://localhost:3000/api/v1/groups" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"…"}' ``` #### Rename a group `PATCH /api/v1/groups/{id}` · MCP tool `update_group` · audience:write · safe to repeat Renames an active group without changing its v2 newsletter parent, membership or consent. - `id` (path, string, required): The group's ID - `name` (body, string, required): The group name, 1 to 100 characters ```bash curl -X PATCH "http://localhost:3000/api/v1/groups/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"…"}' ``` #### Archive a group `DELETE /api/v1/groups/{id}` · MCP tool `archive_group` · audience:write · safe to repeat Archives a group and retains its newsletter-specific members. Does not unsubscribe contacts or change campaigns. - `id` (path, string, required): The group's ID ```bash curl -X DELETE "http://localhost:3000/api/v1/groups/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### List group members `GET /api/v1/groups/{id}/members` · MCP tool `list_group_members` · audience:read · read-only, safe to repeat Lists group members, including unsubscribed contacts. On v2 each row has subscriberId instead of email and is scoped to the group's newsletter. This is not an eligible sending audience. - `id` (path, string, required): The group's ID - `page` (query, integer): Page number, starting at 1; 50 results per page ```bash curl "http://localhost:3000/api/v1/groups/6650f1c2e9b1d4a012345678/members" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Add a group member `POST /api/v1/groups/{id}/members` · MCP tool `add_group_member` · audience:write + audience:read + messages:send · safe to repeat Adds an existing profile to a group once, without changing consent or sending email. On v2 pass subscriberId (or a legacy email alias); the group determines its newsletter. - `id` (path, string, required): The group's ID - `email` (body, string): An existing contact email in this account; membership never grants consent - `subscriberId` (body, string): The v2 canonical subscriber ID; membership never grants consent ```bash curl -X POST "http://localhost:3000/api/v1/groups/6650f1c2e9b1d4a012345678/members" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Remove a group member `POST /api/v1/groups/{id}/members/remove` · MCP tool `remove_group_member` · audience:write + audience:read · safe to repeat Removes a profile from this newsletter's group without unsubscribing it. On v2 pass subscriberId or a legacy email alias. - `id` (path, string, required): The group's ID - `email` (body, string): An existing contact email in this account; membership never grants consent - `subscriberId` (body, string): The v2 canonical subscriber ID; membership never grants consent ```bash curl -X POST "http://localhost:3000/api/v1/groups/6650f1c2e9b1d4a012345678/members/remove" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` ### Signup forms #### List signup forms `GET /api/v1/forms` · MCP tool `list_forms` · content:read · read-only, safe to repeat Lists private form drafts and immutable publication summaries, without subscriber data. - `status` (query, "draft" | "published" | "archived"): Only forms with this status - `newsletterId` (query, string): Limit forms to their owning newsletter - `page` (query, integer): Page number, starting at 1; 50 results per page ```bash curl "http://localhost:3000/api/v1/forms" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Get a signup form `GET /api/v1/forms/{id}` · MCP tool `get_form` · content:read · read-only, safe to repeat Reads a private draft, its current published revision and publication readiness. - `id` (path, string, required): The signup form's ID ```bash curl "http://localhost:3000/api/v1/forms/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Create a signup form `POST /api/v1/forms` · MCP tool `create_form` · content:write Creates an inert private draft. Does not subscribe anyone or send email. - `name` (body, string, required): Private form name, 1 to 100 characters - `draft` (body, object): Optional initial publication fields; incomplete drafts are allowed - `draft.title` (body, string): Public form title; required before publication - `draft.consentText` (body, string): Exact plain-text consent wording approved by the operator - `draft.privacyUrl` (body, string): Real privacy policy URL; no fragments or credentials - `draft.groupId` (body, string): A group belonging to this account and, on v2 accounts, the selected newsletter - `draft.newsletterId` (body, string): The newsletter joined after double opt-in - `draft.allowedOrigins` (body, string[]): Exact HTTPS origins permitted to embed this form; maximum 20 ```bash curl -X POST "http://localhost:3000/api/v1/forms" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"…"}' ``` #### Edit a signup form `PATCH /api/v1/forms/{id}` · MCP tool `update_form` · content:write Updates private draft fields using editVersion. An existing public revision remains unchanged. - `id` (path, string, required): The signup form's ID - `expectedVersion` (body, integer, required): The editVersion returned by the last read; prevents overwriting concurrent edits - `name` (body, string): Private form name, 1 to 100 characters - `draft` (body, object): Merge these fields into the existing private draft - `draft.title` (body, string): Public form title; required before publication - `draft.consentText` (body, string): Exact plain-text consent wording approved by the operator - `draft.privacyUrl` (body, string): Real privacy policy URL; no fragments or credentials - `draft.groupId` (body, string): A group belonging to this account and, on v2 accounts, the selected newsletter - `draft.newsletterId` (body, string): The newsletter joined after double opt-in - `draft.allowedOrigins` (body, string[]): Exact HTTPS origins permitted to embed this form; maximum 20 ```bash curl -X PATCH "http://localhost:3000/api/v1/forms/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"expectedVersion":"…"}' ``` #### Publish a signup form `POST /api/v1/forms/{id}/publish` · MCP tool `publish_form` · settings:manage + messages:send · safe to repeat Publishes an immutable revision only after audience, delivery, sender and source readiness checks pass. Does not send email. - `id` (path, string, required): The signup form's ID - `expectedVersion` (body, integer, required): The editVersion returned by the last read; prevents overwriting concurrent edits ```bash curl -X POST "http://localhost:3000/api/v1/forms/6650f1c2e9b1d4a012345678/publish" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"expectedVersion":"…"}' ``` #### Archive a signup form `DELETE /api/v1/forms/{id}` · MCP tool `archive_form` · settings:manage · safe to repeat Stops new signup requests and pending confirmations for this form. Existing consent and memberships remain unchanged. - `id` (path, string, required): The signup form's ID ```bash curl -X DELETE "http://localhost:3000/api/v1/forms/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` ### Domains Own sending domains with their DNS records, and the shared address. #### List domains `GET /api/v1/domains` · MCP tool `list_domains` · content:read · read-only, safe to repeat Lists the account's own sending domains with their DNS records and status as last checked, and each newsletter's shared address (hello@.), which works with no setup. No parameters. ```bash curl "http://localhost:3000/api/v1/domains" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Add a domain `POST /api/v1/domains` · MCP tool `add_domain` · settings:manage + messages:send · reaches outside ApresMail Adds a domain to send from, in the account's own tenant at the email provider: emails are signed with DKIM and bounces return to a subdomain of it (rp. on PostShiba, bounce. on Amazon SES). The answer lists the DNS records to add; check_domain reads them once they're in. Newsletters can send from it once it's verified. - `domain` (body, string, required): e.g. example.com. An address or a link works too: jane@example.com and https://www.example.com/about both add example.com. - `archiveHost` (body, string): Used when the domain's setup lists an archive step: the subdomain for the archive, tracked links and unsubscribe links. Default: news.. ```bash curl -X POST "http://localhost:3000/api/v1/domains" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain":"example.com"}' ``` #### Retry a domain's sender setup and check its DNS records `POST /api/v1/domains/{id}/check` · MCP tool `check_domain` · settings:manage + messages:send · safe to repeat, reaches outside ApresMail Retries incomplete sender setup, then asks the email provider and DNS again whether the domain's records are in, and looks up its DMARC record or its parent domain's. On PostShiba, a check within a minute of the last one answers what that one found. When its setup lists an archive step, the newsletter sending from the domain moves its archive and links to that subdomain once it points at ApresMail. - `id` (path, string, required): The domain's ID ```bash curl -X POST "http://localhost:3000/api/v1/domains/6650f1c2e9b1d4a012345678/check" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Remove a domain `DELETE /api/v1/domains/{id}` · MCP tool `remove_domain` · settings:manage + messages:send · can't be undone, safe to repeat, reaches outside ApresMail Removes a domain from the email provider, and its archive subdomain from ApresMail when its setup lists one. Newsletters on it can't send until they move to another address. Refused while a campaign from it is sending, paused or scheduled. Adding it again needs new DNS records. - `id` (path, string, required): The domain's ID ```bash curl -X DELETE "http://localhost:3000/api/v1/domains/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` ### Images Pictures for campaign content, on ApresMail's CDN, kept in the account's library to use again. #### List images `GET /api/v1/images` · MCP tool `list_images` · content:read · read-only, safe to repeat Lists the images uploaded to the account, newest first, 50 a page, so a campaign can use one again without uploading it twice. Pass nextCursor back as cursor for the next page. - `limit` (query, integer): Images per page, 50 by default, 100 at most - `cursor` (query, string): nextCursor from the previous page ```bash curl "http://localhost:3000/api/v1/images?limit=20" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Upload an image `POST /api/v1/images` · MCP tool `upload_image` · content:write · reaches outside ApresMail Stores a PNG, JPEG, GIF or WebP image under 4 MB on ApresMail's CDN and returns its URL for campaign content. Send the file as base64 (under about 3 MB, since requests stop at 4.5 MB) or an https:// link ApresMail downloads. The URL never changes: sent emails keep pointing at it. list_images finds images uploaded before. - `base64` (body, string): The image file, base64 encoded; a data: URL works too - `url` (body, string): An https:// link to the image - `fileName` (body, string): Used in the stored file's name - `contentType` (body, string): The file's type, checked against its first bytes ```bash curl -X POST "http://localhost:3000/api/v1/images" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/cover.png"}' ``` ### API keys Keys for the REST API and MCP. #### List API keys `GET /api/v1/api-keys` · MCP tool `list_api_keys` · Dashboard or legacy full credentials only · read-only, safe to repeat Lists the account's API keys, revoked ones included, with their level and when they were last used. Keys themselves are never shown again. No parameters. ```bash curl "http://localhost:3000/api/v1/api-keys" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Create an API key `POST /api/v1/api-keys` · MCP tool `create_api_key` · Dashboard or legacy full credentials only Creates an API key and returns it once; only its hash is stored. Scoped keys have selected permissions and expire after 7, 30 or 90 days. Only dashboard sessions or legacy full credentials can manage keys. Legacy full/signups modes remain for compatibility. An account has up to 20 active keys: one more answers conflict until one is revoked. - `name` (body, string, required): What the key is for, e.g. Website signups - `level` (body, "full" | "signups" | "scoped"): scoped for selected permissions; full/signups preserve legacy integrations - `scopes` (body, "content:read" | "content:write" | "messages:send" | "audience:read" | "audience:write" | "settings:manage"[]): Explicit independent permissions; required for scoped keys - `expiresInDays` (body, number): Scoped key lifetime: 7, 30 or 90 days; default 30 ```bash curl -X POST "http://localhost:3000/api/v1/api-keys" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Draft assistant","level":"scoped","scopes":["content:read","content:write"],"expiresInDays":30}' ``` #### Revoke an API key `DELETE /api/v1/api-keys/{id}` · MCP tool `revoke_api_key` · Dashboard or legacy full credentials only · can't be undone, safe to repeat Revokes an API key for good: requests with it answer unauthorized from now on. A key can revoke itself. - `id` (path, string, required): The API key's ID ```bash curl -X DELETE "http://localhost:3000/api/v1/api-keys/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### List connected apps `GET /api/v1/connected-apps` · MCP tool `list_connected_apps` · Dashboard or legacy full credentials only · read-only, safe to repeat Lists the apps connected to the account with OAuth, such as ChatGPT, Claude and Grok Bot, with when they were connected and last used. Each app has its selected permissions; older connections may retain legacy full access. No parameters. ```bash curl "http://localhost:3000/api/v1/connected-apps" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ``` #### Disconnect an app `DELETE /api/v1/connected-apps/{id}` · MCP tool `revoke_connected_app` · Dashboard or legacy full credentials only · can't be undone, safe to repeat Disconnects an app connected with OAuth: its tokens stop working at once, and it has to ask for access again to come back. An app can disconnect itself. - `id` (path, string, required): The connected app's ID ```bash curl -X DELETE "http://localhost:3000/api/v1/connected-apps/6650f1c2e9b1d4a012345678" \ -H "Authorization: Bearer $APRESMAIL_API_KEY" ```