Resources

Campaigns

.md

Broadcast email campaigns to contact groups.

Campaigns send one email to many recipients (newsletters, product announcements). The workflow: create a draft, configure content and audience, optionally send a test, then queue for sending.

Plan requirement
Sending campaigns requires a plan with broadcasts enabled (Starter or higher). On the Free plan you can create and configure campaigns, but /send fails (see Queue for sending).

List campaignsGET/v1/campaigns

Permission: read. Ordered by created_at, newest first.

Query paramDescription
statusFilter by status: draft, scheduled, sending, sent, paused or cancelled. Other values return 400.
limitDefault 20, max 100.
offsetDefault 0.
Each item also includes contact_group_id, total_delivered, total_opened, total_clicked, total_bounced, scheduled_at, sending_started_at, sending_completed_at, created_at, updated_at, contact_group { id, name, color } and template { id, name }.
JSON
01{
02 "campaigns": [
03 {
04 "id": "...",
05 "name": "July Newsletter",
06 "subject": "What is new in July",
07 "status": "sent",
08 "template_id": null,
09 "contact_group_ids": ["group_id_1"],
10 "total_recipients": 1200,
11 "total_sent": 1180,
12 "...": "..."
13 }
14 ],
15 "pagination": { "total": 1, "limit": 20, "offset": 0 }
16}
UTF-8 · LF · JSONtickos.dev

Get a campaignGET/v1/campaigns/{id}

Permission: read. Returns { campaign } with all campaign fields (including content_html, content_json and content_text), plus contact_group { id, name, color, total_contacts } and template { id, name, subject, email_format } when linked. Returns 404 if the campaign does not exist in your account.

Create a campaignPOST/v1/campaigns

Permission: write. Creates a campaign in draft status and returns 201 with { campaign }.

FieldTypeRequiredDescription
namestringNoCampaign name (non-empty string). Default: "Untitled Broadcast".
clone_fromstringNoID of an existing campaign to copy subject, preview_text, sender (from_email, from_name, reply_to), content, audience, send_type and template_id from. Unknown IDs return 404.
template_idstringNoEmail template ID (e.g. etpl_xxxxxxxx). Links the campaign to a template instead of providing content_html directly. Ignored when clone_from is set (the source campaign template is used).
Template validation
template_id is not checked against your templates when the campaign is created. PATCH /v1/campaigns/{id} validates it and returns 404 for a template that does not exist in your account, so set it via PATCH if you want the check.

Configure the campaignPATCH/v1/campaigns/{id}

Permission: write. Set all the fields needed before sending. Only the fields below are accepted; other keys are ignored, and a body with none of them returns 400. Returns { campaign }.

FieldTypeDescription
namestringCampaign name.
subjectstringEmail subject line.
preview_textstringEmail preview text (shown in inbox previews).
from_emailstringSender email. Must be a valid address (400 otherwise); its domain must be verified for outbound email at send time.
from_namestringSender display name.
reply_tostringReply-to address. Must be a valid address, empty string or null.
template_idstringEmail template ID (etpl_...). If set, the template HTML is used at send time instead of content_html. Must exist in your account (404 otherwise). Set to null to unlink.
content_htmlstringHTML content of the email. Ignored at send time if template_id is set.
content_textstringPlain text fallback.
content_jsonobjectEditor JSON (used by the visual editor).
contact_group_idstringSingle contact group ID (legacy).
contact_group_idsstring[]Array of contact group IDs for the audience (400 if not an array). Takes precedence over contact_group_id.
total_recipientsnumberOverride total recipients count (auto-calculated at send time if 0).
send_typestringimmediate (default) or scheduled.
scheduled_atstringISO 8601 date for scheduled sends. Set it to a future time: it is not validated, and a past time is sent on the next cron run.
statusstringAllowed: draft, scheduled, paused, cancelled (400 otherwise). Cannot set sending or sent manually.
email_formatstringbroadcast (default) or email. Controls how the HTML is wrapped. When a template is linked, the template email_format is used instead.
terminal
Bash
01curl -X PATCH https://api.tickos.dev/v1/campaigns/{id} \
02 -H 'Authorization: Bearer sk_your_api_key' \
03 -H 'Content-Type: application/json' \
04 -d '{
05 "subject": "What is new in July",
06 "from_email": "news@yourdomain.com",
07 "from_name": "Acme News",
08 "content_html": "<h1>July Newsletter</h1><p>Hello!</p>",
09 "contact_group_ids": ["group_id_1", "group_id_2"]
10 }'
UTF-8 · LF · Bashtickos.dev

