What the Toolkit does
Upload vendor HTML (or an .eml), and the Toolkit pre-flights and converts it for your ESP: personalization tokens become your platform's merge tags using your team's saved variables, donation/action links are swapped and UTM-tagged, a compliant unsubscribe block is injected when one is missing, broken body structure is repaired, and a pre-flight report checks mobile rendering, dark mode, alt text, leftover tokens, and attribution before anything is sent. Every change lands in a plain-language ledger and your team's history.
Supported platforms: Iterable, Mailchimp, Klaviyo, ActiveCampaign, HubSpot, SendGrid, Salesforce Marketing Cloud, Campaign Monitor, and a custom option. Teams with an Iterable, Mailchimp, or SendGrid key connected on the Account page can upload the processed template to their platform in one click.
Server API
Everything the tool page does, as one deterministic call. Processing only — the API never uploads to an ESP and never sends email.
Endpoint & auth
POST https://theemailtoolkit.com/api/process X-API-Key: etk_live_... Content-Type: application/json
One key per account, managed by your team admin on Account → Server API (generate, rotate, revoke; the key is shown once and stored hashed). Rate limit: 60 requests/minute per key.
Request
| field | type | meaning |
|---|---|---|
html | string | raw vendor HTML, ≤ 2 MB. Exactly one of html/eml is required |
eml | string | a raw .eml message instead of html — converted like the tool's .eml upload (HTML body extracted, cid: images embedded) |
esp | string | "iterable", "mailchimp", "klaviyo", "activecampaign", "hubspot", "sendgrid", "sfmc", "campaignmonitor", "custom" |
link_replacements | object | original URL → replacement URL, exact match against href values (≤ 200). Omitted or blank = keep that link |
utm | object | {"name": "<saved UTM set>"} or inline {source, medium, campaign, content}, plus overwrite (default false). Merged onto every donation/action link, preserving existing parameters unless overwrite |
text_replacements | array | ordered {find, replace} literal pairs (≤ 200), applied after automatic token conversion; empty replace deletes |
inject_unsubscribe | bool | default true; false skips injection and ledgers the warning |
unsubscribe_block | string | custom HTML used instead of the standard block when one is injected |
mobile_safety_net | bool | default false; viewport meta + targeted max-width media query, ledgered as Mobile: lines |
vendor_variants | object | {"target_url": ..., "vendors": [{"name", "url"}, ...]} (≤ 20) — one HTML per vendor from the processed output, like the tool's vendor zip |
qc_report | bool | default false; include the exportable QC report as {html, filename} |
filename | string | label for the team-history record and variant filenames (default api-request) |
Personalization semantics
Detected tokens (bracket, %%…%%, AMPscript %%=…=%%,
%VAR%, *|…|*, ${…}, bare FNAME-style) convert
to the target platform using the team's saved variables first, then the
platform's stock tags. A fallback embedded in the source token — the Salesforce
IIF(…, "Friend", …) pattern — carries into the output
({{defaultIfEmpty FirstName "Friend"}}). A Handlebars greeting
{{#if firstName}}{{firstName}}{{else}}Patriot{{/if}} converts to
{{defaultIfEmpty FirstName "Patriot"}}; any other Handlebars conditional is
left in place, reported in changes, and flagged by pre-flight. Tokens with no known
mapping and FreeMarker logic blocks are never deleted — they stay in place and are reported.
Response
{
"processed_html": "<html>…</html>",
"changes": [
"https://old.example/give?amount=10 → https://new.example/give?amount=10&utm_source=em (×3)",
"Personalization: %%FirstName%% → {{defaultIfEmpty FirstName "Patriot"}} (×2)",
"Unsubscribe block injected before </body>"
],
"preflight": [{"status": "warn", "text": "…"}, …],
"history": {"id": 123, "html_stored": true, "size": 48213, "limit": 300000}
}
changes is the complete ledger — one entry per mutation, with
full, untruncated URLs, plus explicit notes for anything the API declined to do
(unknown replacement URLs, unmappable tokens, FreeMarker/Handlebars logic left for review).
Verify a run by reading changes. preflight is the tool page's full
pre-flight report, warnings first; it never alters processing. Every run is recorded to team
history, attributed as API.
Errors
| status | error |
|---|---|
| 400 | invalid_json, missing_field, invalid_esp, invalid_link_replacements, invalid_utm, utm_set_not_found, invalid_text_replacements, invalid_vendor_variants, too_many_vendors, invalid_filename, invalid_unsubscribe_block |
| 401 | missing_api_key, invalid_api_key |
| 402 | subscription_inactive |
| 413 | html_too_large (2 MB), request_too_large (request bodies over 10 MB are refused outright) |
| 422 | too_many_tokens, vendor_output_too_large, eml_conversion_failed |
| 429 | rate limit |
Examples
curl -s https://theemailtoolkit.com/api/process \
-H "X-API-Key: $ETK_API_KEY" -H "Content-Type: application/json" \
-d '{"html": "<html>…</html>", "esp": "iterable",
"link_replacements": {"https://old.example/give": "https://new.example/give?utm_source=em&utm_medium=email&utm_campaign=20261020_oct"},
"filename": "vendor-blast.html"}'
import requests
r = requests.post(
"https://theemailtoolkit.com/api/process",
headers={"X-API-Key": ETK_API_KEY},
json={"html": raw_html, "esp": "iterable",
"utm": {"name": "Sept Prospecting"}, "filename": "vendor-blast.html"},
timeout=120,
)
r.raise_for_status()
out = r.json()
open("processed.html", "w").write(out["processed_html"])
for line in out["changes"]:
print(line)
Same input + same account state (saved variables, UTM sets) → byte-identical output. The one non-deterministic field is the opt-in QC report's timestamp.