Keepable
Sender API

HTML and CSS

What a text/html mailpiece body may contain. The allowed tags, the inline-CSS property list, the kp-* branded blocks, and everything the sanitiser strips, so your branded mail renders the way you intend.

A correspondence's body_html lets you brand a mailpiece: your own colours, a hero card, a call-to-action button. To keep every recipient safe, Keepable sanitises the HTML on write: it stores only markup on the allowlist below, and strips the rest. Nothing you send can run a script, load a remote asset, track an open, or break out of its frame.

This page is the reference for exactly what survives. Send anything; only the allowlisted parts are kept. There are no surprises at delivery if you stay within it.

Prefer not to hand-write HTML? The portal composer builds all of this for you (including the branded blocks below) and shows a live preview of what the recipient will see. This page is for senders authoring HTML directly via the API or an upload.

Two ways to brand

There are two levels, and you can mix them:

  • The kp-* blocks (recommended): a small vocabulary of ready-made classes for panels, buttons, and text styles. They carry a light and dark variant, so they stay legible whichever theme the recipient uses. This is the safest way to get the branded "envelope" look.
  • Your own inline styles: for full control, set style="…" on any element, from an allowlisted set of CSS properties. A colour you set is fixed, so it will not adapt to dark mode (see Dark mode).

Allowed elements

GroupTags
Textp, br, span, strong/b, em/i, u, s/strike/del, ins, sub, sup, mark, small, code
Headingsh1-h6
Listsul, ol, li
Structurediv, section, header, footer, figure, figcaption, blockquote, hr
Tablestable, thead, tbody, tfoot, tr, th, td, caption, colgroup, col
Links & imagesa, img

Any tag not listed is unwrapped (its text is kept, the tag dropped). class is allowed on any element; your own class names are kept, though they only take effect for the kp-* vocabulary; Keepable does not accept sender <style> blocks, so a class of your own has no stylesheet to bind to.

Inline styles

Set style="…" on any element. Each declaration is validated against the property list below, and its value is checked (a colour must parse as a colour, a length as a length). A declaration that names a property outside this list, or carries an unsupported value, is dropped; the rest of the style is kept.

CategoryProperties
Colourcolor, background-color, opacity
Spacingpadding (+ -top/-right/-bottom/-left), margin (+ sides)
Borderborder (+ sides), border-color, border-width, border-style, border-radius
Sizewidth, max-width, min-width, height, box-sizing
Texttext-align, text-decoration, text-transform, vertical-align, font-weight, font-size, font-style, font-family, line-height, letter-spacing
Layoutdisplay, flex, flex-direction, flex-wrap, justify-content, align-items, align-self, float, list-style-type

There is no url() property here on purpose: not background-image, not list-style-image, not cursor. That is what makes styles safe: a declaration can never point at a URL, so it can never load a remote asset or become a tracking pixel. position is also excluded, so content cannot escape the reading flow.

The kp-* branded blocks

These classes are pre-styled by Keepable and adapt to light and dark mode. Use them for the branded look without setting a single colour yourself.

ClassOnWhat it does
kp-panelsectionA soft card: a bordered, padded block (a footer, an aside).
kp-panel kp-panel--brandsectionThe branded hero: a full-bleed, edge-to-edge coloured card, the "envelope" at the top of the letter.
kp-ledepThe larger intro line under a heading.
kp-buttonaStyles a link as a call-to-action button. Still a link: it degrades to underlined text anywhere the styling is absent.
kp-mutedpSmall print (contact details, fine print).
kp-contactulA plain, un-bulleted list for contact rows.
ka-align-center / ka-align-right / ka-align-justifyany blockText alignment.
<section class="kp-panel kp-panel--brand">
  <h1>Your April statement is ready</h1>
  <p class="kp-lede">Hi Ada, your account summary for April is ready to view.</p>
  <p>Everything you were sent is sealed and yours to keep.</p>
  <a class="kp-button" href="https://app.example.com/statement">View your statement</a>
</section>

Links may use http, https, mailto, or tel. Every link is given rel="nofollow"; javascript:, vbscript: and every other scheme is removed.

