skrybe campaigns
Create, inspect, edit, test, send and schedule campaigns.
Campaign IDs are the plain integers the web app shows (for example 42).
List IDs are the encrypted strings skrybe lists prints.
Segment IDs are whole numbers from the segments page in the web app.
| Command | Description |
|---|---|
campaigns | Show campaigns, newest first |
campaigns get | One campaign's details, or its HTML |
campaigns stats | Opens, clicks, bounces, complaints, unsubscribes |
campaigns activity | Who opened, clicked, bounced, complained or unsubscribed |
campaigns create | Create a campaign, optionally sending or scheduling it |
campaigns update | Edit a draft or scheduled campaign |
campaigns duplicate | Copy a campaign into a new draft |
campaigns test | Send a test to up to 5 addresses |
campaigns send | Send a draft now |
campaigns schedule | Schedule a draft, or move a scheduled campaign |
campaigns unschedule | Turn a scheduled campaign back into a draft |
campaigns delete | Delete a campaign |
campaigns stop / resume | Not yet available |
campaigns
skrybe campaigns [--status <status>] [--page <n>] [--limit <n>] [--all]
skrybe campaigns ls [--status <status>] [--page <n>] [--limit <n>] [--all]
Shows the brand's campaigns, newest first. ls and list are aliases for the
bare command.
| Flag | Description |
|---|---|
--status <status> | Only campaigns in this state: draft, scheduled, preparing, sending, sent or paused |
--page <n> | Page to show. Default 1 |
--limit <n> | Campaigns per page, 1 to 100. Default 10 |
--all | Fetch every page. Cannot be combined with --page or --limit |
skrybe campaigns --status scheduled
ID STATUS DATE RECIPIENTS TITLE
51 scheduled 2027-06-15 18:05 1,204 Q4 newsletter
48 scheduled 2027-06-01 09:00 318 Customer update
DATE is the scheduled time for a scheduled campaign and the send time
otherwise, in your local timezone. --output json returns the API's fields,
including subject, from_email, sent_at and scheduled_at (Unix seconds),
timezone and source (ui, or api for campaigns created by
emails send).
API: api/campaigns/get-campaigns.php
campaigns get
skrybe campaigns get <campaign-id> [--content]
Shows one campaign: title, subject, status, sender, recipients, send or scheduled time, lists and segments (included and excluded) and its web version link.
| Flag | Description |
|---|---|
--content | Print the campaign's HTML body instead (the plain text body if it has no HTML). With --output json, add html_text and plain_text to the record |
skrybe campaigns get 42
skrybe campaigns get 42 --content > q4.html
A scheduled campaign shows its time in the campaign's own timezone, for
example 2027-06-15 18:05 Africa/Lagos.
API: api/campaigns/get-campaign.php
campaigns stats
skrybe campaigns stats <campaign-id>
Shows a campaign's report: recipients, opens and clicks (unique, rate and total), hard and soft bounces, complaints and unsubscribes, followed by a table of clicks per link.
Status sent
Recipients 1,204
Opens 602 unique (50.4%), 911 total
Clicks 143 unique (11.9%), 201 total
Bounces 9 hard, 4 soft
Complaints 1
Unsubscribes 6
CLICKS UNIQUE LINK
120 98 https://acme.test/q4
81 45 https://acme.test/shop
The open rate is a percentage of delivered emails (recipients less hard
bounces). The click rate is a percentage of recipients. Under --output json
the links are in a links array.
campaigns activity
skrybe campaigns activity <campaign-id> [--type <type>] [--page <n>] [--limit <n>] [--all]
Lists the subscribers behind a campaign's numbers.
| Flag | Description |
|---|---|
--type <type> | opens (default), clicks, bounces, complaints or unsubscribes |
--page <n> | Page to show. Default 1 |
--limit <n> | Subscribers per page, 1 to 1000. Default 100 |
--all | Fetch every page. Cannot be combined with --page or --limit |
Every row has the email and name. Opens add the number of opens and the country of the first open; clicks add the number of clicks and the links clicked.
skrybe campaigns activity 42 --type clicks --all --output json \
| jq -r '.[] | [.email, (.links | join(" "))] | @tsv'
Bounces, complaints and unsubscribes are attributed to a subscriber's latest campaign, as the report in the web app does.
API: api/campaigns/get-activity.php
campaigns create
skrybe campaigns create --title <title> --subject <subject> \
(--html-text <html|file://path> --from-name <name> --from-email <email> --reply-to <email> | --template <id>) \
[--list <list-id>]... [--send | --schedule <date-time>] [options]
Creates a campaign. Without --send or --schedule it is saved as a draft,
which you can then test, send or
schedule.
| Flag | Description |
|---|---|
--title <title> | Title shown in the dashboard. Required |
--subject <subject> | Subject line. Required |
--html-text <html> | HTML body, or file:// and a path to it. Required unless --template gives one |
--plain-text <text> | Plain text body, or file:// and a path to it |
--from-name <name> | The From name. Required unless --template gives one |
--from-email <email> | The From address. Required unless --template gives one |
--reply-to <email> | The Reply-To address. Required unless --template gives one |
--template <template-id> | Start from a template |
--list <list-id> | List to send to. Repeatable |
--segment <segment-id> | Segment to send to. Repeatable |
--exclude-list <list-id> | List to exclude. Repeatable |
--exclude-segment <segment-id> | Segment to exclude. Repeatable |
--query-string <query> | Appended to links, e.g. Google Analytics tags |
--track-opens <mode> | 0 off, 1 on, 2 anonymous |
--track-clicks <mode> | 0 off, 1 on, 2 anonymous |
--send | Send immediately instead of saving a draft |
--schedule <date-time> | Schedule it, e.g. "June 15, 2027 6:05pm". Minutes in steps of 5 |
--timezone <tz> | Timezone for --schedule, e.g. America/New_York. Defaults to the brand's timezone |
Recipients (--list, --segment and the excludes) are only used with --send
or --schedule, which need at least one list or segment. A draft does not
store recipients; you choose them when you send or schedule it.
With --template, the template supplies the HTML, plain text and sender.
Any of --html-text, --plain-text, --from-name, --from-email or
--reply-to given alongside it take precedence. If the template has no
Reply-To, its From address is used.
# A draft from a file
skrybe campaigns create \
--title 'Q4 newsletter' --subject 'Your Q4 update' \
--from-name Acme --from-email hello@acme.test --reply-to hello@acme.test \
--html-text file://campaign.html
# A draft from a template
skrybe campaigns create --template 7 --title 'Q4 newsletter' --subject 'Your Q4 update'
# Create and schedule in one step
skrybe campaigns create --template 7 --title 'Q4 newsletter' --subject 'Your Q4 update' \
--list l7763lkC1Ld892Ty4p3pvxw --schedule 'June 15, 2027 6:05pm' --timezone Africa/Lagos
campaigns create --send does not ask for confirmation. To review the
recipient count first, create a draft and use
campaigns send, which does.
Output: Campaign created (id 51)., Campaign created and now sending. or
Campaign scheduled.. Under --output json:
{"outcome": "created", "campaign_id": "51"}, where outcome is created,
sending or scheduled. The API only returns the new ID for a draft, so
campaign_id is null with --send or --schedule.
API: api/campaigns/create.php;
with --template, also api/templates/get-template.php
campaigns update
skrybe campaigns update <campaign-id> [options]
Edits a draft or scheduled campaign. Only the fields you pass change. At least
one is required. Prints the campaign afterwards, as campaigns get does.
| Flag | Description |
|---|---|
--title <title> | Title shown in the dashboard |
--subject <subject> | Subject line |
--preheader <text> | Preview text shown after the subject in the inbox |
--from-name <name> | The From name |
--from-email <email> | The From address |
--reply-to <email> | The Reply-To address |
--html-text <html> | HTML body, or file:// and a path to it |
--plain-text <text> | Plain text body, or file:// and a path to it |
--query-string <query> | Appended to links |
--track-opens <mode> | 0 off, 1 on, 2 anonymous |
--track-clicks <mode> | 0 off, 1 on, 2 anonymous |
skrybe campaigns update 42 --subject 'Your Q4 update is here' --html-text file://q4-v2.html
A campaign that is sending or already sent cannot be edited
(campaign_sending, campaign_not_draft).
API: api/campaigns/update-campaign.php
campaigns duplicate
skrybe campaigns duplicate <campaign-id> [--title <title>]
skrybe campaigns dup <campaign-id> [--title <title>]
Copies a campaign into a new draft in the same brand. Under table the new
campaign's ID goes to stdout, so it can be captured; under json and text
the new campaign's record is printed.
| Flag | Description |
|---|---|
--title <title> | Title of the copy. Defaults to the original's |
NEW=$(skrybe campaigns duplicate 42 --title 'Q1 newsletter')
API: api/campaigns/duplicate-campaign.php
campaigns test
skrybe campaigns test <campaign-id> --to <email> [--to <email>]...
Sends the campaign as a test to up to 5 addresses.
| Flag | Description |
|---|---|
--to <email> | Address to send to. Repeatable, or comma-separated. Required |
skrybe campaigns test 42 --to ada@example.com --to team@example.com
skrybe campaigns test 42 --to ada@example.com,team@example.com
Duplicate addresses are sent once. Each brand can make 20 test sends an hour,
shared with the test-send box in the web app; past that the command fails with
rate_limited (exit code 4).
Each address is reported separately. If any fails, the command exits 1 with
test_send_failed. --output json prints the result for each address:
{
"campaign_id": 42,
"results": [
{ "email": "ada@example.com", "ok": true, "error": null }
]
}
--output text prints one line per address: email, sent or failed, and
the error.
API: api/campaigns/test-send.php
campaigns send
skrybe campaigns send <campaign-id> (--list <list-id> | --segment <segment-id>)... [options]
Sends a draft now, to the lists and segments you name.
| Flag | Description |
|---|---|
--list <list-id> | List to send to. Repeatable |
--segment <segment-id> | Segment to send to. Repeatable |
--exclude-list <list-id> | List to exclude. Repeatable |
--exclude-segment <segment-id> | Segment to exclude. Repeatable |
--dry-run | Check everything and count the recipients, but send nothing |
-y, --yes | Send without asking. Required when stdin is not a terminal |
At least one --list or --segment is required.
skrybe campaigns send 42 --list l7763lkC1Ld892Ty4p3pvxw --exclude-list 3t8NqP0wKqv7X1v892bVZQ --dry-run
# ✓ Campaign 42 would go to 1,204 recipients. Nothing was sent.
skrybe campaigns send 42 --list l7763lkC1Ld892Ty4p3pvxw --exclude-list 3t8NqP0wKqv7X1v892bVZQ
# Send campaign 42 to 1,204 recipients? [y/N] y
# ✓ Campaign 42 is sending to 1,204 recipients.
Before asking, it runs a dry run, so a wrong list ID or an unverified From
domain fails before the question. From a script it refuses unless you pass
--yes.
Only a draft can be sent. Running the command again after a dropped connection
reports campaign_not_draft instead of sending twice. A scheduled campaign
reports campaign_scheduled; unschedule it first.
Under --output json:
{"outcome": "sending", "campaign_id": 42, "recipients": 1204}, or
"outcome": "dry_run" with --dry-run.
Common errors: recipients_missing, list_not_found, segment_not_found,
no_recipients, domain_not_verified, unauthorised_from_domain,
account_under_review, and quota_exceeded (exit code 4).
campaigns schedule
skrybe campaigns schedule <campaign-id> --at <date-time> (--list <list-id> | --segment <segment-id>)... [options]
Schedules a draft, or moves a campaign that is already scheduled. Takes the
same recipient flags as send, plus a time.
| Flag | Description |
|---|---|
--at <date-time> | When to send, e.g. "2027-06-15 18:05" or "June 15, 2027 6:05pm". Required |
--timezone <tz> | IANA timezone, e.g. Africa/Lagos. Defaults to the account's timezone |
--list <list-id> | List to send to. Repeatable |
--segment <segment-id> | Segment to send to. Repeatable |
--exclude-list <list-id> | List to exclude. Repeatable |
--exclude-segment <segment-id> | Segment to exclude. Repeatable |
--dry-run | Check everything and count the recipients, but change nothing |
-y, --yes | Schedule without asking. Required when stdin is not a terminal |
skrybe campaigns schedule 42 --list l7763lkC1Ld892Ty4p3pvxw --at '2027-06-15 18:05' --timezone Africa/Lagos
# Schedule campaign 42 for 2027-06-15 18:05 Africa/Lagos to 1,204 recipients? [y/N]
The time is read in --timezone, or the account's timezone if you leave it
out, and must be in the future. The recipients count against the brand's
quota as soon as the campaign is scheduled; campaigns unschedule gives the
quota back.
Under --output json:
{"outcome": "scheduled", "campaign_id": 42, "recipients": 1204, "scheduled_at": 1813079100, "timezone": "Africa/Lagos"}.
Errors specific to scheduling: schedule_missing, invalid_schedule,
schedule_in_past, invalid_timezone.
API: api/campaigns/schedule.php
campaigns unschedule
skrybe campaigns unschedule <campaign-id>
Turns a scheduled campaign back into a draft and returns its quota
reservation. Does not ask for confirmation. A campaign that is not scheduled
fails with campaign_not_scheduled.
Under --output json: {"outcome": "draft", "campaign_id": 42}.
API: api/campaigns/unschedule.php
campaigns delete
skrybe campaigns delete <campaign-id> [--yes]
skrybe campaigns rm <campaign-id> [--yes]
Deletes a campaign. A sent campaign loses its report, and a scheduled one has its quota reservation returned. A campaign that is preparing or sending cannot be deleted.
| Flag | Description |
|---|---|
-y, --yes | Delete without asking. Required when stdin is not a terminal |
Delete sent campaign 42 "Q4 newsletter" and its report? This cannot be undone. [y/N]
Under --output json: {"outcome": "deleted", "campaign_id": 42}.
API: api/campaigns/delete-campaign.php
Not yet available
These commands exist so that they explain themselves rather than failing with
"unknown command", but the HTTP API has no endpoint for them yet. They always
fail with not_implemented and exit code 2. Use the web app for now.
| Command | Purpose |
|---|---|
skrybe campaigns stop <campaign-id> | Stop a sending campaign |
skrybe campaigns resume <campaign-id> | Resume a stopped campaign |