Campaigns
Promotional banners and info-boxes that ride alongside delivered mail, targeted by tag, scheduled, reviewed before they go live, and measurable, without becoming a content type.
A campaign is a promotional overlay (a banner or info-box) that Keepable renders above or below a delivered mailpiece. It is not a content type and it is not a separate delivery. You author a campaign once, target it with a tag, and any mail that carries that tag picks it up at render time.
A campaign never changes what you send. A delivery opts into a campaign by
carrying a campaign_tag in its metadata (on a single send, a batch, or an
ingest job), never by setting content_type: campaign. There is no
campaign content type.
Campaigns use their own scopes, separate from content delivery:
campaigns.write to author and run them, campaigns.read to read them and
their metrics. See Authentication.
Lifecycle
A campaign moves through a small, explicit set of states. You only ever change
state through the dedicated endpoints: PUT /campaigns/{id} replaces the
config but never the status.
| Status | Meaning |
|---|---|
draft | Created and editable. Not shown to any recipient yet. |
pending_review | Submitted with publish; waiting on Keepable's human review gate. |
scheduled | Approved and live, eligible to render on tagged mail. |
paused | Temporarily withdrawn from rendering; can be resumed by publishing again. |
archived | Terminal. Immutable; writes return 404. |
draft ──publish──▶ pending_review ──review──▶ scheduled ──pause──▶ paused
▲ │
└──────publish──────┘
(archive ─▶ archived, terminal from any non-archived state)Every campaign goes through review before it can render. This is a deliberate guardrail: promotional content is held to a moderation gate (and DPO/counsel sign-off) before it can appear next to official mail.
Create a campaign
POST /campaigns creates the campaign in draft. The new id is also returned
in the keepable-campaign-id header.
curl https://api.keepable.co/sender/campaigns \
-H "Authorization: Bearer $KEEPABLE_TOKEN" \
-H "Keepable-Version: 2026-05-24" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "June savings push",
"placement": "bottom",
"headline": "Earn 12% on a 90-day savings note",
"body": "Open a fixed savings note from your inbox in two taps.",
"cta_text": "See offers",
"cta_url": "https://example.com/offers",
"show_at": "2026-06-01T00:00:00Z",
"hide_at": "2026-06-30T23:59:59Z",
"targeting": { "tag": "june-savings" }
}'| Field | Required | Notes |
|---|---|---|
name | yes | Internal label for the campaign. |
placement | no | Where the banner renders relative to the mailpiece: top or bottom (default bottom). |
headline | no | Banner headline, up to 80 chars. Composed into a fixed template and escaped at render time, so there is no stored-HTML surface. |
body | no | Supporting banner copy, up to 240 chars. Same safe-render treatment. |
cta_text | no | The call-to-action label. |
cta_url | no | Where the CTA points. Clicks are tracked through a Keepable redirect. |
hero_image_ref | no | An uploaded asset ref (below) used as a 2:1 hero image. |
show_at / hide_at | no | Optional display window (RFC 3339). Outside it, the banner does not render. |
targeting | no | Audience selection (below). An empty or omitted object targets every delivery from this sender. |
The creative is structured: you supply headline, body, a CTA, and an
optional image, and Keepable composes them into a fixed, escaped template. You
never send banner HTML.
Targeting
targeting selects which deliveries the banner rides on. Every field is
optional:
| Field | Notes |
|---|---|
tag | Matched exactly against a delivery's metadata.campaign_tag. The primary, integration-driven mode. |
content_types | Restrict to deliveries of these content types. |
send_date_from / send_date_to | Restrict to deliveries generated in a date window. |
recipient_ids | An explicit recipient id-list audience. |
Banner imagery is added in a second step with POST /campaigns/{id}/assets
(image/png, image/jpeg, image/gif, or image/webp, up to 512 KiB); list
and remove them under the same path, and reference one as hero_image_ref. Read
engagement with GET /campaigns/{id}/metrics.
Targeting a delivery
To put a campaign in front of a recipient, tag the mail. The campaign's
targeting.tag and the delivery's metadata.campaign_tag must match.
{
"recipient": { "identifier_type": "nin", "identifier": "12345678901" },
"subject": "Your March statement",
"content_type": "statement",
"generated_at": "2026-05-24T09:00:00Z",
"metadata": { "campaign_tag": "june-savings" },
"parts": [ { "name": "statement.pdf", "media_type": "application/pdf", "data": "JVBERi0xLjcK..." } ]
}The same metadata.campaign_tag works on a batch send
(Mode A metadata, or a Mode B manifest column), so a whole run can opt into a
campaign at once.
Guardrails
Campaigns sit next to official, identity-anchored mail, so they are deliberately constrained:
- Recipients can opt out of promotional banners, and the opt-out is honored at render time. A banner is never shown to a recipient who declined.
- Identity-critical mail is protected. Banners are not rendered on the most sensitive types (statements, payslips, agreements) by default.
- Every go-live is reviewed.
publishmoves the campaign topending_review; it cannot render until a human approves it. - Clicks are tracked server-side through a redirect, so the
cta_urldestination never sees Keepable identifiers.
Because targeting is by tag, a typo in campaign_tag simply means the mail
renders with no banner. It is never an error. Keep the tag a stable constant
in your sender code, set once where you build the metadata.
Forms
Publish reusable form templates that recipients fill in from their inbox, then read the responses back. Covers field types, template lifecycle, and collecting responses.
Access delegation
Let one tenant act on behalf of another. Request access to a tenant, and accept or reject requests targeting yours.