Download OpenAPI specification:Download
SendFox's REST API lets you manage contacts, campaigns, lists, automations, and forms programmatically. It uses OAuth 2.0 for authentication.
Compatible with AI agents (Claude, ChatGPT, etc.) via the OpenAPI spec. Available at GET /openapi.yaml.
Create a personal access token at https://sendfox.com/account/oauth. Once created, use it in the Authorization header:
Authorization: Bearer {TOKEN}
For integrations that require user authentication, create an OAuth 2.0 client at https://sendfox.com/account/oauth
API requests are limited to 60 requests per minute per authenticated user. Rate limit status is returned in response headers:
X-RateLimit-Limit: Maximum requests per minuteX-RateLimit-Remaining: Remaining requests in current windowRetry-After: Seconds until rate limit resets (only on 429 responses)All error responses use standard HTTP status codes and Laravel's default error format:
message: Human-readable error descriptionerrors: Field-level validation errors (on 422 responses)Every plan can create an API key. Lifetime and Pro plans get the whole API. The Free plan gets a limited set of endpoints: GET /me; reading lists, tags, contact tags, contact fields, campaigns and campaign stats, saved templates and sections, automations, and forms; creating contacts, adding a contact to a list, and tagging a contact; transactional email; and, on accounts with transactional access, sending domains. Any other endpoint returns 402 Payment Required for a Free account. A key's access follows the account's plan, so upgrading opens the rest without a new key.
Accounts restricted by SendFox cannot use authenticated API endpoints. These requests return 403 Forbidden with the error code account_restricted and a link to the account status page.
Send email from your app, one per request or up to 100 per
batch: login codes, receipts, notifications. Unlike the rest of the API, transactional
routes are available on every plan, including free. Use any API key
from Settings > API tokens (the Transactional page makes your first one);
it works on every route your plan includes (see Plans & API Access), the
Domains endpoints included, so your app can add, verify and remove the
domains it sends from.
Transactional keys made before keys were unified carry transactional:*
scopes and keep working on these routes and /domains only; they are
rejected by the rest of the API.
Transactional is for messages your users trigger: password resets, receipts, notifications about their own account. Newsletters, promotions, re-engagement and review requests belong in SendFox campaigns.
Keep your code. Resend's SDKs read a base URL from the environment and
SendFox accepts Resend's request shape at /transactional/resend, so
the switch is two variables plus a verified domain:
RESEND_BASE_URL=https://api.sendfox.com/transactional/resend
RESEND_API_KEY=<your SendFox transactional key>
resend.emails.send(), resend.batch.send() and resend.emails.get()
then go to POST /transactional/resend/emails,
POST /transactional/resend/emails/batch and
GET /transactional/resend/emails/{id}: to, cc and bcc may be a
string or an array, reply_to may be a one-item array, and
tags: [{name, value}] becomes the SendFox tag (the first value).
Every recipient counts as one send against your plan, as Resend counts
them, and a message may carry up to 10 addresses (Resend allows 50). Responses and errors are SendFox's
(202 {id, status}, and the error catalog below with name, message
and fix). attachments work as on Resend ({filename, content, content_type}, with content in base64 or a byte array), except path
URLs and inline images (content_id), which are refused.
scheduled_at is not supported yet. Anything unsupported is refused
with a 422 validation_error rather than sent without it. For webhooks, set your endpoint to the Resend format (see
Webhooks below) and your existing handler and signature check keep
working with the SendFox secret.
Every non-2xx response has the same shape: status, name,
message, fix, link, request_id.
| Status | name | When |
|---|---|---|
| 401 | missing_api_key |
No Authorization header |
| 401 | invalid_api_key |
The key is wrong or revoked |
| 403 | insufficient_scope |
A free-plan key without the needed transactional scope |
| 403 | feature_not_available |
Transactional email is not enabled on the account |
| 403 | domain_not_verified |
The from domain is not verified for this account |
| 403 | sandbox_recipient_not_allowed |
Sandbox sender to an address that is not yours |
| 403 | recipient_suppressed |
A recipient is on your suppression list (suppressed lists them) |
| 403 | account_paused |
Transactional sending is paused for review |
| 403 | account_restricted |
The SendFox account is restricted |
| 404 | not_found |
No message with that id on this account |
| 409 | invalid_idempotent_request |
Same Idempotency-Key, different body, inside 24 hours |
| 422 | validation_error |
Field errors in errors |
| 429 | rate_limit_exceeded |
Over 5/s or 300/min; honor Retry-After |
| 429 | monthly_quota_exceeded |
The plan's sends are used up (remaining, requested); upgrade the SendFox plan from Account > Billing |
Add one https URL under Transactional > Webhooks and SendFox POSTs a
JSON event to it for each of sent, delivered, delayed, bounced,
complained and failed that you pick. Sandbox test addresses send
events too. The URL must resolve to a public address; redirects are not
followed, and SendFox waits up to 10 seconds for an answer.
{
"id": "whd_5f0c2a9e1b7d4c3a8e6f1d2b",
"event": "delivered",
"occurred_at": "2026-10-10T14:03:22+00:00",
"data": {
"message": { "id": "tm_…", "status": "delivered", "to": ["[email protected]"], "subject": "Your code", "tag": "login_code", "…": "same fields as GET /transactional/emails/{id}, without events" },
"recipient": "[email protected]",
"status": null,
"reason": null
}
}
recipient is the address the event is about (a message with cc
gets one event per recipient). status and reason carry the
receiving server's answer on bounced, delayed and failed.
Any 2xx answer counts as delivered. Anything else, or no answer, is
retried after 1 minute, 15 minutes, 2 hours and 18 hours, then given
up. Retries send the same body with the same id, so dedupe on it.
Verify every request. The X-SendFox-Signature header is
t={unix seconds},v1={hex}, where hex is HMAC-SHA256 of
{t}.{raw body} keyed with your signing secret (the whole whsec_…
string). Compute it over the raw body before parsing the JSON, compare
in constant time, and reject a t more than five minutes old. Requests
also carry X-SendFox-Event and X-SendFox-Delivery (the body's id).
Resend format. Switching from Resend? Set the webhook's format to
Resend under Transactional > Webhooks and the body is shaped like a
Resend event instead, with the same event types (email.sent,
email.delivered, email.delivery_delayed, email.bounced,
email.complained, email.failed) and svix-id, svix-timestamp
and svix-signature headers signed the way Svix signs, so your
existing handler and webhooks.verify() call keep working with the
SendFox secret. data.email_id is the SendFox tm_ id, data.tags
is {"tag": "<your tag>"}, a bounce or delay carries Resend's
bounce object with the receiving server's reason, and
data.recipient names the address the event is about.
{
"type": "email.bounced",
"created_at": "2026-10-10T14:03:22+00:00",
"data": {
"created_at": "2026-10-10T14:03:20+00:00",
"email_id": "tm_…",
"from": "Acme <[email protected]>",
"to": ["[email protected]"],
"subject": "Your code",
"tags": { "tag": "login_code" },
"bounce": { "message": "550 5.1.1 User unknown", "subType": "5.1.1", "type": "Permanent" },
"recipient": "[email protected]"
}
}
Queues one email. The response is immediate; delivery happens in the
background and is reported through GET /transactional/emails/{id}
and your webhook.
Idempotency. Send an Idempotency-Key header (1 to 256 characters)
to make retries safe. Within 24 hours, the same key with the same body
returns the original message (with an idempotent-replayed: true
header); the same key with a different body is a 409 invalid_idempotent_request.
Sandbox. Send from [email protected] with no DNS setup.
It delivers only to your own account email and to the four test
addresses. Test addresses never reach a mailbox and never count toward
your limits; each one synthesizes an outcome so you can exercise your
integration: [email protected], [email protected],
[email protected], [email protected].
Limits. Transactional email has no quota of its own: every
address in to, cc and bcc counts as one send against your
SendFox plan, the same monthly sends your campaigns use, with no
daily cap. When the plan has a cap, every response carries
x-sendfox-sends-remaining, the sends left across campaigns and
transactional email; a plan without an enforced cap sends no header.
Past the cap, sends are refused with 429 monthly_quota_exceeded
carrying remaining and requested. Each message holds at most 10
addresses across to, cc and bcc, at most 2 in bcc. Requests
are limited to 5 per second and 300 per minute per key.
Custom headers. Pass headers to add list-management and
threading headers. For one-click unsubscribe (RFC 8058, which Gmail
and Yahoo require of bulk senders) send List-Unsubscribe with an
https: URL in angle brackets and List-Unsubscribe-Post set to
List-Unsubscribe=One-Click; the URL must accept a POST whose body
is List-Unsubscribe=One-Click and unsubscribe the recipient without
any further click. See the headers field for what is allowed.
| Idempotency-Key | string [ 1 .. 256 ] characters |
| from required | string <= 320 characters Sender. Either an address or a display name followed by one in angle brackets. The domain must be plain ASCII (for an internationalized domain, use its punycode form, starting xn--) and verified under Account > Domains (a subdomain of a verified domain also works), or be the sandbox sender |
| to required | Array of strings <email> [ 1 .. 10 ] items 1 to 10 addresses. to, cc and bcc together may hold at most 10, and every address counts as one email against your plan's sends, so a message to three people uses three. |
| cc | Array of strings <email> <= 10 items |
| bcc | Array of strings <email> <= 2 items At most 2 addresses; counts toward the 10-address total. |
| reply_to | string or null <email> |
| subject required | string <= 998 characters |
| html | string HTML body, at most 500 KB (UTF-8 bytes). Send html, text, or both. |
| text | string Plain-text body, at most 100 KB (UTF-8 bytes). Send html, text, or both. |
| tag | string or null <= 64 characters ^[a-z0-9][a-z0-9_.-]*$ A label for grouping in the dashboard, for per-tag open/click tracking switches, and for per-tag alerts. Lowercase letters, digits, dots, dashes and underscores. Messages without a tag are grouped by subject. Tags that look like authentication mail ( |
object or null <= 20 properties Custom headers, at most 20, as | |
Array of objects or null <= 10 items Files to attach, at most 10 and 5 MB in total once decoded. Each is |
{- "subject": "Your login code",
- "html": "<p>Your code is <strong>483 920</strong>.</p>",
- "text": "string",
- "tag": "login_code",
- "headers": {
- "List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
}, - "attachments": [
- {
- "filename": "invite.ics",
- "content": "string",
- "content_type": "text/calendar; method=REQUEST"
}
]
}{- "id": "tm_4f0c9b2e7d1a5c3e8b6f2a9d",
- "status": "queued"
}Newest first. Standard SendFox paginator; bodies are never included.
| status | string Enum: "queued" "sent" "delivered" "bounced" "complained" "failed" "suppressed" |
| tag | string |
| to | string <email> Exact match on the first |
| mode | string Enum: "live" "test" |
| page | integer |
| per_page | integer <= 100 Default: 25 |
{- "data": [
- {
- "id": "tm_4f0c9b2e7d1a5c3e8b6f2a9d",
- "status": "queued",
- "mode": "live",
- "from": "string",
- "to": [
- "string"
], - "cc": [
- "string"
], - "bcc": [
- "string"
], - "reply_to": "string",
- "subject": "string",
- "tag": "string",
- "headers": {
- "property1": "string",
- "property2": "string"
}, - "attachments": [
- {
- "filename": "string",
- "content_type": "string",
- "size": 0
}
], - "idempotency_key": "string",
- "failure": {
- "name": "string",
- "message": "string"
}, - "accepted_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "delivered_at": "2019-08-24T14:15:22Z",
- "failed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "events": [
- {
- "event": "sent",
- "occurred_at": "2019-08-24T14:15:22Z",
- "reason": "string",
- "status": "string"
}
]
}
], - "current_page": 0,
- "total": 0,
- "per_page": 0
}Queues up to 100 emails in one request and reports a result per item.
Items are independent: one invalid address, unverified sender or
suppressed recipient is reported at its index and the rest are
queued. The response is 200 whenever the batch itself was accepted,
even if every item was refused; read each entry's error.
Limits. The whole batch's sends (one per to, cc and bcc
address across every item) are reserved against your plan before
any item is looked at. A batch that does not fit is refused with
429 monthly_quota_exceeded and nothing is sent, so a retry can
never half-send. Sends for refused items, test addresses and replays
are returned. A batch request counts as one request against the 5
per second and 300 per minute rate limit; the plan's sends are what
bound volume.
Idempotency. Two levels. An Idempotency-Key header makes the
whole batch replayable for 24 hours: the same key with the same
emails returns the original results with idempotent-replayed: true;
a different body is a 409 invalid_idempotent_request. A per-item
idempotency_key protects each email on its own, exactly like the
header on POST /transactional/emails, and is the one to rely on when
your retry may be built from a different batch.
Sandbox, test addresses and tags work per item as on the single route.
| Idempotency-Key | string [ 1 .. 256 ] characters |
required | Array of objects [ 1 .. 100 ] items 1 to 100 emails. Each item takes the same fields as |
{- "emails": [
- {
- "subject": "Your receipt for order 1042",
- "html": "<p>Thanks, Jane.</p>",
- "tag": "receipt",
- "idempotency_key": "order-1042"
}, - {
- "to": [
- "not-an-email"
], - "subject": "Your receipt for order 1043",
- "html": "<p>Thanks.</p>",
- "tag": "receipt"
}
]
}{- "data": [
- {
- "index": 0,
- "id": "tm_4f0c9b2e7d1a5c3e8b6f2a9d",
- "status": "queued"
}, - {
- "index": 1,
- "error": {
- "status": 422,
- "name": "validation_error",
- "message": "The request body has invalid or missing fields.",
- "fix": "See the errors object for each field.",
- "errors": {
- "to.0": [
- "The to.0 field must be a valid email address."
]
}
}
}
]
}The message without its bodies, plus its event timeline.
| id required | string Example: tm_4f0c9b2e7d1a5c3e8b6f2a9d |
{- "id": "tm_4f0c9b2e7d1a5c3e8b6f2a9d",
- "status": "queued",
- "mode": "live",
- "from": "string",
- "to": [
- "string"
], - "cc": [
- "string"
], - "bcc": [
- "string"
], - "reply_to": "string",
- "subject": "string",
- "tag": "string",
- "headers": {
- "property1": "string",
- "property2": "string"
}, - "attachments": [
- {
- "filename": "string",
- "content_type": "string",
- "size": 0
}
], - "idempotency_key": "string",
- "failure": {
- "name": "string",
- "message": "string"
}, - "accepted_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "delivered_at": "2019-08-24T14:15:22Z",
- "failed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "events": [
- {
- "event": "sent",
- "occurred_at": "2019-08-24T14:15:22Z",
- "reason": "string",
- "status": "string"
}
]
}Returns a paginated list of contacts (100 per page by default, up to 1000 via per_page).
Supports engagement filtering through filter[...] query parameters, so you can answer
questions like "who last opened over a year ago" without paging the whole account. All
filter conditions are AND-ed. Pass count_only=true to get just the number of matches —
the cheapest way to size an audience before acting on it.
| query | string Search query for filtering contacts |
| unsubscribed | boolean Filter unsubscribed contacts |
string Filter by specific email | |
| per_page | integer [ 1 .. 1000 ] Default: 100 Contacts per page |
| count_only | boolean Return only the number of matching contacts, with no contact records |
| filter[status] | string Enum: "active" "engaged" "inactive" "new" "unconfirmed" "unsubscribed" "bounced" "invalid" Engagement status |
| filter[last_opened_after] | string <date-time> Contacts whose most recent open is on or after this date |
| filter[last_opened_before] | string <date-time> Contacts whose most recent open is before this date. Excludes contacts who never opened — use filter[never_opened] for those. |
| filter[last_clicked_after] | string <date-time> Contacts whose most recent click is on or after this date |
| filter[last_clicked_before] | string <date-time> Contacts whose most recent click is before this date |
| filter[last_sent_after] | string <date-time> Contacts last sent to on or after this date |
| filter[last_sent_before] | string <date-time> Contacts last sent to before this date |
| filter[created_after] | string <date-time> Contacts created on or after this date |
| filter[created_before] | string <date-time> Contacts created before this date |
| filter[never_opened] | boolean Only contacts who have never opened an email |
| filter[never_clicked] | boolean Only contacts who have never clicked a link |
| filter[never_sent] | boolean Only contacts who have never been sent an email |
| filter[in_list_ids] | Array of integers Only contacts still subscribed to any of these lists. Members who unsubscribed from the list are not matched, so results may be smaller than before per-list unsubscribe shipped. |
| filter[not_in_list_ids] | Array of integers Exclude contacts who are members of any of these lists, whether or not they unsubscribed from it. |
| filter[tag_ids] | Array of integers Only contacts still subscribed to any of these tags. Somebody who opted out of the tag keeps it but is not matched. Same rule as filter[in_list_ids]; see the tag_ids note in the bulk-action filter schema. |
| filter[not_tag_ids] | Array of integers Exclude contacts carrying any of these tags |
{- "count": 0,
- "filter": "string"
}| email required | string <email> |
| first_name | string |
| last_name | string |
| ip_address | string |
| lists | Array of integers Array of list IDs to add the contact to |
Array of objects |
{- "first_name": "string",
- "last_name": "string",
- "ip_address": "string",
- "lists": [
- 0
], - "contact_fields": [
- {
- "name": "string",
- "value": "string"
}
]
}{- "id": 0,
- "first_name": "string",
- "last_name": "string",
- "ip_address": "string",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}{- "id": 0,
- "first_name": "string",
- "last_name": "string",
- "ip_address": "string",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Update contact details including name, list memberships, and custom fields
| id required | integer |
| first_name | string |
| last_name | string |
| lists | Array of integers Array of list IDs (replaces all current list memberships) |
Array of objects |
{- "first_name": "string",
- "last_name": "string",
- "lists": [
- 0
], - "contact_fields": [
- {
- "name": "string",
- "value": "string"
}
]
}{- "id": 0,
- "first_name": "string",
- "last_name": "string",
- "ip_address": "string",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Returns paginated email deliverables and contact-level engagement summary
| id required | integer |
{- "contact": {
- "last_sent_at": "2019-08-24T14:15:22Z",
- "last_opened_at": "2019-08-24T14:15:22Z",
- "last_clicked_at": "2019-08-24T14:15:22Z",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "bounced_at": "2019-08-24T14:15:22Z"
}, - "deliverables": {
- "data": [
- {
- "campaign_title": "string",
- "campaign_id": 0,
- "sent_at": "2019-08-24T14:15:22Z",
- "opened_at": "2019-08-24T14:15:22Z",
- "clicked_at": "2019-08-24T14:15:22Z",
- "bounced_at": "2019-08-24T14:15:22Z",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "spam_at": "2019-08-24T14:15:22Z"
}
]
}
}Import up to 1,000 contacts in a single request. Creates new contacts or updates existing ones.
required | Array of objects <= 1000 items |
{- "contacts": [
]
}{- "created": 0,
- "updated": 0
}Queues one action against every contact the filter matches, applied in chunks in the background. Returns immediately with an id to poll.
Run with dry_run: true first. A dry run reports how many contacts match and changes
nothing — it is the only way to catch a filter that matches more than intended, and a bulk
write is not undoable. An empty filter (which would match the whole account) is rejected
unless it is a dry run.
Per-campaign conditions (opened_campaign_id, not_opened_campaign_id,
clicked_campaign_id) are available here but not on GET /contacts, because they are
resolved in the background rather than inside a request.
Bulk delete and bulk unsubscribe are deliberately not offered.
Because in_list_ids matches only subscribed members, remove_from_list with
filter.in_list_ids set to that same list leaves the memberships of people who
unsubscribed from it in place. They already receive nothing from that list.
tag_ids behaves the same way, so remove_tag with filter.tag_ids set to that
same tag leaves the tag on people who unsubscribed from it. They already receive
nothing sent to that tag. To reach every carrier regardless of opt-out status,
select them by some other filter, or read them from the tag endpoints.
| action required | string Enum: "apply_tag" "remove_tag" "add_to_list" "remove_from_list" |
| target_id required | integer The tag id (apply_tag/remove_tag) or list id (add_to_list/remove_from_list) |
| dry_run | boolean Default: false When true, only count the matches — nothing is modified |
object (ContactFilter) An engagement filter over the account's contacts. Every condition is AND-ed. Omitting the object entirely means "all contacts". |
{- "action": "apply_tag",
- "target_id": 0,
- "dry_run": false,
- "filter": {
- "status": "active",
- "last_opened_after": "2019-08-24T14:15:22Z",
- "last_opened_before": "2019-08-24T14:15:22Z",
- "last_clicked_after": "2019-08-24T14:15:22Z",
- "last_clicked_before": "2019-08-24T14:15:22Z",
- "last_sent_after": "2019-08-24T14:15:22Z",
- "last_sent_before": "2019-08-24T14:15:22Z",
- "created_after": "2019-08-24T14:15:22Z",
- "created_before": "2019-08-24T14:15:22Z",
- "never_opened": true,
- "never_clicked": true,
- "never_sent": true,
- "in_list_ids": [
- 0
], - "not_in_list_ids": [
- 0
], - "tag_ids": [
- 0
], - "not_tag_ids": [
- 0
], - "opened_campaign_id": 0,
- "not_opened_campaign_id": 0,
- "clicked_campaign_id": 0
}
}{- "id": 0,
- "status": "pending",
- "action": "apply_tag",
- "target_id": 0,
- "dry_run": true,
- "filter": "string",
- "matched_count": 0,
- "processed_count": 0,
- "error": "string",
- "started_at": "2019-08-24T14:15:22Z",
- "completed_at": "2019-08-24T14:15:22Z"
}For a dry run, matched_count is the answer and nothing was modified. A run whose match set exceeds the per-request ceiling fails without applying anything.
| id required | integer |
{- "id": 0,
- "status": "pending",
- "action": "apply_tag",
- "target_id": 0,
- "dry_run": true,
- "filter": "string",
- "matched_count": 0,
- "processed_count": 0,
- "error": "string",
- "started_at": "2019-08-24T14:15:22Z",
- "completed_at": "2019-08-24T14:15:22Z"
}| query | string Search query for filtering contacts |
{- "data": [
- {
- "id": 0,
- "first_name": "string",
- "last_name": "string",
- "ip_address": "string",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "current_page": 0,
- "total": 0,
- "per_page": 0
}| email required | string <email> |
{- "email": "[email protected]"
}{- "id": 0,
- "first_name": "string",
- "last_name": "string",
- "ip_address": "string",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}{- "data": [
- {
- "id": 0,
- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "is_web_gated": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "current_page": 0,
- "total": 0,
- "per_page": 0
}Creates a campaign as a draft. To send it, use the send endpoint or provide scheduled_at. Subject lines cannot start with "RE:" or "FWD:". At least one list is required if scheduled_at is provided.
| title required | string <= 191 characters |
| subject required | string <= 191 characters |
| preview_text | string or null <= 191 characters Inbox preview snippet shown beneath the subject line. Optional. |
| html required | string <= 1000000 characters Email body HTML content. Unless the campaign uses the Code Your Own layout, tags outside the editor's set are removed on save, along with event-handler attributes (such as onclick) and javascript: links. |
| from_name required | string <= 191 characters |
| from_email required | string <email> <= 191 characters |
| scheduled_at | string <date-time> Schedule send time (omit for draft). Must include at least one list. |
| lists | Array of integers Array of list IDs to send to |
| excluded_lists | Array of integers List IDs whose contacts should be excluded from the send. Exclusion wins over inclusion. |
| to_contact_tags | Array of integers Tag IDs whose contacts should receive the campaign. Can be combined with lists. |
| excluded_contact_tags | Array of integers Tag IDs whose contacts should be excluded from the send. Exclusion wins over inclusion. |
| web_publish | boolean Publish the email to the web on your primary Smart Page once it sends. Requires a Smart Page. Omit it to list the email on every Smart Page whose Newsletters tab is on; send false to keep it off the web. |
| web_gate | boolean Default: false On a published email, ask new readers for their email address to read past the opening. The account-wide signup requirement in Settings overrides this choice when enabled, including when false is sent or this field is omitted. |
{- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "lists": [
- 0
], - "excluded_lists": [
- 0
], - "to_contact_tags": [
- 0
], - "excluded_contact_tags": [
- 0
], - "web_publish": true,
- "web_gate": false
}{- "id": 0,
- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "is_web_gated": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Returns stored counts and rates for one to 100 distinct owned campaigns in request order. The entire request fails with the same validation error for missing, deleted or foreign IDs. Does not return link stats or query individual delivery events.
| campaign_ids[] required | Array of integers [ 1 .. 100 ] items unique [ items >= 1 ] Repeat campaign_ids[] for each campaign ID. |
{- "data": [
- {
- "link_stats": [
- {
- "url": "string",
- "clicks": 0
}
], - "sent_count": 0,
- "unique_open_count": 0,
- "unique_click_count": 0,
- "unsubscribe_count": 0,
- "bounce_count": 0,
- "spam_count": 0,
- "open_rate": 0,
- "click_rate": 0,
- "unsubscribe_rate": 0,
- "bounce_rate": 0,
- "spam_rate": 0,
- "id": 0
}
]
}{- "id": 0,
- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "is_web_gated": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Only draft campaigns (not yet sent) can be updated. All fields are optional.
| id required | integer |
| title | string <= 191 characters |
| subject | string <= 191 characters |
| preview_text | string or null <= 191 characters Inbox preview snippet. Pass null to clear. |
| html | string <= 1000000 characters Email body HTML content. Filtered on save the same way as when creating a campaign. |
| from_name | string <= 191 characters |
| from_email | string <email> <= 191 characters |
| scheduled_at | string or null <date-time> Set to null to unschedule, or a datetime to schedule |
| lists | Array of integers Replaces all list assignments |
| excluded_lists | Array of integers Replaces the excluded lists (contacts in these lists are removed from the send). |
| to_contact_tags | Array of integers Replaces the included tag audience (contacts carrying these tags receive the campaign). |
| excluded_contact_tags | Array of integers Replaces the excluded tag audience (contacts carrying these tags are removed from the send). |
{- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "lists": [
- 0
], - "excluded_lists": [
- 0
], - "to_contact_tags": [
- 0
], - "excluded_contact_tags": [
- 0
]
}{- "id": 0,
- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "is_web_gated": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Schedules a draft campaign for immediate sending. The campaign must:
All existing abuse prevention applies automatically: content approval workflow, sending throttles, spam detection, and bounce rate monitoring.
| id required | integer |
{- "id": 0,
- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "is_web_gated": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Delivers the rendered campaign to the addresses given so it can be checked in a real inbox. This is a real send to those addresses only: the campaign's audience is untouched and the campaign stays a draft. Recipients are added to the account's "My Test Contacts" list.
Limits: up to 30 addresses per call (1 on the free plan) and 30 test sends per
account per day, shared with Send Test in the SendFox app. A test from a newly
created account goes to the account's own address only, unless the campaign sends
from a domain the account has verified; notice in the response says so when
addresses were dropped.
| id required | integer |
| emails required | Array of strings <email> [ 1 .. 30 ] items Addresses to deliver the test to. Trimmed and de-duplicated. |
{
}{- "sent": true,
- "campaign_id": 0,
- "notice": "string"
}Returns sent count and open/click/bounce/unsubscribe/spam counts and rates, all read from stored counters.
Pass include_link_stats=true to also get link_stats — the campaign's links ranked by click count. That part is opt-in because it counts rows in email_link_clicks once per link rather than reading a counter, so leaving it off keeps this endpoint as cheap as it has always been.
| id required | integer |
| include_link_stats | boolean Default: false When true, include the link_stats breakdown in the response |
| link_limit | integer [ 1 .. 100 ] Default: 50 How many links to return in link_stats, most-clicked first. Ignored unless include_link_stats is true. |
{- "link_stats": [
- {
- "url": "string",
- "clicks": 0
}
], - "sent_count": 0,
- "unique_open_count": 0,
- "unique_click_count": 0,
- "unsubscribe_count": 0,
- "bounce_count": 0,
- "spam_count": 0,
- "open_rate": 0,
- "click_rate": 0,
- "unsubscribe_rate": 0,
- "bounce_rate": 0,
- "spam_rate": 0
}Returns the contacts in one of a sent campaign's engagement groups. non_openers covers contacts the campaign actually sent to who did not open it — queued and cancelled deliverables are excluded, since they never had the chance.
| id required | integer |
| type required | string Enum: "openers" "clickers" "non_openers" "bounced" "unsubscribed" Which engagement group to list |
| per_page | integer [ 1 .. 1000 ] Default: 100 Records per page |
{- "data": [
- {
- "id": 0,
- "contact_id": 0,
- "email": "string",
- "first_name": "string",
- "last_name": "string",
- "sent_at": "2019-08-24T14:15:22Z",
- "opened_at": "2019-08-24T14:15:22Z",
- "clicked_at": "2019-08-24T14:15:22Z",
- "bounced_at": "2019-08-24T14:15:22Z",
- "unsubscribed_at": "2019-08-24T14:15:22Z"
}
], - "current_page": 0,
- "total": 0,
- "per_page": 0
}Creates a new draft with the original's content, sender, and exclusions, aimed at the chosen slice of the original audience. Omit scheduled_at to leave it as a draft. The source campaign's lists are deliberately not carried over — the engagement segment is the audience.
| id required | integer |
| audience required | string Enum: "non_openers" "openers" "clickers" |
| subject | string <= 191 characters Optional new subject line; defaults to the original's |
| title | string <= 191 characters Optional internal name; defaults to "Resend: |
| scheduled_at | string <date-time> Optional send time; omit to leave the resend as a draft |
{- "audience": "non_openers",
- "subject": "string",
- "title": "string",
- "scheduled_at": "2019-08-24T14:15:22Z"
}{- "id": 0,
- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "is_web_gated": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| query | string Search query for filtering lists |
{- "data": [
- {
- "id": 0,
- "name": "string",
- "user_id": 0,
- "subscribed_contacts_count": 0,
- "unsubscribed_contacts_count": 0,
- "public_description": "string",
- "average_email_open_percent": 0,
- "average_email_click_percent": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "current_page": 0,
- "total": 0,
- "per_page": 0
}| name required | string |
{- "name": "string"
}{- "id": 0,
- "name": "string",
- "user_id": 0,
- "subscribed_contacts_count": 0,
- "unsubscribed_contacts_count": 0,
- "public_description": "string",
- "average_email_open_percent": 0,
- "average_email_click_percent": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Returns list details including average open and click rates
| id required | integer |
{- "id": 0,
- "name": "string",
- "user_id": 0,
- "subscribed_contacts_count": 0,
- "unsubscribed_contacts_count": 0,
- "public_description": "string",
- "average_email_open_percent": 0,
- "average_email_click_percent": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| id required | integer |
| name required | string <= 191 characters |
{- "name": "string"
}{- "id": 0,
- "name": "string",
- "user_id": 0,
- "subscribed_contacts_count": 0,
- "unsubscribed_contacts_count": 0,
- "public_description": "string",
- "average_email_open_percent": 0,
- "average_email_click_percent": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Members who no longer receive this list, and when they stopped. Each row is a Contact with two extra fields.
The two timestamps are never merged. list_unsubscribed_at is this list only. unsubscribed_at keeps the meaning it has everywhere else in this API: the account-wide opt-out. Treating a per-list timestamp as an account-wide one would suppress that person across your whole integration because they left a single list, so read unsubscribed_scope if you need one field to branch on.
| list_id required | integer |
| query | string Search query for filtering contacts |
{- "data": [
- {
- "id": 0,
- "first_name": "string",
- "last_name": "string",
- "ip_address": "string",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "list_unsubscribed_at": "2019-08-24T14:15:22Z",
- "unsubscribed_scope": "list"
}
], - "current_page": 0,
- "total": 0,
- "per_page": 0
}Stops this one list for this one contact. Their other lists and their account-wide status are untouched, which is what distinguishes this from PATCH /unsubscribe — that endpoint is account-wide and is unchanged.
Idempotent. Repeating the request returns the original timestamp rather than moving it, so a retry never loses the date the person actually asked.
The opt-out is recorded durably, so it survives the contact being removed from the list and added back: re-adding them does not start sending again.
There is no matching resubscribe endpoint. Taking consent away on someone's behalf is safe for an integration to do; handing it back is not.
| list_id required | integer |
| email required | string <email> Email address of a contact already on this list |
{- "email": "[email protected]"
}{- "id": 0,
- "first_name": "string",
- "last_name": "string",
- "ip_address": "string",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "list_unsubscribed_at": "2019-08-24T14:15:22Z",
- "unsubscribed_scope": "list"
}Returns every contact on the list. This is membership, not subscription: someone who unsubscribed from the list is still a member and is still returned, so that an integration mirroring this endpoint can tell "removed from the list" apart from "asked to stop receiving it". Pass status to narrow it.
| list_id required | integer |
| query | string Search query for filtering contacts |
| status | string Enum: "subscribed" "unsubscribed" Optional. Omit for every member, which is the long-standing behaviour of this endpoint. |
{- "data": [
- {
- "id": 0,
- "first_name": "string",
- "last_name": "string",
- "ip_address": "string",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "current_page": 0,
- "total": 0,
- "per_page": 0
}Adds an existing contact to a list. If the contact is already in the list, no duplicate is created.
| list_id required | integer |
| contact_id required | integer ID of the contact to add |
{- "contact_id": 0
}{- "id": 0,
- "first_name": "string",
- "last_name": "string",
- "ip_address": "string",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| list_id required | integer |
| contact_id required | integer |
{- "id": 0,
- "first_name": "string",
- "last_name": "string",
- "ip_address": "string",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| query | string Search query for filtering forms |
{- "data": [
- {
- "id": 0,
- "title": "string",
- "landing_page_id": 0,
- "redirect_url": "string",
- "gdpr_required": true,
- "url": "string",
- "lists": [
- {
- "id": 0,
- "name": "string",
- "user_id": 0,
- "subscribed_contacts_count": 0,
- "unsubscribed_contacts_count": 0,
- "public_description": "string",
- "average_email_open_percent": 0,
- "average_email_click_percent": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "current_page": 0,
- "total": 0,
- "per_page": 0
}Creates a subscription form linked to one or more lists. Free users are limited to 1 form.
| title required | string <= 191 characters |
| lists required | Array of integers Array of list IDs to attach |
| redirect_url | string or null <uri> URL to redirect to after subscription |
| gdpr_required | boolean Whether GDPR consent checkbox is required |
{- "title": "string",
- "lists": [
- 0
], - "gdpr_required": true
}{- "id": 0,
- "title": "string",
- "landing_page_id": 0,
- "redirect_url": "string",
- "gdpr_required": true,
- "url": "string",
- "lists": [
- {
- "id": 0,
- "name": "string",
- "user_id": 0,
- "subscribed_contacts_count": 0,
- "unsubscribed_contacts_count": 0,
- "public_description": "string",
- "average_email_open_percent": 0,
- "average_email_click_percent": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}{- "id": 0,
- "title": "string",
- "landing_page_id": 0,
- "redirect_url": "string",
- "gdpr_required": true,
- "url": "string",
- "lists": [
- {
- "id": 0,
- "name": "string",
- "user_id": 0,
- "subscribed_contacts_count": 0,
- "unsubscribed_contacts_count": 0,
- "public_description": "string",
- "average_email_open_percent": 0,
- "average_email_click_percent": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| id required | integer |
| title | string <= 191 characters |
| lists | Array of integers Replaces all list assignments |
| redirect_url | string or null <uri> |
| gdpr_required | boolean |
{- "title": "string",
- "lists": [
- 0
], - "gdpr_required": true
}{- "id": 0,
- "title": "string",
- "landing_page_id": 0,
- "redirect_url": "string",
- "gdpr_required": true,
- "url": "string",
- "lists": [
- {
- "id": 0,
- "name": "string",
- "user_id": 0,
- "subscribed_contacts_count": 0,
- "unsubscribed_contacts_count": 0,
- "public_description": "string",
- "average_email_open_percent": 0,
- "average_email_click_percent": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}{- "data": [
- {
- "id": 0,
- "user_id": 0,
- "title": "string",
- "active": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "automation_triggers": [
- {
- "id": 0,
- "automation_id": 0,
- "type": "apply_list",
- "list_id": 0,
- "contact_tag_id": 0,
- "campaign_id": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "automation_items": [
- {
- "id": 0,
- "type": "email",
- "automation_id": 0,
- "campaign_id": 0,
- "parent_item_id": 0,
- "branch": "yes",
- "delay_hours": 0,
- "send_order": 0,
- "condition_type": "opened_previous",
- "condition_list_id": 0,
- "condition_contact_tag_id": 0,
- "condition_email_link_id": 0,
- "yes_path_ends": true,
- "no_path_ends": true,
- "campaign": {
- "id": 0,
- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "is_web_gated": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}
], - "current_page": 0,
- "total": 0,
- "per_page": 0
}| title required | string |
| trigger_type | string Enum: "apply_list" "apply_tag" "open_campaign" "click_campaign" |
| trigger_list_id | integer Required when trigger_type is apply_list |
| trigger_contact_tag_id | integer Required when trigger_type is apply_tag |
| trigger_campaign_id | integer Required when trigger_type is open_campaign or click_campaign. Any campaign you own, sent or not yet sent, including an email inside another automation. |
| active | boolean |
{- "title": "string",
- "trigger_type": "apply_list",
- "trigger_list_id": 0,
- "trigger_contact_tag_id": 0,
- "trigger_campaign_id": 0,
- "active": true
}{- "id": 0,
- "user_id": 0,
- "title": "string",
- "active": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "automation_triggers": [
- {
- "id": 0,
- "automation_id": 0,
- "type": "apply_list",
- "list_id": 0,
- "contact_tag_id": 0,
- "campaign_id": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "automation_items": [
- {
- "id": 0,
- "type": "email",
- "automation_id": 0,
- "campaign_id": 0,
- "parent_item_id": 0,
- "branch": "yes",
- "delay_hours": 0,
- "send_order": 0,
- "condition_type": "opened_previous",
- "condition_list_id": 0,
- "condition_contact_tag_id": 0,
- "condition_email_link_id": 0,
- "yes_path_ends": true,
- "no_path_ends": true,
- "campaign": {
- "id": 0,
- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "is_web_gated": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}Returns automation with triggers, items, and campaign stats
| id required | integer |
{- "id": 0,
- "user_id": 0,
- "title": "string",
- "active": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "automation_triggers": [
- {
- "id": 0,
- "automation_id": 0,
- "type": "apply_list",
- "list_id": 0,
- "contact_tag_id": 0,
- "campaign_id": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "automation_items": [
- {
- "id": 0,
- "type": "email",
- "automation_id": 0,
- "campaign_id": 0,
- "parent_item_id": 0,
- "branch": "yes",
- "delay_hours": 0,
- "send_order": 0,
- "condition_type": "opened_previous",
- "condition_list_id": 0,
- "condition_contact_tag_id": 0,
- "condition_email_link_id": 0,
- "yes_path_ends": true,
- "no_path_ends": true,
- "campaign": {
- "id": 0,
- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "is_web_gated": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}Update title, trigger, or active status. Activating reschedules stale deliverables.
| id required | integer |
| title | string |
| active | boolean |
| trigger_type | string Enum: "apply_list" "apply_tag" "open_campaign" "click_campaign" |
| trigger_list_id | integer Required when trigger_type is apply_list |
| trigger_contact_tag_id | integer Required when trigger_type is apply_tag |
| trigger_campaign_id | integer Required when trigger_type is open_campaign or click_campaign |
{- "title": "string",
- "active": true,
- "trigger_type": "apply_list",
- "trigger_list_id": 0,
- "trigger_contact_tag_id": 0,
- "trigger_campaign_id": 0
}{- "id": 0,
- "user_id": 0,
- "title": "string",
- "active": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "automation_triggers": [
- {
- "id": 0,
- "automation_id": 0,
- "type": "apply_list",
- "list_id": 0,
- "contact_tag_id": 0,
- "campaign_id": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "automation_items": [
- {
- "id": 0,
- "type": "email",
- "automation_id": 0,
- "campaign_id": 0,
- "parent_item_id": 0,
- "branch": "yes",
- "delay_hours": 0,
- "send_order": 0,
- "condition_type": "opened_previous",
- "condition_list_id": 0,
- "condition_contact_tag_id": 0,
- "condition_email_link_id": 0,
- "yes_path_ends": true,
- "no_path_ends": true,
- "campaign": {
- "id": 0,
- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "is_web_gated": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}| id required | integer |
| subject required | string |
| html required | string |
| from_name required | string |
| from_email required | string <email> |
| delay_hours | integer [ 0 .. 5000 ] Hours to wait before sending (default 24, first email defaults to 0) |
{- "subject": "string",
- "html": "string",
- "from_name": "string",
- "delay_hours": 5000
}{- "id": 0,
- "type": "email",
- "automation_id": 0,
- "campaign_id": 0,
- "parent_item_id": 0,
- "branch": "yes",
- "delay_hours": 0,
- "send_order": 0,
- "condition_type": "opened_previous",
- "condition_list_id": 0,
- "condition_contact_tag_id": 0,
- "condition_email_link_id": 0,
- "yes_path_ends": true,
- "no_path_ends": true,
- "campaign": {
- "id": 0,
- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "is_web_gated": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| id required | integer |
| subject | string |
| html | string |
| from_name | string |
| from_email | string <email> |
| delay_hours | integer [ 0 .. 5000 ] |
| send_order | integer >= 1 |
{- "subject": "string",
- "html": "string",
- "from_name": "string",
- "delay_hours": 5000,
- "send_order": 1
}{- "id": 0,
- "type": "email",
- "automation_id": 0,
- "campaign_id": 0,
- "parent_item_id": 0,
- "branch": "yes",
- "delay_hours": 0,
- "send_order": 0,
- "condition_type": "opened_previous",
- "condition_list_id": 0,
- "condition_contact_tag_id": 0,
- "condition_email_link_id": 0,
- "yes_path_ends": true,
- "no_path_ends": true,
- "campaign": {
- "id": 0,
- "title": "string",
- "subject": "string",
- "preview_text": "string",
- "html": "string",
- "from_name": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "is_web_gated": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Returns a paginated list of the user's whitelabel/sender domains.
Sending domains are shared by marketing campaigns and transactional email.
Every Domains endpoint accepts a paid-plan API key or a transactional key
that carries the transactional:domains scope. A transactional key
without that scope gets a 403 in the transactional error shape
(insufficient_scope), and one on an account without transactional
access gets feature_not_available.
{- "data": [
- {
- "id": 0,
- "domain": "string",
- "sendgrid_whitelabel_domain_id": 0,
- "validated_at": "2019-08-24T14:15:22Z",
- "dns": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "current_page": 0,
- "total": 0,
- "per_page": 0
}Adds a new sender domain and creates the corresponding SendGrid whitelabel domain. Requires a provisioned SendGrid subuser; accounts get one shortly after signup.
| domain required | string <= 191 characters Domain name (e.g., example.com). Do not include @ or protocol. |
{- "domain": "string"
}{- "id": 0,
- "domain": "string",
- "sendgrid_whitelabel_domain_id": 0,
- "validated_at": "2019-08-24T14:15:22Z",
- "dns": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Returns domain details including DNS records needed for verification
| id required | integer |
{- "id": 0,
- "domain": "string",
- "sendgrid_whitelabel_domain_id": 0,
- "validated_at": "2019-08-24T14:15:22Z",
- "dns": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Triggers DNS validation for the domain via SendGrid. Returns whether validation passed and any errors for specific DNS records.
| id required | integer |
{- "message": "string",
- "valid": true,
- "domain": {
- "id": 0,
- "domain": "string",
- "sendgrid_whitelabel_domain_id": 0,
- "validated_at": "2019-08-24T14:15:22Z",
- "dns": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
}{- "id": 0,
- "name": "string",
- "contacts_count": 0,
- "contact_limit": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Creates a custom field for contacts. The name is auto-generated from the label as a slug.
Provide type (text, number, or date) or the existing contact_field_type_id UUID.
If both are provided, they must identify the same type. UUIDs are the same for every account:
| Type | contact_field_type_id |
|---|---|
| text | 0abe5601-7738-43c8-858f-81911ecf89ee |
| number | e43c6471-b3ff-4541-80c5-964c62decd15 |
| date | fe7dd76c-5113-4159-bc7c-0dbf0ed8d404 |
The value is immutable after creation and is not returned by any endpoint - responses
carry the human-readable type (text, number, date) instead.
| label required | string <= 191 characters Human-readable field label |
| contact_field_type_id | string Enum: "0abe5601-7738-43c8-858f-81911ecf89ee" "e43c6471-b3ff-4541-80c5-964c62decd15" "fe7dd76c-5113-4159-bc7c-0dbf0ed8d404" UUID of the field's data type. Required unless type is provided; must match type if both are provided. |
| type required | string Enum: "text" "number" "date" Readable data type; required unless contact_field_type_id is provided. |
{- "label": "string",
- "contact_field_type_id": "0abe5601-7738-43c8-858f-81911ecf89ee",
- "type": "text"
}{- "id": 0,
- "label": "string",
- "name": "string",
- "type": "text"
}Updates the label and auto-regenerates the name slug.
Note: contact_field_type_id is immutable and cannot be changed after creation.
| id required | integer |
| label required | string <= 191 characters |
{- "label": "string"
}{- "id": 0,
- "label": "string",
- "name": "string",
- "type": "text"
}