Campaign workflow

Creating a campaign always requires at least 2 API calls: POST to create the draft, then PATCH to configure content, sender and audience. Once configured, you call /send to queue it. Here are the 3 scenarios:

Example 1: Immediate send with HTML

You provide the HTML directly via content_html. All recipients receive the same content.

terminal
Bash
01# Step 1: Create the campaign (returns the campaign id)
02curl https://api.tickos.dev/v1/campaigns \
03 -H 'Authorization: Bearer sk_your_api_key' \
04 -H 'Content-Type: application/json' \
05 -d '{ "name": "July Newsletter" }'
06 
07# Step 2: Configure content, sender and audience
08curl -X PATCH https://api.tickos.dev/v1/campaigns/{id} \
09 -H 'Authorization: Bearer sk_your_api_key' \
10 -H 'Content-Type: application/json' \
11 -d '{
12 "subject": "What is new in July",
13 "from_email": "news@yourdomain.com",
14 "from_name": "Acme News",
15 "content_html": "<h1>July Newsletter</h1><p>Hello!</p>",
16 "contact_group_ids": ["group_id_1"]
17 }'
18 
19# Step 3: Send immediately
20curl -X POST https://api.tickos.dev/v1/campaigns/{id}/send \
21 -H 'Authorization: Bearer sk_your_api_key'
UTF-8 · LF · Bashtickos.dev

Example 2: Immediate send with template (personalized)

Link a template via template_id. The template HTML can contain {{variables}} that are resolved for each contact (e.g. "Hello {{first_name}}" becomes "Hello Jane" for Jane Doe).

terminal
Bash
01# Step 1: Create the campaign linked to a template
02curl https://api.tickos.dev/v1/campaigns \
03 -H 'Authorization: Bearer sk_your_api_key' \
04 -H 'Content-Type: application/json' \
05 -d '{ "name": "July Newsletter", "template_id": "etpl_xxxxxxxx" }'
06 
07# Step 2: Configure sender and audience (content comes from the template)
08curl -X PATCH https://api.tickos.dev/v1/campaigns/{id} \
09 -H 'Authorization: Bearer sk_your_api_key' \
10 -H 'Content-Type: application/json' \
11 -d '{
12 "subject": "What is new in July",
13 "from_email": "news@yourdomain.com",
14 "from_name": "Acme News",
15 "contact_group_ids": ["group_id_1"]
16 }'
17 
18# Step 3 (optional): Send a test to yourself first
19curl -X POST https://api.tickos.dev/v1/campaigns/{id}/test-send \
20 -H 'Authorization: Bearer sk_your_api_key' \
21 -H 'Content-Type: application/json' \
22 -d '{ "test_email": "me@example.com" }'
23 
24# Step 4: Send immediately
25curl -X POST https://api.tickos.dev/v1/campaigns/{id}/send \
26 -H 'Authorization: Bearer sk_your_api_key'
UTF-8 · LF · Bashtickos.dev

Example 3: Scheduled send

Set send_type to "scheduled" and scheduled_at to an ISO 8601 date in the future. The campaign is queued and the cron job sends it at the specified time. Works with both content_html and template_id. If send_type is "scheduled" but scheduled_at is empty, /send sends immediately.

