API reference

Every endpoint, every operation, and the conventions that hold across all of them.

Base URL https://emails.sh/v1. Every request carries Authorization: Bearer esh_..., every body is JSON, and every response is JSON. There is no versioning header: /v1 is the version, and a breaking change would be /v2.

ConventionWhat it means
Content-Typeapplication/json on anything with a body. A form encoding is rejected.
TimestampsRFC 3339 with a Z offset, always UTC.
IdsPrefixed and opaque: em_ for an email, dom_ for a domain, whd_ for a webhook delivery. Do not parse them.
ErrorsTwo shapes. Most routes answer { error: { code, message, next } }; topics, suppressions, templates, the contacts collection, and the rate limiter answer { error: "code", message?, hint? }. Parse both. See /docs/errors.
Rate limits600 requests a minute per key, which is 10 a second, and 30 a minute per IP with no key. Over it, 429 with retry-after in seconds.
Paginationlimit and before on list endpoints. Newest first unless the endpoint says otherwise.

Emails

POST /v1/emails

Send one email.

POST /v1/emails/batch

Send up to 100 in one request.

GET /v1/emails/:id

Status and delivery events for one email.

PATCH /v1/emails/:id

{ send_at } moves a booked send to a new time. It keeps its id. 409 too_late_to_reschedule once it has gone.

GET /v1/emails

The delivery log. limit defaults to 25 and tops out at 100.

POST /v1/emails/:id/cancel

Call off an email booked with send_at, before it goes.

DELETE /v1/messages/scheduled/:id

The same cancel, by the older spelling. Still works.

emails.send

Send one email. The endpoint every integration starts with, and the only one many ever use. 200 when the status is queued, 202 when it is scheduled.

Arguments

from string required
Sender, as an address or as "Name <address>". The domain must be verified on this workspace, or be onboarding@emails.sh while you are testing.
to string[] required
Recipients. Up to 50 addresses counted across to, cc, and bcc together.
subject string required
html string
HTML body. Give html, text, or both.
text string
Plain-text body. Sent as the alternative part when html is present.
cc string[]
bcc string[]
reply_to string | string[]
Where replies go, if not the from address.
headers Record<string, string>
Extra headers, for example List-Unsubscribe.
attachments { filename, content_base64, content_type? }[]
Base64 bytes with no data: prefix. 40 MB total per message, counted after base64 encoding.
tags Record<string, string>
Labels stored with the email and echoed on every webhook for it.
send_at string
When to send it, up to 30 days out. An ISO 8601 timestamp such as 2026-08-04T09:00:00Z, a relative offset such as "in 1 min", or a clock time such as "tomorrow at 9am", read as UTC. Omit to send now.
idempotency_key string
Your own id for this send. A repeat within 24 hours returns the first result instead of sending again.

Returns{ id, status: "queued" | "scheduled" }

emails.batch

Send up to 100 emails in one request. Each entry succeeds or fails on its own, and the response keeps the order you sent them in.

Arguments

emails Send[] required
Up to 100 send bodies, each exactly as emails.send takes one.

Returns{ data: ({ id, status } | { id: null, status: "failed", error })[] }

emails.get

Status and delivery events for one email: when it was accepted, when the receiving server took it, and the bounce or complaint if there was one.

Arguments

id string required
The id a send returned.

Returns{ id, status, to, subject, created_at, events[] }

Domains

GET /v1/domains

Domains, with the records a pending one still needs.

POST /v1/domains

{ domain } returns every DNS record to publish.

GET /v1/domains/:id

One domain, with the records it still needs if it is pending.

POST /v1/domains/:id/verify

Check the records now and report which are missing.

PATCH /v1/domains/:id

{ tracking_host } sets the hostname in front of tracked links. null goes back to the shared one.

DELETE /v1/domains/:id

Remove a domain. DELETE /v1/domains?id= is the older spelling and still works.

domains.list

Every sending domain on the workspace, with the DNS records a pending one still needs.

