Campaigns
Campaign management operations.
/campaigns
Create a campaign
Creates a marketing campaign in the draft state for the authenticated user. Set its
content — subject, HTML/text body, from address, target audience, schedule, and tracking
options — now or via subsequent draft updates. The campaign is not sent on creation; drive it
through its lifecycle with PATCH /campaigns/{campaign} (draft → scheduled). Requires the
campaigns:write scope.
Request body
application/vnd.api+json
— CreateCampaignRequestDocument
|
Attribute
|
Type
|
Description
|
|---|---|---|
name
required
|
string | |
subject
required
|
string | Supports merge tags — see `body_html`. Values are inserted literally here, with any carriage return or line feed removed, because the subject becomes a single header line. Must not itself contain a carriage return or line feed; a subject that does is rejected with `422`. |
body_html
required
|
string | Supports merge tags: `{{name}}`, `{{email}}`, `{{unsubscribe_url}}`, `{{unsubscribe_mailto}}`, and any key from a contact's `custom_fields`. Every recipient's message is rendered individually. A tag with no matching value renders as empty. Values are HTML-escaped here and inserted literally in `subject` and `body_text`. The unsubscribe tags are populated only for real audience contacts, never for a `test_mode` send. |
body_text
|
string|null | Supports merge tags — see `body_html`. Values are inserted literally here. |
from_address
|
string|null | Optional sender address for the campaign. When set, it must belong to the sending domain's family: the audience's sending domain itself, a subdomain of it, or a parent of it. Addresses outside that family are rejected with a `422`. When null, the campaign sends from `noreply@` on the sending domain. |
reply_to
|
string | Optional reply-to addresses for the whole campaign — where replies land instead of the `from` address. A single address or an array of up to 10 addresses. Every address must belong to the sending domain's family: the sending domain itself, a subdomain of it, or a parent of it (for example, a campaign on `mail.acme.com` allows `support@acme.com`). Addresses outside that family are rejected with a `422`. |
test_mode
|
boolean | |
test_recipient_email
|
string|null | |
track_opens
|
boolean|null | Per-campaign open-tracking override. Absent or null follows the sending domain's default; false forces tracking off; true requires open tracking enabled on the domain, otherwise 422. |
track_clicks
|
boolean|null | Per-campaign click-tracking override. Absent or null follows the sending domain's default; false forces tracking off; true requires click tracking enabled on the domain, otherwise 422. |
agent_id
|
string|null | The agent identity every send of this campaign is attributed to, for per-agent reputation. Binds the identity to marketing mail on first sight: an identifier already sending transactional mail answers 409 with code agent_category_conflict. Past the account's active-agent limit a new identifier is accepted but recorded as null. |
curl -X POST https://api.minimailer.app/campaigns \ -H "Authorization: Bearer $MINIMAILER_API_TOKEN" \ -H "Accept: application/vnd.api+json" \ -H "Content-Type: application/vnd.api+json" \ -d '{"data":{"type":"campaigns","attributes":{"name":"...","subject":"...","body_html":"...","body_text":"...","from_address":"...","reply_to":"...","test_mode":true,"test_recipient_email":"...","track_opens":true,"track_clicks":true,"agent_id":"..."}}}'
Responses
|
Status
|
Description
|
|---|---|
| 201 |
Campaign created.
CampaignDocument
|
| 401 |
Authentication is required.
ErrorDocument
|
| 403 |
The token lacks the scope this operation requires (error code missing_scope), or the request breaks a JSON:API protocol rule (client_generated_id, unsupported_relationship_update). Never returned for a record that belongs to another account — that case is a 404.
ErrorDocument
|
| 404 |
The referenced audience does not exist or belongs to another account.
ErrorDocument
|
| 415 |
The request Content-Type must be application/vnd.api+json, with no media type parameters other than profile (ext is rejected — no JSON:API extensions are supported — as is any other parameter such as charset).
ErrorDocument
|
| 422 |
The request document failed validation.
ValidationErrorDocument
|
| 409 |
The request conflicts with the endpoint or current state. Branch on the error code: conflict — the document's resource type does not match the endpoint, its id does not match the resource being updated, or an idempotency key was reused with a different payload; agent_category_conflict — the agent_id names an identity already bound to the other kind of mail (one agent identity sends either transactional or marketing mail, never both; an identity dormant for 30 days may rebind). (Creating with a client-generated id and updating relationships on an endpoint that does not support them return 403 instead.)
ErrorDocument
|
| 406 |
The Accept header mentions the JSON:API media type only in instances modified by disallowed media type parameters (or only with unsupported ext extensions), so no acceptable representation remains.
ErrorDocument
|
| 429 |
The request was rate limited. Branch on the error code: rate_limited means the account exceeded its API rate limit, and agent_throttled means the agent named by agent_id has a restricted sender reputation and has exhausted the send allowance for its tier.
ErrorDocument
|
/campaigns
List campaigns
Returns the authenticated user's campaigns, newest first, each with its current lifecycle
status (draft, scheduled, sending, paused, sent, cancelled, or failed).
Supports the JSON:API filter, sort, include, and sparse fields parameters. Requires
the campaigns:read scope.
Parameters
|
Name
|
In
|
Description
|
|---|---|---|
filter
|
query | Exact-match filters, e.g. filter[status]=delivered. Only the fields this endpoint allow-lists are accepted; an unsupported filter returns 400. |
sort
|
query | Comma-separated sort fields; prefix a field with - for descending, e.g. sort=-created_at,name. Defaults to newest-first. Only allow-listed fields are accepted; an unsupported sort returns 400. |
include
|
query | Comma-separated related resources to embed in the response included member (JSON:API compound documents). Example: include=records,webhooks. Nested paths use dot notation, max depth 2. Relationships not declared by the resource, or over-deep paths, return 400. |
fields
|
query | Per-type sparse fieldsets. Example: fields[domains]=name,status restricts the returned attributes for that type. The type must be present in the document and each field must be a known attribute, otherwise 400 is returned. |
page
|
query | Cursor-based pagination, e.g. page[cursor]=eyJpZCI6...&page[size]=50. The cursor is an opaque token issued by the API — take it from the response's links.next / links.prev URLs or meta.next_cursor / meta.prev_cursor; never construct one. size defaults to 25 and is silently capped at 100 (out-of-range or non-numeric values fall back to the default). An invalid or expired cursor falls back to the first page; a cursor replayed with a different sort than it was issued under returns 400; unknown page members such as page[number] return 400. Follow links.next until it is null (or meta.has_more is false) to traverse a collection; links.first restarts traversal. Link URLs carry these parameters URL-encoded (page%5Bcursor%5D=...). |
curl -X GET https://api.minimailer.app/campaigns \ -H "Authorization: Bearer $MINIMAILER_API_TOKEN" \ -H "Accept: application/vnd.api+json"
Responses
|
Status
|
Description
|
|---|---|
| 400 |
The include or fields query parameter is not supported (unknown relationship, over-deep include, or unknown sparse-fieldset type or field).
ErrorDocument
|
| 200 |
Campaigns retrieved.
CampaignCollectionDocument
|
| 401 |
Authentication is required.
ErrorDocument
|
| 403 |
The token lacks the scope this operation requires (error code missing_scope), or the request breaks a JSON:API protocol rule (client_generated_id, unsupported_relationship_update). Never returned for a record that belongs to another account — that case is a 404.
ErrorDocument
|
| 406 |
The Accept header mentions the JSON:API media type only in instances modified by disallowed media type parameters (or only with unsupported ext extensions), so no acceptable representation remains.
ErrorDocument
|
| 429 |
The request was rate limited. Branch on the error code: rate_limited means the account exceeded its API rate limit, and agent_throttled means the agent named by agent_id has a restricted sender reputation and has exhausted the send allowance for its tier.
ErrorDocument
|
/campaigns/{campaign}
Show a campaign
Returns a single campaign owned by the authenticated user, including its content, target
audience, schedule, and current lifecycle status. Requires the campaigns:read scope.
Parameters
|
Name
|
In
|
Description
|
|---|---|---|
include
|
query | Comma-separated related resources to embed in the response included member (JSON:API compound documents). Example: include=records,webhooks. Nested paths use dot notation, max depth 2. Relationships not declared by the resource, or over-deep paths, return 400. |
fields
|
query | Per-type sparse fieldsets. Example: fields[domains]=name,status restricts the returned attributes for that type. The type must be present in the document and each field must be a known attribute, otherwise 400 is returned. |
campaign
required
|
path | The campaign identifier. |
curl -X GET https://api.minimailer.app/campaigns/{campaign} \ -H "Authorization: Bearer $MINIMAILER_API_TOKEN" \ -H "Accept: application/vnd.api+json"
Responses
|
Status
|
Description
|
|---|---|
| 400 |
The include or fields query parameter is not supported (unknown relationship, over-deep include, or unknown sparse-fieldset type or field).
ErrorDocument
|
| 200 |
Campaign retrieved.
CampaignDocument
|
| 401 |
Authentication is required.
ErrorDocument
|
| 403 |
The token lacks the scope this operation requires (error code missing_scope), or the request breaks a JSON:API protocol rule (client_generated_id, unsupported_relationship_update). Never returned for a record that belongs to another account — that case is a 404.
ErrorDocument
|
| 404 |
The resource does not exist, or it belongs to another account. The two cases return the same status, error code and detail by design, so the error document does not confirm that an identifier is in use.
ErrorDocument
|
| 406 |
The Accept header mentions the JSON:API media type only in instances modified by disallowed media type parameters (or only with unsupported ext extensions), so no acceptable representation remains.
ErrorDocument
|
| 429 |
The request was rate limited. Branch on the error code: rate_limited means the account exceeded its API rate limit, and agent_throttled means the agent named by agent_id has a restricted sender reputation and has exhausted the send allowance for its tier.
ErrorDocument
|
/campaigns/{campaign}
Update a campaign
Updates a campaign owned by the authenticated user. This one endpoint does two jobs: editing content and driving the lifecycle state machine.
Content edits (subject, body, from address, schedule, tracking) are allowed only while
the campaign is a draft; editing content on any other status returns 422.
Status transitions follow the allowed edges; any other transition returns 422:
draft→scheduled(queue it to send at itsscheduled_at) orcancelledscheduled→cancelledsending→pausedpaused→sending(resume — re-queues the not-yet-sent recipients) orcancelled
Internal edges (scheduled → sending when the schedule fires, sending → sent/failed)
happen automatically and are never triggered through this endpoint. Requires the
campaigns:write scope.
Parameters
|
Name
|
In
|
Description
|
|---|---|---|
campaign
required
|
path | The campaign identifier. |
Request body
application/vnd.api+json
— UpdateCampaignRequestDocument
|
Attribute
|
Type
|
Description
|
|---|---|---|
status
|
string | |
name
|
string | |
subject
|
string | Must not contain a carriage return or line feed; a subject that does is rejected with `422`. Merge-tag values are inserted with those characters removed, because the subject becomes a single header line. |
body_html
|
string | |
body_text
|
string|null | |
from_address
|
string|null | Optional sender address for the campaign. When set, it must belong to the sending domain's family: the audience's sending domain itself, a subdomain of it, or a parent of it. Addresses outside that family are rejected with a `422`. When null, the campaign sends from `noreply@` on the sending domain. |
reply_to
|
string | Optional reply-to addresses for the whole campaign — where replies land instead of the `from` address. A single address or an array of up to 10 addresses. Every address must belong to the sending domain's family: the sending domain itself, a subdomain of it, or a parent of it. Addresses outside that family are rejected with a `422`. Editable only while the campaign is a draft. |
test_mode
|
boolean | |
test_recipient_email
|
string|null | |
scheduled_at
|
string|null | |
track_opens
|
boolean|null | Per-campaign open-tracking override. Absent or null follows the sending domain's default; false forces tracking off; true requires open tracking enabled on the domain, otherwise 422. |
track_clicks
|
boolean|null | Per-campaign click-tracking override. Absent or null follows the sending domain's default; false forces tracking off; true requires click tracking enabled on the domain, otherwise 422. |
agent_id
|
string|null | Draft-only, like every content attribute: a campaign that has left draft keeps the identity its sends were attributed to. Same binding rules as on create (409 agent_category_conflict for an identity already sending transactional mail). |
curl -X PATCH https://api.minimailer.app/campaigns/{campaign} \ -H "Authorization: Bearer $MINIMAILER_API_TOKEN" \ -H "Accept: application/vnd.api+json" \ -H "Content-Type: application/vnd.api+json" \ -d '{"data":{"type":"campaigns","attributes":{"status":"...","name":"...","subject":"...","body_html":"...","body_text":"...","from_address":"...","reply_to":"...","test_mode":true,"test_recipient_email":"...","scheduled_at":"...","track_opens":true,"track_clicks":true,"agent_id":"..."}}}'
Responses
|
Status
|
Description
|
|---|---|
| 200 |
Campaign updated.
CampaignDocument
|
| 401 |
Authentication is required.
ErrorDocument
|
| 403 |
The token lacks the scope this operation requires (error code missing_scope), or the request breaks a JSON:API protocol rule (client_generated_id, unsupported_relationship_update). Never returned for a record that belongs to another account — that case is a 404.
ErrorDocument
|
| 404 |
The resource does not exist, or it belongs to another account. The two cases return the same status, error code and detail by design, so the error document does not confirm that an identifier is in use.
ErrorDocument
|
| 415 |
The request Content-Type must be application/vnd.api+json, with no media type parameters other than profile (ext is rejected — no JSON:API extensions are supported — as is any other parameter such as charset).
ErrorDocument
|
| 422 |
The request document failed validation.
ValidationErrorDocument
|
| 409 |
The request conflicts with the endpoint or current state. Branch on the error code: conflict — the document's resource type does not match the endpoint, its id does not match the resource being updated, or an idempotency key was reused with a different payload; agent_category_conflict — the agent_id names an identity already bound to the other kind of mail (one agent identity sends either transactional or marketing mail, never both; an identity dormant for 30 days may rebind). (Creating with a client-generated id and updating relationships on an endpoint that does not support them return 403 instead.)
ErrorDocument
|
| 406 |
The Accept header mentions the JSON:API media type only in instances modified by disallowed media type parameters (or only with unsupported ext extensions), so no acceptable representation remains.
ErrorDocument
|
| 429 |
The request was rate limited. Branch on the error code: rate_limited means the account exceeded its API rate limit, and agent_throttled means the agent named by agent_id has a restricted sender reputation and has exhausted the send allowance for its tier.
ErrorDocument
|
/campaigns/{campaign}
Delete a campaign
Deletes a campaign owned by the authenticated user. To stop an in-flight send, transition it
to cancelled via PATCH rather than deleting it. Statistics for a campaign that has already
sent are retained on its send records. Requires the campaigns:write scope.
Parameters
|
Name
|
In
|
Description
|
|---|---|---|
campaign
required
|
path | The campaign identifier. |
curl -X DELETE https://api.minimailer.app/campaigns/{campaign} \ -H "Authorization: Bearer $MINIMAILER_API_TOKEN" \ -H "Accept: application/vnd.api+json"
Responses
|
Status
|
Description
|
|---|---|
| 204 | Campaign deleted. |
| 401 |
Authentication is required.
ErrorDocument
|
| 403 |
The token lacks the scope this operation requires (error code missing_scope), or the request breaks a JSON:API protocol rule (client_generated_id, unsupported_relationship_update). Never returned for a record that belongs to another account — that case is a 404.
ErrorDocument
|
| 404 |
The resource does not exist, or it belongs to another account. The two cases return the same status, error code and detail by design, so the error document does not confirm that an identifier is in use.
ErrorDocument
|
| 406 |
The Accept header mentions the JSON:API media type only in instances modified by disallowed media type parameters (or only with unsupported ext extensions), so no acceptable representation remains.
ErrorDocument
|
| 429 |
The request was rate limited. Branch on the error code: rate_limited means the account exceeded its API rate limit, and agent_throttled means the agent named by agent_id has a restricted sender reputation and has exhausted the send allowance for its tier.
ErrorDocument
|
/campaigns/{campaign}/stats
Show campaign stats
Returns aggregate delivery and engagement statistics for a campaign — totals such as
recipients, deliveries, opens, clicks, bounces, complaints, and unsubscribes derived from its
send events. Open and click figures reflect only the tracking that was enabled for the
campaign. Figures are most meaningful once the campaign has reached sending or sent.
Requires the campaigns:read scope.
Parameters
|
Name
|
In
|
Description
|
|---|---|---|
campaign
required
|
path | The campaign identifier. |
curl -X GET https://api.minimailer.app/campaigns/{campaign}/stats \ -H "Authorization: Bearer $MINIMAILER_API_TOKEN" \ -H "Accept: application/vnd.api+json"
Responses
|
Status
|
Description
|
|---|---|
| 200 |
Campaign statistics retrieved.
CampaignStatsDocument
|
| 401 |
Authentication is required.
ErrorDocument
|
| 403 |
The token lacks the scope this operation requires (error code missing_scope), or the request breaks a JSON:API protocol rule (client_generated_id, unsupported_relationship_update). Never returned for a record that belongs to another account — that case is a 404.
ErrorDocument
|
| 404 |
The resource does not exist, or it belongs to another account. The two cases return the same status, error code and detail by design, so the error document does not confirm that an identifier is in use.
ErrorDocument
|
| 406 |
The Accept header mentions the JSON:API media type only in instances modified by disallowed media type parameters (or only with unsupported ext extensions), so no acceptable representation remains.
ErrorDocument
|
| 429 |
The request was rate limited. Branch on the error code: rate_limited means the account exceeded its API rate limit, and agent_throttled means the agent named by agent_id has a restricted sender reputation and has exhausted the send allowance for its tier.
ErrorDocument
|