The campaign stays in "scheduled" status until the cron processes it at the scheduled_at time.
Bash
01# Step 1: Create the campaign
02curl https://api.tickos.dev/v1/campaigns \
03 -H 'Authorization: Bearer sk_your_api_key' \
04 -H 'Content-Type: application/json' \
05 -d '{ "name": "Black Friday Sale" }'
06 
07# Step 2: Configure everything including schedule
08curl -X PATCH https://api.tickos.dev/v1/campaigns/{id} \
09 -H 'Authorization: Bearer sk_your_api_key' \
10 -H 'Content-Type: application/json' \
11 -d '{
12 "subject": "Black Friday: 50% off everything!",
13 "from_email": "promo@yourdomain.com",
14 "from_name": "Acme Store",
15 "template_id": "etpl_xxxxxxxx",
16 "contact_group_ids": ["group_id_1", "group_id_2"],
17 "send_type": "scheduled",
18 "scheduled_at": "2026-11-28T08:00:00Z"
19 }'
20 
21# Step 3: Queue the scheduled send
22curl -X POST https://api.tickos.dev/v1/campaigns/{id}/send \
23 -H 'Authorization: Bearer sk_your_api_key'
24 
25# Response: { "campaign": { "status": "scheduled", ... }, "scheduled": true }
UTF-8 · LF · Bashtickos.dev
Cancel a scheduled campaign
To cancel a scheduled campaign, PATCH it with { "status": "cancelled" } or { "status": "draft" } to re-edit it.

Templates and variables

When a campaign uses template_id, the API fetches the latest HTML from the template at send time. If you update the template later, future sends automatically use the new version.

System variables

These variables are resolved automatically for each recipient from their contact record. Use {{key}} with no spaces inside the braces. Variables are replaced in the template HTML only, not in the campaign subject.

VariableResolves toExample
{{name}}Full name of the contact.Jane Doe
{{first_name}}First word of the full name.Jane
{{email}}Email address of the contact.jane@example.com
{{phone}}Reserved. Currently always resolves to an empty string in campaign sends.(empty)
{{unsubscribe_url}}Unique unsubscribe link (auto-generated).https://app.tickos.dev/unsubscribe?token=...

Custom variables

Templates can define custom variables with default values (e.g. {{company_name}} with default "Acme"). These are configured in the template itself from the dashboard. Campaigns do not accept per-recipient custom values, so every recipient gets the template default. Variables with no value are left as-is.

Variable priority
System variables (from the contact record) are resolved first, so a template default for name, first_name, email or phone is never used. The {{unsubscribe_url}} variable works in both modes (with or without template).
template_id vs content_html
If both template_id and content_html are set, template_id takes priority at send time. To go back to using content_html directly, set template_id to null via PATCH.

Send a test emailPOST/v1/campaigns/{id}/test-send

Permission: write. Sends the campaign content to a single test email address directly via SES (bypasses the queue). The campaign needs subject, content (content_html or template_id) and from_email (400 otherwise). The subject is prefixed with [Test]. If the campaign uses a template, variables are resolved with test data (name: "Test User", email: the test address) and template defaults; no custom values are applied. {{unsubscribe_url}} is replaced with "#".

Test sends skip suppression checks and plan send limits, and do not block the real send to the same address. Returns { success: true, email_id }.

terminal
Bash
01curl -X POST https://api.tickos.dev/v1/campaigns/{id}/test-send \
02 -H 'Authorization: Bearer sk_your_api_key' \
03 -H 'Content-Type: application/json' \
04 -d '{ "test_email": "me@example.com" }'
UTF-8 · LF · Bashtickos.dev

Queue for sendingPOST/v1/campaigns/{id}/send

Permission: write. No request body required. The campaign must be in draft or scheduled status (409 otherwise) and have: subject, content (content_html or template_id), from_email, and at least one contact group (400 otherwise).

ScenarioWhat happens
Immediate (send_type not set or "immediate")Sets status to "sending", processes the first chunk (1000 contacts). Remaining emails continue in background via cron.
Scheduled (send_type: "scheduled" + scheduled_at)Sets status to "scheduled" and returns { campaign, scheduled: true }. The cron job processes it at the scheduled_at time.

For an immediate send, the response is 200 with { campaign, total_queued, total_skipped, done, message }. done is true when every recipient was processed ("Broadcast sent successfully"); otherwise the remaining recipients are processed in the background ("Broadcast is being sent. Remaining emails will be processed in the background."). Unsubscribed, blocked and suppressed contacts are skipped. If the audience has no eligible contacts, the campaign is marked sent with total_queued 0.

SituationResponse
Campaign not found404.
Status is not draft or scheduled409.
Missing subject, content, audience or from_email400.
Plan without broadcasts, not enough remaining monthly email quota for the whole audience, from domain not verified, or linked template missing or empty500 with { error } describing the reason (for example "Not enough email quota. Campaign needs ..."). The campaign goes back to draft so you can fix the issue and retry.
Monthly, daily or hourly send limit already reached (or reached mid-send)200 with done: false. The campaign stays in sending status and the cron retries every minute until the limit allows.
Scheduled sends
For scheduled campaigns, the plan, quota and domain checks run when the cron starts sending, not when you call /send. If a check fails at that point, the campaign returns to draft status.