Takes no arguments.

Returns{ domains: Domain[] }

domains.create

Add a sending domain. The response carries every DNS record to publish, so setup can finish without opening the dashboard.

Arguments

domain string required
A domain or subdomain you control, for example mail.acme.com.

Returns{ id, domain, verification_status, records[] }

domains.get

One domain, in the shape the list gives it, with the DNS records still to publish if it is pending. An id from another workspace reads as missing.

Arguments

id string required
The id the create response returned.

ReturnsDomain

domains.verify

Check the records now rather than waiting for the nightly pass. Safe to call repeatedly, and the answer says which records are still missing.

Arguments

id string required

Returns{ domain, verified, records: (Record & { found })[] }

domains.delete

Remove a domain. Anything still sending from it starts failing, so move senders first.

Arguments

id string required

Returns{ deleted: id }

API keys

GET /v1/api-keys

Keys on the workspace. Values are never listed.

POST /v1/api-keys

{ name, scopes? } returns the key once.

DELETE /v1/api-keys/:id

Revoke a key, effective on the next request.

api_keys.list

Keys on the workspace, with the last time each was used. Values are never listed.

Takes no arguments.

Returns{ api_keys: ApiKey[] }

api_keys.create

Create a key. The value is returned once and never again.

Arguments

name string required
What it is for, so a later reader can revoke the right one.
scopes string[]
Defaults to full access. Give ["mail:send"] to a key that only sends.

Returns{ id, name, key }

api_keys.revoke

Revoke a key. It stops working on the next request, with no grace period.

Arguments

id string required

Returns{ deleted: id }

Webhooks

GET /v1/webhooks

Endpoints. Secrets are not listed.

POST /v1/webhooks

{ url, events?, headers? } returns the signing secret once.

GET /v1/webhooks/:id

One endpoint. The secret is never in a read.

PATCH /v1/webhooks/:id

Change an endpoint. rotate_secret returns a new secret once. PATCH /v1/webhooks?id= is the older spelling and still works.

DELETE /v1/webhooks/:id

Remove an endpoint. DELETE /v1/webhooks?id= is the older spelling and still works.

GET /v1/webhooks/deliveries

What each attempt got back. webhook_id and limit narrow it.

POST /v1/webhooks/deliveries

{ webhook_id } sends a test; { delivery_id } replays a stored one.

webhooks.list

Registered endpoints and the events each is subscribed to. Signing secrets are not listed.

Takes no arguments.

Returns{ webhooks: Webhook[] }

webhooks.create

Register an endpoint. The signing secret comes back once, in this response.

Arguments

url string required
An https endpoint of yours.
events string[]
Defaults to email.delivered, email.bounced, and email.complained.
headers Record<string, string>
Sent with every delivery, for a gateway that wants its own token.

Returns{ id, url, events, secret }

webhooks.get

One endpoint, with its events, whether it is active, and its failure counters. The signing secret is not in a read, at any scope.

Arguments

id string required

ReturnsWebhook

webhooks.update

Change an endpoint in place. Fields you leave out are left alone.

Arguments

id string required
url string
A new https endpoint. Fixing a typo here keeps the id and the secret.
events string[]
Replaces the subscription wholesale rather than adding to it.
active boolean
Turning it back on clears the failure count, so it does not trip on the next miss.
headers Record<string, string>
rotate_secret boolean
Mints a new signing secret and returns it once. The old one keeps signing for a grace window, so the receiver can be redeployed without dropping events.

ReturnsWebhook, plus secret and previous_secret_valid_until when rotate_secret was set

webhooks.deliveries

What each attempt got back: status code, response body, and duration. This is how you tell a broken endpoint from a missing event.

Arguments

webhook_id string
limit number
Defaults to 25.

Returns{ deliveries: Delivery[] }

webhooks.delete

Remove an endpoint. Queued deliveries for it are dropped.

Arguments

id string required

Returns{ deleted: id }

Audiences

