Keepable docs
Sender API

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.

StatusMeaning
draftCreated and editable. Not shown to any recipient yet.
pending_reviewSubmitted with publish; waiting on Keepable's human review gate.
scheduledApproved and live, eligible to render on tagged mail.
pausedTemporarily withdrawn from rendering; can be resumed by publishing again.
archivedTerminal. 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.

Create a campaign
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" }
  }'
FieldRequiredNotes
nameyesInternal label for the campaign.
placementnoWhere the banner renders relative to the mailpiece: top or bottom (default bottom).
headlinenoBanner headline, up to 80 chars. Composed into a fixed template and escaped at render time, so there is no stored-HTML surface.
bodynoSupporting banner copy, up to 240 chars. Same safe-render treatment.
cta_textnoThe call-to-action label.
cta_urlnoWhere the CTA points. Clicks are tracked through a Keepable redirect.
hero_image_refnoAn uploaded asset ref (below) used as a 2:1 hero image.
show_at / hide_atnoOptional display window (RFC 3339). Outside it, the banner does not render.
targetingnoAudience 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:

FieldNotes
tagMatched exactly against a delivery's metadata.campaign_tag. The primary, integration-driven mode.
content_typesRestrict to deliveries of these content types.
send_date_from / send_date_toRestrict to deliveries generated in a date window.
recipient_idsAn 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.

A tagged single send
{
  "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. publish moves the campaign to pending_review; it cannot render until a human approves it.
  • Clicks are tracked server-side through a redirect, so the cta_url destination 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.