# Parlo — Full API Reference > Developer email API for transactional email, built on AWS SES. This is the > complete, self-contained reference for LLMs and agents. Machine-readable spec: > https://getparlo.io/openapi.yaml Base URL: https://api.getparlo.io/v1 Auth: HTTP bearer token — send `Authorization: Bearer parlo_live_xxx` on every request. Content-Type: application/json. Scope: transactional email only. Marketing (audiences, templates, broadcasts) is designed and sent from the Parlo dashboard, not this API. ## Objects ### Email - id (string) — object id (UUID) - to (string[]) - from (string) - subject (string) - status (string) — one of: queued, sent, delivered, bounced, complained, failed - opened (boolean) - clicked (boolean) - last_event_at (string, ISO 8601, nullable) - created_at (string, ISO 8601, nullable) GET /v1/emails/:id and webhook payloads carry three further fields, which the list endpoint omits: - stream (string) — always "transactional" - tags (object of string→string) — the tags you sent on the send - error (string, nullable) — why a failed send failed Message bodies are never stored, so no endpoint returns one. ### Domain - id (string) — object id (UUID) - name (string) - status (string) — one of: pending, verified, failed - verified_at (string, ISO 8601, nullable) - created_at (string, ISO 8601, nullable) - records (DnsRecord[]) — present on create/show/verify: DNS to add - auth (object) — present on create/show/verify: advisory auth-health { dkim:{status}, spf:{status}, dmarc:{status, policy}, checked_at } ### DnsRecord - type (string) — CNAME | TXT - name (string) - value (string) - required (boolean) — true for the DKIM CNAMEs needed to verify; false for recommended SPF/DMARC ## Endpoints ### POST /v1/emails — send a transactional email Request body: - from (string, required) — sender address; its domain must be a verified sending domain - to (string | string[], required) - subject (string, required) - html (string, optional) — provide at least one of html or text - text (string, optional) - reply_to (string, optional) - cc (string | string[], optional) - bcc (string | string[], optional) - data (object, optional) — values substituted into [placeholder] tokens in subject/html/text - tags (object of string→string, optional) Optional header: Idempotency-Key — a repeated key returns the original email instead of sending again. Response 202: { "id": "", "status": "queued" }. Poll GET /v1/emails/:id for the outcome. Example: ``` curl https://api.getparlo.io/v1/emails \ -H "Authorization: Bearer parlo_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "from": "you@yourdomain.com", "to": "user@example.com", "subject": "Welcome, [name]", "html": "

Hi [name], thanks for joining.

", "data": { "name": "Alice" } }' ``` ### GET /v1/emails/:id — retrieve an email Response 200: an Email object. 404 { "message": "Email not found", "code": "email_not_found" } if it doesn't exist for your company. ### GET /v1/emails — list sent emails Response 200: { "emails": [ Email (list fields) ], "meta": { "current_page", "next_page", "prev_page", "total_pages", "total_count" } } Newest first. Filters combine with AND: - status=bounced — one status, or a comma-separated list (bounced,failed) - to=user@example.com — matches any recipient; case-insensitive substring - since=2026-08-01 — strict ISO 8601 date or timestamp - until=2026-08-31 — same; a bare date covers that whole day - tags[type]=receipt — repeat for more keys, all must match; max 10 - search_term=invoice — substring match on subject - page=1&per_page=25 — per_page defaults to 25, maximum 100 A malformed filter is a 422, never a silently unfiltered result. Codes: status/invalid, since/invalid, until/invalid, tags/too_many. Example: ``` curl "https://api.getparlo.io/v1/emails?status=bounced,failed&since=2026-08-01&tags[env]=prod" \ -H "Authorization: Bearer parlo_live_xxx" ``` ### GET /v1/domains — list sending domains Response 200: { "domains": [ Domain (summary fields) ] } ### POST /v1/domains — add a sending domain Request body: { "name": "yourdomain.com" } (required) Response 201: a Domain object including `records` (3 DKIM CNAMEs + recommended SPF & DMARC) and `auth`. Add the records at your DNS provider, then call verify. 422 codes: invalid (not a valid domain name), already_registered. ### GET /v1/domains/:id — retrieve a domain Response 200: a Domain object (with `records` + `auth`). 404 code "domain_not_found". ### POST /v1/domains/:id/verify — re-check a domain's DNS Re-reads verification status from the provider and refreshes the SPF/DMARC auth-health. Response 200: the updated Domain object. ### DELETE /v1/domains/:id — remove a domain Response 200: { "id": "", "deleted": true } ## Webhooks Instead of polling GET /v1/emails/:id, configure an endpoint and Parlo POSTs each event to it. Configuration is dashboard-only (Settings -> Webhooks), not through this API: an API key that could rewrite the endpoint would let a leaked key reroute the whole event stream. The endpoint must be HTTPS and publicly reachable. Events: email.delivered, email.bounced, email.complained, email.opened, email.clicked, email.failed. opened/clicked fire on the first open or click only. Marketing sends never produce webhooks. The Settings test button sends `webhook.test`, which is deliberately not one of the six, so a test can never be mistaken for a real delivery. Payload — `data` is exactly what GET /v1/emails/:id returns: ``` { "id": "", "type": "email.delivered", "created_at": "2026-08-10T12:34:56Z", "data": { "id": "", "to": ["user@example.com"], "from": "you@yourdomain.com", "subject": "Welcome, Alice", "status": "delivered", "opened": false, "clicked": false, "last_event_at": "...", "created_at": "...", "stream": "transactional", "tags": {"env": "prod"}, "error": null } } ``` Signature: every request carries `Parlo-Signature: t=,v1=`, where v1 is the HMAC-SHA256 of the string "{t}.{raw request body}" keyed with your signing secret. Sign the raw body as received — re-serializing the JSON changes the bytes and every signature then fails. Compare in constant time, and reject a t far from your own clock: the timestamp is inside the signed string, so a captured payload cannot be replayed under a fresh t. Delivery: at-least-once and possibly out of order. Answer 2xx within 10s (5s to connect). 408, 429, 5xx and network or TLS errors retry with backoff, up to 5 attempts; every other 4xx and any 3xx is permanent and is not retried. Redirects are never followed. The event id is stable across retries — dedupe on it, and trust data.status over arrival order. ## Errors All errors are JSON: { "message": string, "code": string } - 401 unauthorized — missing or invalid API key - 404 email_not_found | domain_not_found - 422 validation — code is one of: invalid, blank, too_many, domain_not_verified, recipient_suppressed, quota_exceeded, already_registered - 502 provider_error — upstream email provider (SES) error ## Tooling - OpenAPI 3.1 spec: https://getparlo.io/openapi.yaml - MCP server (for AI agents): `npx -y parlo-mcp` — e.g. `claude mcp add parlo --env PARLO_API_KEY=parlo_live_xxx -- npx -y parlo-mcp`. Tools: send_email, get_email, list_domains, create_domain, get_domain, verify_domain, delete_domain. ## Links - Docs: https://getparlo.io/docs - Pricing: https://getparlo.io/pricing - Sign up: https://app.getparlo.io/register