HTML Sanitizer

Coerces externally-sourced rich-text HTML — a paste from Word or Google Docs, an import, a migration — into the tag set the Speakap rich-text schema allows.

Unlike validation, it never rejects. Anything unsupported is rewritten into the nearest allowed shape, or dropped; the surrounding text always survives. Run a paste through this endpoint before sending it as htmlBody, and content that would have failed validation with a 400 is accepted instead.

ENDPOINT

POST https://api.speakap.io/html/sanitizer

REQUEST

  • URI: /html/sanitizer
  • Method: POST
  • Content-Type: application/json
  • Authentication: OAuth 2.0

Request Body

NameTypeDescription
inputstringHTML to sanitize. Required field. Maximum 512000 characters.

Example

{
    "input": "<div>Hello <b>world</b></div>"
}

Response

{
    "sanitizedHtml": "<p>Hello <strong>world</strong></p>"
}
StatusData
200Returns the cleaned HTML under sanitizedHtml
400Missing or non-string input, or over the length limit: returns an Error object
401Unauthorized: returns an Error object

Output tags

The result only ever contains these tags, with only these attributes.

TagAttributes kept
p, h1–h5style (text-align only, with value left, center or right)
ul, ol, li— (<li> alignment is not supported and is dropped)
br—
span—
ahref, target, rel, download, data-file-type, data-upload, data-content-type, data-description, data-embed-type, data-name, data-presentation, data-provider, data-title, data-embed-url, data-preview, data-thumbnail, data-file-size, data-thumbnail-width, data-thumbnail-height, data-rel, data-eid
strong, i, u, s—
imgsrc, alt, style, data-image-class, data-giphy-id, title, width, height, data-scale, data-rel, data-eid
figurestyle, data-rel, data-align
figcaption— (plain text only)
videosrc, poster, preload, data-rel, data-eid

Each attribute below is dropped on its own when the schema would reject its value; the element stays:

AttributeKept when
data-rel on <a>attachment or mention (attachment only on a figure anchor)
data-rel on <img>, <video>attachment
data-rel on <figure>always set, see Media blocks
data-align on <figure>left, center or right
data-preview, data-embed-url, data-thumbnailan absolute URL with a scheme href accepts (http, https, mailto, ftp, nntp, news, tel)
data-file-size, data-thumbnail-width, data-thumbnail-height, width, heighta whole number, 0 or more (10px, 50%, 1.5, -1 are dropped)
data-scalea whole number from 1 to 100
data-eida valid EID
preloadnone

Text attributes such as data-title, data-description or data-embed-type are kept as given; data-embed-type is free text. Every other attribute — class included — is stripped.

Media blocks

Media always ends up as its own top-level <figure> with a data-rel that names its kind:

KindShape
image<figure data-rel="image"><img /><figcaption>plain text</figcaption></figure> (caption optional)
giphy<figure data-rel="giphy"><img /></figure> — only when the input figure said giphy
video<figure data-rel="video"><video src></video></figure>
file<figure data-rel="file"><a data-rel="attachment"></a></figure> — data-rel="attachment" is added when missing
embed<figure data-rel="embed"><a></a></figure> — only when the input figure said embed

A <figure> always holds exactly one media element. Everything else in it is moved out:

  • Several images or videos become one figure each, in order.
  • Paragraphs, headings and loose text follow the figure(s) as <p>/block content; a block left empty once its media is taken out is dropped.
  • The first caption goes to the image figure it belongs to (the one before it, otherwise the next one); captions of other kinds, and any extra caption, become a <p> after the figure.
  • A figure without any media is unwrapped: its caption or text becomes a <p>, an empty one disappears.
  • A <video> without its own src takes the first <source>'s; <source> children and controls are dropped.
  • A link around media (<a href><img></a>) is dropped; the media keeps its own block.
  • An <a> inside a figure becomes a file/embed block only when the figure is data-rel="file"/"embed" or the anchor is data-rel="attachment", and it wraps no media; its text is kept as plain text. Any other link in a figure is ordinary paragraph text.

Coercion rules

Three rules cover the general case:

  1. An unsupported block tag at the top level is replaced by <p>, keeping its content — <div>, <section>, <blockquote>, <pre>.
  2. A block tag nested inside a text block becomes an inline <span> — like <p> inside an <li>, as Google Docs produces.
  3. Anything else unsupported is reduced to its text, wrapped in a <p> when it would otherwise sit loose at the top level.

Specific behaviours worth knowing:

InputOutputWhy
<b>, <em><strong>, <i>Normalised to one spelling per style
<span style="font-weight:bold"><strong>Inline styling becomes the matching tag
<p><span style="text-align:center"><p style="text-align: center;"><span>Alignment is a block concern, so it moves up to the <p> or heading
<h2 style="text-align:center"><h2 style="text-align: center;">Headings keep their alignment like paragraphs
<p style="text-align:justify">, start, end<p>Only left, center and right alignment is kept
<h6><h5>The editor offers five heading levels; alignment is kept
<p>text<img></p><p>text</p><figure data-rel="image"><img /></figure>An image always gets its own top-level <figure>
<ul><li>a</li></ul><ul><li><span>a</span></li></ul>List item text always sits in a <span>
<ul><li><p>a</p><h2>b</h2></li></ul><ul><li><span>a</span><span>b</span></li></ul><li> takes inline content, not blocks
<ul><li>a<img>b<ul><li>c</li></ul></li><li>d</li></ul><ul><li><span>ab</span></li></ul><figure data-rel="image"><img /></figure><ul><li><span>c</span></li><li><span>d</span></li></ul>Media goes right after its list item's own text and splits the list; the rest starts again at the top level, nested items keep their shape
<img src="/a.png">, javascript: or data: mediaremoved where it sits, nothing else movesOnly absolute http/https media is accepted downstream
<ol start="5"><ol>Numbered lists always count from 1
<ul><ul><li>, <li> with no listitems joined into the surrounding list, or wrapped in <ul>Every <li> sits directly in a <ul>/<ol>
a list nested five levels deepits items become siblings on the fourth levelFour levels of list nesting is the limit
<table>, <sub>, <sup>, <hr>, <audio>content only, tag goneNo equivalent in the schema
<script>, <style>removed with their contentNever rendered

Void tags come back XML-self-closed (<br />, <img />) so the output parses as XML.