Mailprompt developer docs
The Mailprompt API is a server-to-server HTTP API for the same account data the app uses. It creates drafts and requests approvals; it never sends email or makes a flow live without a person.
The contract is an OpenAPI 3.1 document, served as JSON and rendered operation by operation in the API reference. If a behaviour is not in the document, it is not part of the contract.
Authentication
Create a key in Settings, then API keys. Send it as a Bearer token on every request:
curl https://api.mailprompt.ai/v1/account \
-H 'Authorization: Bearer mp_live_your_key'Live keys start with mp_live_; test keys start with mp_test_. The secret is shown once and stored as a SHA-256 hash with a short prefix and the last four characters. A key sent in the query string is rejected with 400 key_in_query rather than accepted.
There is no CORS. A browser snippet should post to the tracking endpoint with a public identifier, never with an account API key.
Scopes
Each endpoint requires a scope. A key with the wrong scope gets 403, not a silent downgrade.
| Scope | Allows |
|---|---|
profiles:read | Read profiles, lists and consent state. |
profiles:write | Create and update profiles and list memberships. |
events:write | Send tracking events. Separate, higher rate-limit bucket. |
lists:write | Create lists and add or remove members. |
flows:read | Read flow definitions, status and results. |
flows:write | Create draft flows and update steps. Going live still needs approval. |
campaigns:read | Read campaigns, schedules and results. |
campaigns:write | Create draft campaigns and request approval. |
results:read | Read revenue, attribution and deliverability reporting. |
webhooks:read | Read webhook endpoints and their delivery log. |
webhooks:write | Create, update, rotate and delete webhook endpoints; send test events. |
Test mode
An mp_test_ key hits a synthetic demo store. Reads return example data, writes are accepted and discarded, nothing sends and no credits are charged. Every response carries X-Mailprompt-Test-Mode: true. Use it to check a mapping before a real run.
Errors
Every error uses one envelope: { error: { code, message, request_id } }. Quote the request id in support requests. Validation failures add error.fields; a failed pre-send checklist adds error.details.checks.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed request, invalid cursor, oversized body. |
| 400 | key_in_query | The API key was sent in the query string. Use the Authorization header. |
| 401 | unauthorized | Missing, malformed, unknown, revoked or expired key. |
| 403 | forbidden | Missing scope, IP not allowed, or the plan is read-only. |
| 404 | not_found | The resource does not exist in this account. Cross-store ids return 404. |
| 409 | conflict | State conflict, concurrent duplicate, or an approval is required or already granted. |
| 409 | idempotency_mismatch | The same Idempotency-Key was used with a different body. |
| 422 | validation_failed | Field validation failed. The key is still valid. |
| 402 | payment_required | Not enough credits for this billable write. |
| 409 | approval_required | The request needs a human approval before it can proceed. |
| 429 | rate_limited | Rate limit or monthly quota exceeded. Read Retry-After. |
| 429 | quota_exceeded | The monthly call allowance is used up. Read Retry-After. |
| 500 | internal | Unexpected error. No stack trace or internal message is returned. |
Rate limits and quotas
The per-key token bucket is sized by plan. Heavy endpoints consume five tokens; the events endpoint has its own bucket at ten times the plan rate. Responses include X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and, on a 429, Retry-After.
| Plan | Requests / minute | Calls / month | Writes |
|---|---|---|---|
| Starter | 60 | 10,000 | Read-only |
| Growth | 120 | 100,000 | Allowed |
| Pro | 300 | 500,000 | Allowed |
| Agency | 600 | 2,000,000 | Allowed |
Twenty failed authentication attempts from one IP in a minute block that IP for five minutes before the key lookup. A monthly allowance is separate from the per-minute bucket and returns a quota error.
Pagination
Lists are cursor-paginated. Pass limit (default 50, maximum 500) and the opaque cursor from the previous response, then read next_cursor. A null cursor means the last page. The SDK's list() iterates pages for you and caps the iteration at 1,000 pages.
Idempotency
Send an Idempotency-Key (up to 100 characters) on a write. The first response is stored for 24 hours and replayed for the same method, path and body. A different body with the same key returns 409 idempotency_mismatch; a concurrent duplicate returns 409 conflict. The SDK adds a key to every POST automatically.
Webhooks
Create and manage endpoints with the webhooks:write scope; reads use webhooks:read. The same controls live in the app under Settings → Webhooks.
Registering endpoints
POST /v1/webhooks takes url, events and an optional description. The URL must be https and resolve only to public addresses; the same SSRF check runs before every delivery. An account can have at most 20 endpoints. The response contains the signing secret exactly once — store it before leaving the page. Use GET /v1/webhooks to list, GET /v1/webhooks/{id} to read one, PATCH /v1/webhooks/{id} to change the URL, events or enabled, and DELETE /v1/webhooks/{id} to stop deliveries and remove the log.
POST /v1/webhooks/{id}/rotate-secret returns a new signing secret once. GET /v1/webhooks/{id}/deliveries pages the last 30 days of attempts, each with the event, status, HTTP status, attempt number, duration and time. POST /v1/webhooks/{id}/test queues a signed webhook.test event that is not subscribable, and POST /v1/webhooks/deliveries/{id}/redeliver sends the same event id and payload again.
curl -X POST https://api.mailprompt.ai/v1/webhooks \
-H 'Authorization: Bearer mp_live_your_key' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://example.com/mailprompt/webhooks",
"events": ["message.sent", "message.clicked"],
"description": "Production order sync"
}'Each delivery is a POST with Mailprompt-Event, Mailprompt-Delivery and a Mailprompt-Signature header. The payload is:
{
"id": "evt_…",
"type": "message.clicked",
"created_at": 1762948800000,
"account_id": "acct_…",
"data": { }
}id is stable across retries and manual redeliveries; deduplicate on it. Requests time out after 10 seconds and redirects are not followed. Registration and every delivery run an SSRF check.
Event types
| Event | Sent when |
|---|---|
profile.subscribed | A profile confirmed or was marked subscribed. |
profile.unsubscribed | A profile unsubscribed through a link or the API. |
message.sent | A message was accepted for delivery. |
message.bounced | A message hard or soft bounced. |
message.complained | A recipient marked the message as spam. |
message.clicked | A tracked link was clicked. |
order.attributed | An order was attributed to a message within the click window. |
flow.approved | A flow version was approved by a person. |
campaign.sent | A campaign finished sending. |
approval.requested | A draft is waiting for a human approval. |
credits.low | The account balance crossed the low-credits threshold. |
A webhook.test event is produced only by the test-event button and is not subscribable. Failed deliveries retry after 1m, 5m, 30m, 2h, 6h, 12h, 24h. After 20 consecutive failures the endpoint is disabled and the owner is emailed. Deliveries are kept for 30 days.
Signature verification
The header is t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "t.body")>. Verify against the raw request bytes, compare in constant time, and reject a timestamp older than five minutes. The SDK exports verifyWebhookSignature for Node.
Node
import { verifyWebhookSignature } from '@mailprompt/sdk';
// Express: keep the raw body. Re-serialising JSON changes the bytes.
app.post('/webhooks/mailprompt', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verifyWebhookSignature(
req.body,
req.get('Mailprompt-Signature') ?? '',
process.env.MAILPROMPT_WEBHOOK_SECRET!,
);
if (!ok) return res.status(400).send('bad signature');
const event = JSON.parse(req.body.toString('utf8'));
// Deduplicate on event.id: retries reuse the same id.
res.status(200).send('ok');
});Python
import hashlib
import hmac
import time
def mailprompt_verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = {}
for item in header.split(","):
key, _, value = item.strip().partition("=")
parts[key] = value
timestamp = int(parts.get("t", "0") or 0)
if abs(time.time() - timestamp) > tolerance:
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))PHP
<?php
function mailprompt_verify(string $secret, string $header, string $body, int $tolerance = 300): bool {
$parts = [];
foreach (explode(',', $header) as $part) {
[$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
$parts[$key] = $value;
}
$timestamp = (int) ($parts['t'] ?? 0);
if (abs(time() - $timestamp) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $body, $secret);
return hash_equals($expected, $parts['v1'] ?? '');
}SDK quickstart
The TypeScript SDK is generated from the same OpenAPI document. It handles retries, honours Retry-After, sends an idempotency key on writes and exposes cursor pagination as an async iterator.
pnpm add @mailprompt/sdkimport { Mailprompt, MailpromptError } from '@mailprompt/sdk';
const mp = new Mailprompt({ apiKey: process.env.MAILPROMPT_API_KEY! });
// Auto-paginates; bounded to 1,000 pages by default.
for await (const profile of mp.profiles.list({ limit: 100 })) {
console.log(profile.id, profile.email);
}
// A draft flow. The API never makes it live.
const flow = await mp.flows.create({
name: 'Welcome series',
definition: {
name: 'Welcome series',
trigger: { metric: 'Subscribed to List' },
audience: 'subscribed',
steps: [{ wait: '1h' }],
},
});
// Creates a pending Approval. A human approves it in the app.
const approval = await mp.flows.requestApproval(flow.id);
try {
await mp.profiles.get('missing_id');
} catch (error) {
if (error instanceof MailpromptError) {
console.error(error.status, error.code, error.requestId);
}
}The client retries 429 and 5xx responses up to three times with a 30-second per-attempt timeout. Configure with maxRetries, timeoutMs, or a custom fetch.
MCP server
The same tools are available to an MCP client through a hosted connector or a local stdio package. The MCP setup guide has the connector URL, the config snippets and the full tool list. Approvals stay human there too.
Versioning
This document describes API version 1. Additive fields are not breaking; a removed field or a changed response shape is. The OpenAPI document is the changelog: compare /openapi.json between deployments if you need to diff a change.