GET /v1/audiences

Audiences on the workspace.

POST /v1/audiences

{ name, description? } creates one.

GET /v1/audiences/:id

One audience, with its double opt-in setting and where that setting came from.

PATCH /v1/audiences/:id

{ name?, description?, require_double_opt_in?, confirmation_subject?, confirmation_body? }

DELETE /v1/audiences/:id

Soft delete an audience.

GET /v1/audiences/:id/contacts

Members. ?status=&limit=&offset=. id on each row is the membership id.

POST /v1/audiences/:id/contacts

Bulk import of { email, attributes?, status?, tags? } rows, as JSON or CSV. ?dry_run=true reports without writing.

GET /v1/audiences/:id/contacts/:member

One membership, by membership id.

PATCH /v1/audiences/:id/contacts/:member

{ attributes?, subscribed?, status? } on one membership.

DELETE /v1/audiences/:id/contacts/:member

Remove one membership. It records no unsubscribe.

POST /v1/audiences/:id/contacts/:member/confirm

Send the double opt-in confirmation email. Nothing else ever sends it.

audiences.list

Audiences on the workspace. An audience is a named list of contacts with their subscription state.

Takes no arguments.

Returns{ audiences: Audience[] }

audiences.create

Create an audience.

Arguments

name string required

Returns{ id, name }

audiences.members

Members of one audience. Each row carries a membership id, which is the id every other member route takes, and a contact_id, which is the workspace-level contact behind it.

Arguments

audience_id string required
status string
subscribed, unsubscribed, pending, or cleaned.
limit number
Defaults to 100, maximum 1000.
offset number

Returns{ contact_count, subscribed_count, contacts: Member[] }

audiences.import

Bulk import into an audience. Up to 10000 rows and 8 MB per call. An address already on the suppression list lands as cleaned rather than subscribed and is counted in held_back.

Arguments

audience_id string required
contacts { email, attributes?, status?, tags? }[] required
Rows to import. A bare array, { contacts: [] }, { data: [] }, or one object all work, and CSV works when the Content-Type says csv.
dry_run boolean
Query parameter. Reports what would happen and writes nothing.

Returns{ created, updated, unchanged, duplicates, tags_added, imported, held_back, skipped, held[], errors[] }

audiences.updateMember

Change one membership. Recording an unsubscribe here is what keeps the address from being mailed by the next broadcast.

Arguments

audience_id string required
member string required
The membership id from audiences.members. Not a contact id and not an email address.
attributes Record<string, string>
Per-list merge fields for this membership.
subscribed boolean
Wins over status when both are given.
status string
subscribed, unsubscribed, pending, or cleaned.

ReturnsMember

audiences.removeMember

Remove one membership from an audience. It does not delete the contact, and it does not record an unsubscribe: set status to unsubscribed for that.

Arguments

audience_id string required
member string required
The membership id.

Returns{ deleted: id }

Broadcasts

GET /v1/broadcasts

Broadcasts, newest first. limit defaults to 50 and tops out at 100.

POST /v1/broadcasts

Create a draft. from is the one required field.

GET /v1/broadcasts/:id

One broadcast with its body, its stats, and its problems[].

PATCH /v1/broadcasts/:id

Edit a draft. from is not patchable, and a broadcast past draft answers 409.

DELETE /v1/broadcasts/:id

Cancel it.

POST /v1/broadcasts/:id/cancel

The same cancel, as a POST.

POST /v1/broadcasts/:id/send

{ scheduled_at? }. Without it, it goes now.

POST /v1/broadcasts/:id/test

{ to } sends it to up to 5 addresses of yours.

GET /v1/broadcasts/:id/preview

?email= renders it without sending, and lists the merge fields.

POST /v1/broadcasts/preview

The same render for a body you have not saved. Nothing is written.

GET /v1/broadcasts/:id/recipients

?status=&limit=&offset= over per-recipient results.

broadcasts.create

