Sendry Docs
API Reference

Campaigns

Create and send broadcast email campaigns to audiences.

Campaigns let you send one email to an entire audience. You can schedule campaigns in advance or send immediately.

Campaign lifecycle

draft → scheduled → sending → sent
                 └→ cancelled

Create campaign

POST /v1/campaigns

Requires full_access scope.

Request body

{
  "name": "March Newsletter",
  "subject": "What's new in March",
  "from": "Newsletter <news@acme.com>",
  "audience_id": "aud_abc123",
  "html": "<h1>March Update</h1><p>Here's what happened this month...</p>",
  "unsubscribe_url": "https://acme.com/unsubscribe",
  "scheduled_at": "2025-03-15T09:00:00Z"
}
FieldTypeRequiredDescription
namestringYesCampaign name (internal).
subjectstringYesEmail subject line.
fromstringYesSender address. Domain must be verified.
audience_idstringYesID of the audience to send to.
htmlstringNo*HTML body. At least one of html or text required.
textstringNo*Plain-text body.
template_idstringNoUse a saved template.
unsubscribe_urlstringYesUnsubscribe URL for CAN-SPAM compliance.
scheduled_atstringNoISO 8601 datetime. Omit to send immediately.

Response

{
  "id": "camp_abc123",
  "name": "March Newsletter",
  "status": "scheduled",
  "audience_id": "aud_abc123",
  "subject": "What's new in March",
  "scheduled_at": "2025-03-15T09:00:00Z",
  "created_at": "2025-03-12T09:00:00Z"
}

Get campaign

GET /v1/campaigns/:id

Requires read_only scope.


List campaigns

GET /v1/campaigns

Requires read_only scope.


Update campaign

PATCH /v1/campaigns/:id

Requires full_access scope. Only campaigns in draft or scheduled status can be updated.


Cancel campaign

POST /v1/campaigns/:id/cancel

Requires full_access scope. Cancels a scheduled campaign before it starts sending. Cannot cancel a campaign that is already in sending or sent status.


Delete campaign

DELETE /v1/campaigns/:id

Requires full_access scope. Only draft or cancelled campaigns can be deleted.


Campaign stats

GET /v1/campaigns/:id/stats

Requires read_only scope. Returns delivery statistics for a sent campaign.

{
  "campaign_id": "camp_abc123",
  "total": 4200,
  "sent": 4198,
  "delivered": 4100,
  "opened": 1722,
  "clicked": 492,
  "bounced": 98,
  "complained": 4,
  "unsubscribed": 12
}

Plan limits

PlanCampaigns
Free3
Pro25
BusinessUnlimited
EnterpriseUnlimited

Marketing emails sent via campaigns count toward the marketingEmailsPerMonth quota. Free plan does not allow marketing emails (quota = 0).


A/B testing

A/B-test a broadcast by setting an ab_test object on create or update. Variant B can override the subject, preview text, HTML, plain text, and/or template. Any unset variant-B field falls back to variant A's value.

Create with A/B test

curl -X POST https://api.sendry.online/v1/campaigns \
  -H "Authorization: Bearer sn_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "March newsletter",
    "subject": "What is new in March",
    "from": "news@acme.com",
    "audience_id": "aud_abc123",
    "html": "<p>Variant A body</p>",
    "ab_test": {
      "enabled": true,
      "split_percent": 50,
      "sample_percent": 80,
      "winner_metric": "open_rate",
      "evaluate_after_minutes": 60,
      "variant_b": {
        "subject": "March: a fresh take",
        "html": "<p>Variant B body</p>"
      }
    }
  }'
FieldTypeRequiredDescription
enabledboolyesTurns A/B testing on for this campaign.
split_percentnumberno1–99. % of the test sample receiving variant B. Default 50.
sample_percentnumberno1–100. % of the audience that participates in the test. Rest is holdback.
winner_metricstringcond."open_rate" or "click_rate". Required when sample_percent < 100.
evaluate_after_minutesnumberno5–10080. How long to wait before auto-picking. Default 60.
variant_b.subjectstringnoOverride for B's subject. Falls back to A.
variant_b.preview_textstringnoOverride for B's preview text.
variant_b.htmlstringnoOverride for B's HTML.
variant_b.text_contentstringnoOverride for B's plain text.
variant_b.template_idstringnoOverride template for B.

When enabled is true, at least one variant_b override must be set.

Get A/B test results

GET /v1/campaigns/{id}/ab-test/results
{
  "enabled": true,
  "variant_a": { "sent": 400, "opened": 120, "clicked": 30, "open_rate": 0.30, "click_rate": 0.075 },
  "variant_b": { "sent": 400, "opened": 160, "clicked": 28, "open_rate": 0.40, "click_rate": 0.070 },
  "winner_variant": null,
  "winner_evaluated_at": null,
  "winner_metric": "open_rate",
  "holdback_total": 200,
  "sample_percent": 80,
  "split_percent": 50,
  "evaluate_after_minutes": 60
}

Manually pick a winner

Requires sending_access scope. Setting a winner enqueues the holdback recipients (if any) to receive the winning variant's content.

curl -X POST https://api.sendry.online/v1/campaigns/cp_abc/ab-test/pick-winner \
  -H "Authorization: Bearer sn_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "variant": "b" }'

Returns the same shape as GET /ab-test/results with the winner now set.

Auto-pick

When sample_percent < 100 and winner_metric is set, the ab-test-evaluator worker polls every 5 minutes after send_started_at + evaluate_after_minutes and picks the winner deterministically (higher rate wins; ties → "a"). The holdback recipients then receive the winning variant's content via the same campaign-send worker.

On this page