Images do not load. The recipient reads your body inside a sandboxed frame on an opaque origin, under default-src 'none' with img-src data:. A remote <img src="https://…"> survives the sanitiser and then fetches nothing, which is the tracking-pixel vector this design closes: a sender cannot learn when a letter was opened, and that is a promise the reader is given rather than a setting.

There is no way to carry your own image bytes alongside a body on this API. Send the picture-heavy version as a PDF document, which seals, and keep body_html for the words around it. See What your body is for how to say which of the two the recipient should read first.

What your body is

A body and a document on the same file are one of two things, and the recipient lays them out differently. body_role is how you say which:

  • cover (the default, and what you get by saying nothing): your body is a note that introduces the document. The recipient reads it above the document, and the document is drawn in full below, because your note is not a substitute for it.
  • document: your body is the document, rendered as HTML. This is the case when you send a prepared mailpiece beside the sealed PDF of the same letter. The recipient leads with your markup at full height and reduces the PDF to a preview they can open, because the pages underneath would otherwise be the same letter a second time.
POST https://api.keepable.co/sender/v2/correspondence
Authorization: Bearer {{KEEPABLE_TOKEN}}
Content-Type: application/json

{
  "kind": "letter",
  "subject": "Your January statement",
  "body_html": "<section>…the statement, as HTML…</section>",
  "body_role": "document",
  "document_ids": ["doc_01J8ZQ4T"],
  "recipients": [
    { "addressing": { "type": "nin", "nin": "12345678901" } }
  ]
}

document is a claim about both halves: send it only when a body and a document are both present and are the same content. On a file with no document it changes nothing. On a document your body does not actually render, it puts pages the reader needs behind a preview.

Reach for document when you already produce a designed letter in HTML and seal a PDF of it for the record. Reach for cover — or say nothing — for a covering message, which is most mail.

Dark mode

Recipients can read in light or dark mode. The kp-* blocks flip automatically. A colour you set with an inline style does not: a dark teal you choose for text stays dark teal, which may be unreadable on a dark background.

If your brand look must hold in both themes, build it from the kp-* blocks. If you set your own colours, set the background and the text together on the same element (as in a fully-coloured panel) so you control the contrast; don't rely on the recipient's theme behind your text.

Always removed

Regardless of the above, these are stripped every time; they are the reason the sanitiser exists:

  • <script>, event handlers (onclick, onerror, …), and any executable content
  • <style> blocks and external stylesheets (<link rel="stylesheet">)
  • url() in CSS, so no declaration can name an asset at all
  • <iframe>, <object>, <embed>, and javascript: / vbscript: / data: URLs
  • position in any form, so content cannot escape the reading flow

And in the frame the recipient reads it in, nothing external is fetched: no image, font, or stylesheet from a host you control, which is the same guarantee one layer down.

A full example

A complete branded letter (a hero, body, a CTA, and a contact footer) using only the allowlist:

<section class="kp-panel kp-panel--brand">
  <h1>Your orange envelope 2026</h1>
  <p class="kp-lede">Sign in to open your annual pension summary.</p>
  <ul>
    <li>Your pension forecast</li>
    <li>What you earned toward your pension this year</li>
    <li>Tips that affect your future pension</li>
  </ul>
  <a class="kp-button" href="https://app.example.com/open">Open your envelope</a>
</section>

<p>You can read your statement any time from your Keepable inbox.</p>

<section class="kp-panel">
  <h3>Questions?</h3>
  <p class="kp-muted">We are glad to help, any time.</p>
  <ul class="kp-contact">
    <li><a href="tel:+2340000000000">+234 000 000 0000</a></li>
    <li><a href="mailto:support@example.com">support@example.com</a></li>
  </ul>
</section>

Send it as the body_html of a correspondence; the recipient sees the branded card in both light and dark mode.

body_html is write-only. It is what the recipient reads, and it is not echoed back on any read endpoint, so keep your own copy of what you sent if you need to show it in your own system. {{key}} placeholders in it are filled per recipient from merge, HTML-escaped on the way in.

On this page