Docs
API reference Sending Template & block import/export

Template & block import/export

Templates and content blocks round-trip as markdown. One file holds the body, the plain-text alternative, and the parameter/section/slot schemas, so a template can live in your repository, move between workspaces, or be generated by a script.

This is the file-level counterpart to template-mode, which covers filling a template in at send time.

Required scopes#

EndpointScope
GET /api/templates/{id}/exporttemplates:read
POST /api/templates/checktemplates:read
POST /api/templates/importtemplates:write
PUT /api/templates/{id}/importtemplates:write
GET /api/blocks/{id}/exporttemplates:read
POST /api/blocks/importtemplates:write
DELETE /api/templates/{id}templates:write
DELETE /api/blocks/{id}templates:write

templates:* covers blocks as well as templates — there is no separate blocks: scope.

Template endpoints accept an id or a slug in the path, so you can address a template by the stable name you gave it rather than storing a UUID.

The file format#

Front matter for metadata, ## headings for everything else, code blocks for content:

---
kind: template
name: My Template
format: mjml
---

## MJML

​```html
<mjml>…</mjml>
​```

## Plain text

​```
The text/plain alternative.
​```

## Parameters

​```json
[ … ]
​```

## Sections

​```json
[ … ]
​```

## Slots

​```json
[ … ]
​```

Required: kind: template, name, and a body section matching the format. Everything else is optional — omit any schema heading you don’t use.

  • format defaults to mjml. The only accepted values are mjml and html.
  • The body heading must match: ## MJML for format: mjml, ## HTML for format: html. Getting this wrong is the most common import failure.
  • ## Plain text and ## Plaintext are both accepted.
  • Blocks use kind: block.

Export#

curl "https://api.radarsend.com/t/acme/api/templates/welcome-email/export" \
  -H "X-API-Key: $API_KEY" \
  -o welcome-email.md

Responds with text/markdown; charset=utf-8 — the raw file, not JSON. Blocks export the same way from /api/blocks/{id}/export.

Markdown import currently creates a new block via POST /api/blocks/import. There is no markdown replacement endpoint for an existing block; update an existing block through the regular JSON block endpoint.

Import as a new template#

curl -X POST "https://api.radarsend.com/t/acme/api/templates/import" \
  -H "X-API-Key: $API_KEY" \
  -H 'Content-Type: text/markdown' \
  --data-binary @welcome-email.md

Returns 201 with the created template.

Two body forms are accepted, and they are equivalent:

Content-TypeBody
text/markdown or text/plainThe raw markdown
application/json{ "markdown": "---\nkind: template\n…" }

Use the JSON form when your HTTP client makes raw bodies awkward; use text/markdown when you are posting a file straight off disk. Anything else returns 400 INVALID_PAYLOAD.

Check a template without saving it#

curl -X POST "https://app.example.com/t/acme/api/templates/check" \
  -H "X-API-Key: $API_KEY" \
  -H 'Content-Type: application/json' \
  -d "$(jq -n --rawfile md welcome-email.md '{markdown: $md}')"

Runs the same validation as an import, without saving anything, and reports every problem at once rather than stopping at the first. It needs only templates:read. It is built for converting an email from another platform, so it also lints for what such an email leaves behind:

  • the previous platform’s merge tags (*|FNAME|*, %UNSUBSCRIBELINK%, …);
  • MJML 3 syntax that current MJML drops;
  • a missing {{unsubscribe_url}};
  • a recipient condition wrapped around a section;
  • HTML typed into a rich_text value;
  • http:// and relative URLs.
Body field
markdownRequired. The template, in the file format above.
bindingOptional. Content to render, shaped like template_binding on /render. Without it, each field renders its sample, then its default.

Always returns 200:

