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
| Name | Type | Description |
|---|---|---|
| input | string | HTML to sanitize. Required field. Maximum 512000 characters. |
Example
{
"input": "<div>Hello <b>world</b></div>"
}Response
{
"sanitizedHtml": "<p>Hello <strong>world</strong></p>"
}| Status | Data |
|---|---|
| 200 | Returns the cleaned HTML under sanitizedHtml |
| 400 | Missing or non-string input, or over the length limit: returns an Error object |
| 401 | Unauthorized: returns an Error object |
Output tags
The result only ever contains these tags, with only these attributes.
| Tag | Attributes kept |
|---|---|
| p, h1–h5 | style (text-align only, with value left, center or right) |
| ul, ol, li | — (<li> alignment is not supported and is dropped) |
| br | — |
| span | — |
| a | href, 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 | — |
| img | src, alt, style, data-image-class, data-giphy-id, title, width, height, data-scale, data-rel, data-eid |
| figure | style, data-rel, data-align |
| figcaption | — (plain text only) |
| video | src, poster, preload, data-rel, data-eid |
Each attribute below is dropped on its own when the schema would reject its value; the element stays:
| Attribute | Kept 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-thumbnail | an absolute URL with a scheme href accepts (http, https, mailto, ftp, nntp, news, tel) |
data-file-size, data-thumbnail-width, data-thumbnail-height, width, height | a whole number, 0 or more (10px, 50%, 1.5, -1 are dropped) |
data-scale | a whole number from 1 to 100 |
data-eid | a valid EID |
preload | none |
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:
| Kind | Shape |
|---|---|
| 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 ownsrctakes the first<source>'s;<source>children andcontrolsare 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 isdata-rel="file"/"embed"or the anchor isdata-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:
- An unsupported block tag at the top level is replaced by
<p>, keeping its content —<div>,<section>,<blockquote>,<pre>. - A block tag nested inside a text block becomes an inline
<span>— like<p>inside an<li>, as Google Docs produces. - 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:
| Input | Output | Why |
|---|---|---|
<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: media | removed where it sits, nothing else moves | Only absolute http/https media is accepted downstream |
<ol start="5"> | <ol> | Numbered lists always count from 1 |
<ul><ul><li>, <li> with no list | items joined into the surrounding list, or wrapped in <ul> | Every <li> sits directly in a <ul>/<ol> |
| a list nested five levels deep | its items become siblings on the fourth level | Four levels of list nesting is the limit |
<table>, <sub>, <sup>, <hr>, <audio> | content only, tag gone | No equivalent in the schema |
<script>, <style> | removed with their content | Never rendered |
Void tags come back XML-self-closed (<br />, <img />) so the output parses as XML.