Create a broadcast as a draft. The response carries ready and problems[], so you can tell whether it can send yet without trying.

Arguments

from string required
Sender, as an address or as "Name <address>". The domain must be verified on this workspace, or be onboarding@emails.sh while you are testing.
audience_id string
Who it goes to. Required before it can send.
segment_id string
Narrow the audience to the members a segment matches.
topic_id string
The topic the send is filed under, which is what gives it a working one-click opt-out.
subject string
name string
What it is called in the dashboard. Not seen by a recipient.
html string
text string
template_id string
Use a stored template instead of an inline body.
template_version_id string
reply_to string
track_opens boolean
track_clicks boolean

Returns{ id, status: "draft", ready, problems[] }

broadcasts.send

Send a draft, or book it. Sending now answers with the recipient count and how many were skipped; booking answers with the time it will go.

Arguments

id string required
scheduled_at string
RFC 3339, strictly in the future and at most 30 days out. Omit to send now.

Returns{ id, status, queued, batches, skipped, recipients } or { id, status, scheduled_at }

broadcasts.test

Send the broadcast to yourself first, rendered exactly as a recipient would get it. It does not change the draft status.

Arguments

id string required
to string | string[] required
Up to 5 addresses.

Returns{ sent, skipped }

broadcasts.preview

The rendered subject and body without sending anything, plus merge_fields saying which fields the audience actually supplies and which are missing.

Arguments

id string required
email string
Render for one member of the audience rather than for a sample.

Returns{ subject, html, text, merge_fields[], audience_sample_size }

broadcasts.previewContent

The same render as broadcasts.preview, for a body you have not saved. Nothing is written and nothing is sent, so id comes back null.

Arguments

subject string required
html string
One of html or text is required.
text string
from string
Shown back on the preview. It is not resolved against your domains here.
replyTo string
audienceId string
Render against a real member of this audience. Leave it out and the recipient is a placeholder with no attributes.
email string
Which member of audienceId to render for.

Returns{ id: null, subject, html, text, headers, merge_fields[], audience_sample_size }

broadcasts.recipients

Per-recipient results for one broadcast, with the reason a skipped or failed row did not go.

Arguments

id string required
status string
pending, sent, delivered, bounced, complained, failed, or skipped.
limit number
Defaults to 100, maximum 500.
offset number

Returns{ stats, recipients: Recipient[] }

broadcasts.cancel

Call off a scheduled or sending broadcast. Messages already handed to the mail servers have gone.

Arguments

id string required

Returns{ id, status: "cancelled" }

Segments

GET /v1/segments

Segments. ?audience_id= narrows to one audience.

POST /v1/segments

{ name, audience_id?, match?, rules? } creates one.

GET /v1/segments/:id

?count=live recomputes member_count instead of reading the cached one.

PATCH /v1/segments/:id

Rules are replaced wholesale, never merged.

DELETE /v1/segments/:id

Remove a segment. Contacts are untouched.

GET /v1/segments/:id/members

?mailable=true&limit=&after= over who it matches now.

segments.create

A saved filter over contacts. Membership is computed when it is read rather than stored, so a segment is never stale.

Arguments

name string required
audience_id string
Leave it out for a workspace-wide segment. status and joined rules need one.
match "all" | "any"
Defaults to all.
rules Rule[]
Up to 20, and never nested.
description string

Returns{ id, name, describes, member_count }

segments.get

One segment, its rules, and the sentence describing them.

Arguments

id string required
count string
Pass live to recompute member_count now instead of reading the cached one.

ReturnsSegment

segments.update

Change a segment. Send the whole rule list every time, including the rules you are keeping.

Arguments

id string required
match "all" | "any"
rules Rule[]
Replaced wholesale. Rules are never merged into what is there.

ReturnsSegment

segments.members

Who a segment currently matches, as a cursor-paged list.

Arguments

id string required
mailable boolean
Only members who are subscribed and not suppressed.
limit number
Defaults to 100, maximum 1000.
after string
The next_after cursor from the previous page.