{
  "valid": false,
  "errors": [
    {
      "severity": "error",
      "code": "FOREIGN_MERGE_TAG",
      "message": "MJML: tags from another platform: *|FNAME|* (Mailchimp)",
      "fix": "Map each to its RadarSend equivalent …"
    }
  ],
  "warnings": [],
  "template": { "name": "Welcome", "slug": "welcome-email", "format": "mjml", "parameters": 3, "sections": 1, "slots": 0 },
  "preview": null
}
  • errors would block a save or break the email; warnings are worth a look. Every issue carries a machine-readable code and a one-line fix.
  • preview ({ binding, body_html, body_text }) is present only when there are no errors. Merge fields in it render for a sample recipient, and system links point at example.com.

This is the endpoint behind the MCP check_template tool, which also returns a screenshot of the preview.

Replace an existing template#

curl -X PUT "https://api.radarsend.com/t/acme/api/templates/welcome-email/import" \
  -H "X-API-Key: $API_KEY" \
  -H 'Content-Type: text/markdown' \
  --data-binary @welcome-email.md

Replaces the body and the schemas from the file. This is a replace, not a merge: a parameter absent from the uploaded file is removed from the template.

Check what depends on a template before replacing it:

curl "https://api.radarsend.com/t/acme/api/templates/welcome-email/usage" \
  -H "X-API-Key: $API_KEY"

Broadcasts that already rendered from a template are unaffected — their content is frozen at send time. The risk is to drafts and sequences still pointing at it.

Keeping templates in version control#

The round-trip is stable, which makes a plain git-based workflow practical:

# Pull every template into a directory
for slug in welcome-email monthly-digest; do
  curl -s "$BASE/api/templates/$slug/export" -H "X-API-Key: $API_KEY" \
    -o "templates/$slug.md"
done

# Push a change back
curl -X PUT "$BASE/api/templates/welcome-email/import" \
  -H "X-API-Key: $API_KEY" \
  -H 'Content-Type: text/markdown' \
  --data-binary @templates/welcome-email.md

Exporting the same template twice produces identical bytes, and bodies are stored verbatim — nothing is reformatted or minified on import. So a scheduled export is a reliable drift check: a diff means someone edited the template in the app.

One wrinkle on the first export after an import: if your file has no slug in its front matter, one is generated from name (and de-duplicated if it collides), so the export comes back carrying a slug your source did not have. Put an explicit slug in front matter and the file round-trips unchanged.

Deleting a template or block#

DELETE /api/templates/{id_or_slug}
DELETE /api/blocks/{id_or_slug}

Both need templates:write and return 204 on success.

A delete is refused while the template or block is still in use — 409 IN_USE, with the usage counts in details:

{
  "error": {
    "code": "IN_USE",
    "message": "Template is referenced by active drafts or recent broadcasts",
    "details": { "active_drafts": 2, "recent_broadcasts": 1 }
  }
}

For a template, “in use” means active drafts or recent broadcasts; for a block, active drafts only. Pass ?force=true to skip the check and delete anyway.

Think about force before you reach for it. It removes the definition that in-flight drafts still point at, and unlike an export/import round-trip there is nothing to restore from unless you kept the markdown. Exporting first (above) gives you that copy.

Errors#

CodeStatusMeaning
INVALID_PAYLOAD400Body was neither raw markdown nor { "markdown": … }.
IN_USE409Delete refused: still referenced. details carries the counts.
NOT_FOUND404No template or block with that id or slug.
INSUFFICIENT_SCOPE403The key is missing templates:write.
INVALID_PARAM_PATH400A parameter used with a sub-field, e.g. {{ param.hero.url }}. Parameters are used whole; declare a separate url parameter for an image or link.
INVALID_ITEM_REFERENCE400A section row field with a sub-field or filter, e.g. {{item:headline | upcase}}.

These two apply to every template and block save, not only imports: each is a spelling that used to save but rendered empty or broke the email. An update is checked only in the bodies it rewrites.

Malformed markdown — a missing required heading, unparseable JSON in a schema block — is reported as a validation error naming the section at fault.

See errors.md for the full envelope.