Delete a campaignDELETE/v1/campaigns/{id}

Permission: admin. Only draft campaigns can be deleted. Non-draft campaigns return 409.

List unsubscribed contactsGET/v1/campaigns/unsubscribed

Permission: read. Returns contacts that have unsubscribed from broadcasts, with the campaign name, newest first. Query params: limit (default 50, max 100) and offset. Returns { unsubscribed: [...], pagination: { total, limit, offset } }.

Resubscribe a contactDELETE/v1/campaigns/unsubscribed

Permission: write. Removes the unsubscribe record. Body: { "id": "record_id" } or { "email": "user@example.com" }. Returns { resubscribed: true, removed }, or 404 if no record matches.

Get broadcast settingsGET/v1/campaigns/settings

Permission: read. Returns { settings } with the account-wide broadcast settings (logo, footer and sender defaults) listed below.

Update broadcast settingsPATCH/v1/campaigns/settings

Permission: write. Only the fields below are accepted; other keys are ignored, and a body with none of them returns 400. Returns the updated { settings }.

FieldTypeDescription
logo_urlstringAccount logo URL used in broadcasts. Also set by uploading with purpose "logo".
broadcast_logo_alignmentstringleft, center or right (400 otherwise).
broadcast_footer_alignmentstringleft, center or right (400 otherwise).
broadcast_unsubscribe_messagestringText shown with the unsubscribe link.
broadcast_company_namestringCompany name shown in the footer.
broadcast_company_addressstringStreet address shown in the footer.
broadcast_company_citystringCity.
broadcast_company_statestringState or region.
broadcast_company_zipstringPostal code.
broadcast_company_countrystringCountry.
broadcast_default_domainstringDefault sending domain for new broadcasts.
broadcast_default_sender_namestringDefault sender display name.
broadcast_default_from_prefixstringDefault local part of the sender address (the part before @).
broadcast_default_reply_tostringDefault reply-to address.
terminal
Bash
01curl -X PATCH https://api.tickos.dev/v1/campaigns/settings \
02 -H 'Authorization: Bearer sk_your_api_key' \
03 -H 'Content-Type: application/json' \
04 -d '{
05 "broadcast_company_name": "Acme Inc",
06 "broadcast_company_country": "US",
07 "broadcast_logo_alignment": "center"
08 }'
UTF-8 · LF · Bashtickos.dev

Upload a broadcast imagePOST/v1/uploads/broadcast-image

Permission: write. Uploads an image (sent as base64 JSON) for use in campaign content or as the account logo. Returns 201 with { url, file_name }; use url in your content_html.

FieldTypeRequiredDescription
file_namestringYesOriginal file name. The stored file gets a generated name.
content_typestringYesimage/jpeg, image/png, image/gif, image/webp or image/svg+xml.
file_datastringYesBase64-encoded file content. A data URI (data:image/png;base64,...) is also accepted.
entity_idstringNoOptional ID used to group the file in storage (for example the campaign ID). Characters other than letters, digits, _ and - are removed.
purposestringNocontent (default) or logo. Max size: 5 MB for content, 2 MB for logo (400 otherwise). With logo, the uploaded URL also replaces the account logo (logo_url).
terminal
Bash
01curl https://api.tickos.dev/v1/uploads/broadcast-image \
02 -H 'Authorization: Bearer sk_your_api_key' \
03 -H 'Content-Type: application/json' \
04 -d '{
05 "file_name": "hero.png",
06 "content_type": "image/png",
07 "file_data": "iVBORw0KGgoAAAANSUhEUgAA...",
08 "purpose": "content"
09 }'
UTF-8 · LF · Bashtickos.dev
Each recipient gets a unique unsubscribe link through the {{unsubscribe_url}} variable. TickOS does not add a footer for you: include {{unsubscribe_url}} in your content_html or template (the unsubscribe footer of the dashboard editor already uses it). Unsubscribed contacts are excluded from future campaigns.