Returns{ segment_id, total, next_after, members[] }

Contacts

GET /v1/contacts

?q=&lookup=&limit=. Accept: text/vcard returns a .vcf instead of JSON.

POST /v1/contacts

Field mode, or { vcard } for up to 1000 cards at once.

GET /v1/contacts/:id

One contact.

PATCH /v1/contacts/:id

Change a contact.

DELETE /v1/contacts/:id

Remove a contact.

GET /v1/contacts/duplicates

Likely duplicate pairs. POST merges { survivor_id, loser_id }.

GET /v1/contacts/:id/tags

Tags on a contact.

POST /v1/contacts/:id/tags

{ tags } or { tag }. Each real change queues a tag.added automation event.

DELETE /v1/contacts/:id/tags

?tag=vip removes one, and queues tag.removed.

GET /v1/contacts/:id/attributes

Workspace-level attributes on a contact.

PATCH /v1/contacts/:id/attributes

Merge patch. null clears one name.

GET /v1/contacts/:id/subscriptions

Every address, whether it is suppressed, and what it is subscribed to.

contacts.tags

Read or add tags on a contact. Tags are 1 to 64 characters, hold no comma or newline, are compared without case, and are stored lowercased.

Arguments

id string required
The contact id.
tags string[]
On the POST. A single tag may be given as tag instead.

Returns{ tags, added }

contacts.attributes

Workspace-level facts about a contact, readable by every segment and automation. These are not the per-list merge fields a broadcast substitutes.

Arguments

id string required
attributes Record<string, string | null>
A merge patch: names you send are written, names you leave out are untouched, and null clears one. 1 to 100 names per call.

Returns{ attributes, changed[] }

contacts.subscriptions

Every address on a contact, whether it is suppressed, and the audiences and topics it is subscribed to. The one call that answers "will this person receive anything".

Arguments

id string required

Returns{ contact_id, subscriptions[] }

Templates

GET /v1/templates

Templates on the workspace.

POST /v1/templates

{ name, slug?, subject?, html?, text?, variables?, publish? }

GET /v1/templates/:id

One template with every version.

PATCH /v1/templates/:id

{ name?, slug?, description? } only. Content is never edited here.

DELETE /v1/templates/:id

Soft delete.

GET /v1/templates/:id/versions

Versions and which one is published.

POST /v1/templates/:id/versions

Write a draft version. It never publishes.

POST /v1/templates/:id/publish

{ version_id } or { version }, or neither to publish the latest.

POST /v1/templates/:id/render

Strict render of the published version, exactly as a send does it.

POST /v1/templates/:id/preview

Lenient render of any version, with markup warnings.

templates.create

Create a template and its first version.

Arguments

name string required
slug string
What your code names. Derived from the name when you leave it out.
subject string
html string
text string
variables (string | { name, default?, required? })[]
publish boolean
Defaults to false, so a new template starts with nothing live.

Returns{ id, name, slug, published_version_id, latest_version, sendable }

templates.addVersion

Write a new draft version. It never publishes, so editing a password reset reaches nobody until you say so.

Arguments

id string required
subject string
Defaults to an empty string.
html string
One of html or text is required.
text string
variables (string | { name, default?, required? })[]

ReturnsVersion

templates.publish

Point live sends at a version.

Arguments

id string required
version_id string
Or version, as a number. Omit both to publish the latest.

Returns{ published_version_id }

templates.render

Render the published version strictly, exactly as a send would. A missing variable is 422 template_variables_missing rather than a blank.

Arguments

id string required
variables Record<string, string>
preheader string

Returns{ subject, html, text, preheader }

templates.preview

Render leniently, filling anything you did not supply with a sample. Also returns warnings about markup mail clients will not render.

Arguments

id string required
version_id string
Preview a draft rather than the published version.
variables Record<string, string>
preheader string

Returns{ subject, html, text, values, filled_with_samples, warnings[] }

