Skip to content

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.

ScopeAllows
profiles:readRead profiles, lists and consent state.
profiles:writeCreate and update profiles and list memberships.
events:writeSend tracking events. Separate, higher rate-limit bucket.
lists:writeCreate lists and add or remove members.
flows:readRead flow definitions, status and results.
flows:writeCreate draft flows and update steps. Going live still needs approval.
campaigns:readRead campaigns, schedules and results.
campaigns:writeCreate draft campaigns and request approval.
results:readRead revenue, attribution and deliverability reporting.
webhooks:readRead webhook endpoints and their delivery log.
webhooks:writeCreate, 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.

HTTPCodeMeaning
400bad_requestMalformed request, invalid cursor, oversized body.
400key_in_queryThe API key was sent in the query string. Use the Authorization header.
401unauthorizedMissing, malformed, unknown, revoked or expired key.
403forbiddenMissing scope, IP not allowed, or the plan is read-only.
404not_foundThe resource does not exist in this account. Cross-store ids return 404.
409conflictState conflict, concurrent duplicate, or an approval is required or already granted.
409idempotency_mismatchThe same Idempotency-Key was used with a different body.
422validation_failedField validation failed. The key is still valid.
402payment_requiredNot enough credits for this billable write.
409approval_requiredThe request needs a human approval before it can proceed.
429rate_limitedRate limit or monthly quota exceeded. Read Retry-After.
429quota_exceededThe monthly call allowance is used up. Read Retry-After.
500internalUnexpected 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.

PlanRequests / minuteCalls / monthWrites
Starter6010,000Read-only
Growth120100,000Allowed
Pro300500,000Allowed
Agency6002,000,000Allowed

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

EventSent when
profile.subscribedA profile confirmed or was marked subscribed.
profile.unsubscribedA profile unsubscribed through a link or the API.
message.sentA message was accepted for delivery.
message.bouncedA message hard or soft bounced.
message.complainedA recipient marked the message as spam.
message.clickedA tracked link was clicked.
order.attributedAn order was attributed to a message within the click window.
flow.approvedA flow version was approved by a person.
campaign.sentA campaign finished sending.
approval.requestedA draft is waiting for a human approval.
credits.lowThe 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/sdk
import { 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.