Topics

GET /v1/topics

?include_archived=true to see the retired ones as well.

POST /v1/topics

{ name, key?, description?, default_opt_in?, required? }

GET /v1/topics/:id

One topic.

PATCH /v1/topics/:id

Everything but key, which is immutable.

DELETE /v1/topics/:id

Archives rather than deletes.

GET /v1/topics/preferences

?email= returns what one address has said, and its preference page URL.

POST /v1/topics/preferences

{ email, topic, subscribed?, source? }. Omit subscribed to ask rather than write.

topics.create

A named category of mail a recipient can turn off on its own.

Arguments

name string required
key string
Lowercase letters, digits, and hyphens, at least 2 characters. Derived from the name when you leave it out, and never changeable afterwards.
description string
default_opt_in boolean
Defaults to true.
required boolean
Defaults to false. A required topic cannot be switched off.

ReturnsTopic

topics.preferences

Read or record what one address has said about your topics. Keyed by address, so somebody who was never in an audience still has a working opt-out.

Arguments

email string required
topic string
A key or an id. On the POST.
subscribed boolean
Leave it out to ask rather than to write.
source string
Where the answer came from, for your own audit trail.

Returns{ email, preference_url, preferences[] }

Automations

GET /v1/automations

Automations, with their trigger, version, and last error.

POST /v1/automations

Raw YAML, or { yaml } as JSON.

GET /v1/automations/:id

One automation, with its graph, its YAML, and its trigger URL if it has one.

PATCH /v1/automations/:id

{ enabled?, yaml? }

DELETE /v1/automations/:id

Remove it, and cancel every waiting run.

GET /v1/automations/:id.yaml

The document, with the version in x-emailssh-automation-version.

PUT /v1/automations/:id.yaml

Replace the document with the raw YAML body.

GET /v1/automations/:id/versions

The last 50 versions, each with its YAML.

POST /v1/automations/:id/versions

{ version_id } restores one as a new version.

POST /v1/automations/:id/trigger

{ email | contact_id, idempotency_key, data? } starts a run.

GET /v1/automations/:id/runs

?status=&limit= over runs.

GET /v1/automations/:id/runs/:runId

One run and every step it executed.

DELETE /v1/automations/:id/runs/:runId

Cancel a run that is waiting.

automations.create

Create an automation from a YAML document. It is validated whole: a refusal names the field, says what to write instead, and carries the line number.

Arguments

yaml string required
The document. Post it raw with a YAML or text/plain Content-Type, or wrapped as { yaml }.

Returns{ id, name, slug, trigger, enabled, version, yaml }

automations.pull

GET /v1/automations/:id.yaml. The raw document, with the current version in the x-emailssh-automation-version response header. Comments and key order survive the round trip.

Arguments

id string required

Returnstext/yaml

automations.push

PUT /v1/automations/:id.yaml. Replaces the document and writes a new version.

Arguments

id string required
yaml string required
The raw body. This is the write half of the pull and push pair a CI job uses.

Returnstext/yaml

automations.trigger

Start a run of an automation whose trigger is api.call.

Arguments

id string required
email string
One of email or contact_id is required.
contact_id string
idempotency_key string required
Required, not optional. A repeat answers 409 duplicate.
data object
Anything here is readable in the flow as {{ trigger.<name> }}.

Returns{ run_id, status }

automations.runs

Runs of one automation, with how many steps executed, how many emails went, and the error if it stopped.

Arguments

id string required
status string
running, waiting, completed, stopped, failed, or cancelled.
limit number
Defaults to 50, maximum 200.

Returns{ runs: Run[] }

Analytics

GET /v1/analytics

?from=&to=&group_by=&breakdown=&mail_class=&domain=&template_id=&tag=

GET /v1/analytics/tags

?days= returns the tag keys worth breaking down by.

analytics.get

Sends, deliveries, bounces, complaints, clicks, and opens over a window, as totals and as a series.

Arguments

from string
YYYY-MM-DD in UTC. Defaults to 30 days ago.
to string
YYYY-MM-DD in UTC, inclusive. The window may be at most 400 days.
group_by "day" | "week" | "month"
Defaults to day.
breakdown "domain" | "tag" | "mail_class" | "template"
tag_key string
Required when breakdown is tag.
mail_class "transactional" | "marketing"
domain string
template_id string
tag string
Filter to one tag, written key:value.

Returns{ range, totals, series[], breakdown, notes }

analytics.tags

Which tag keys are worth breaking down by, with how much mail each carried. Up to 25.

Arguments

days number
Defaults to 30, clamped to 1 through 400.

Returns{ tag_keys: [{ key, sent }] }

Suppressions

GET /v1/suppressions

?reason=&email=&limit=&before= over blocked addresses.

POST /v1/suppressions

{ email, reason? } blocks one yourself.

DELETE /v1/suppressions/:id

Lift one by id. A global row answers 404, and so does another workspace's.

DELETE /v1/suppressions

?email= clears one by address. ?id= is the older spelling of the route above and still works.

suppressions.list

Addresses nothing will reach on this workspace, and why each one is there. Rows with is_global set are ours rather than yours.

Arguments

reason "bounce" | "complaint" | "unsub" | "manual"
email string
Ask about one address.
limit number
Defaults to 50, from 1 to 200.
before string
The next_cursor from the previous page.

Returns{ suppressions[], next_cursor, total }

suppressions.create

Block an address yourself, for somebody who asked you to stop by replying rather than by clicking.

Arguments

email string required
reason string
Defaults to manual.

ReturnsSuppression

suppressions.delete

Clear one, when you know the address is good again. A global row answers 404 and cannot be cleared.

Arguments

id string
Give id or email.
email string

Returns{ deleted }

Dedicated IPs

GET /v1/ips

Dedicated addresses, shared pools, the default region, and data residency.

GET /v1/ips/:id

One address, with a live reverse DNS check.

PATCH /v1/ips/:id

{ paused?, daily_cap? } and nothing else.

ips.list

Dedicated addresses on the workspace, the shared pools anything else goes through, the default region, and where sending, storage, and compute physically happen.

Takes no arguments.

Returns{ default_region, residency, ips[], shared_pools[] }

ips.get

One address, plus a live reverse DNS check: what the PTR should say, what it says, and whether the forward lookup confirms it.

Arguments

id string required

ReturnsIp & { reverse_dns }

ips.update

The only two things about an address you can change. POST /v1/ips takes one out of inventory and DELETE /v1/ips/:id hands it back; this route is for pausing one and lowering its cap.

Arguments

id string required
paused boolean
Stop routing mail through it without giving it up.
daily_cap number | null
A non-negative integer, or null for no cap of your own. It can lower the warmup allowance and never raise it.

ReturnsIp

Received mail

GET /v1/messages

?unread_only=true&thread_id=&limit= over received mail.

GET /v1/messages/:id

One received message in full.

POST /v1/messages/:id/reply-all

{ body } answers on the same thread.

POST /v1/messages/:id/forward

{ to, body?, mode? }

POST /v1/messages/:id/archive

{ archived?, unread? }

GET /v1/messages/:id/attachments/:filename

One attachment, as its own bytes.

GET /v1/threads

Conversations, newest first.

GET /v1/threads/:id

Every message in one conversation.

messages.list

Mail that arrived at an address on a domain of yours with inbound turned on.

Arguments

unread_only boolean
thread_id string
limit number
Defaults to 25.

Returns{ messages: Message[] }

messages.get

One received message with its full body, headers, and attachment list.

Arguments

id string required

ReturnsMessage

messages.reply

Answer a received message on its own thread, with References and In-Reply-To set for you.

Arguments

id string required
The message you are answering.
html string
text string

Returns{ id, status }

threads.get

Every message in one conversation, oldest first.

Arguments

id string required

ReturnsThread