SonarSend API Endpoints Email library

Email library

Reusable emails with immutable version history.

10 endpoints Base URL · https://api.sonarsend.com Auth · X-API-Key
The Message object 10 fields
created_atdate-time
current_contentobject · nullable

The frozen content of current_version (subject, bodies, optional template provenance), or null when current_version is 0. Present on the single-entry GET.

current_versioninteger

The latest appended version number. 0 before any content is written.

iduuid

Canonical entry ID.

namestring
slugstring

Stable per-tenant key, derived from name when omitted. Address the entry by it instead of the UUID.

statusenum

Lifecycle. archived hides the entry while keeping its history.

One of: draft, published, archived
tagsstring[]
tenant_iduuid
updated_atdate-time

List library entries

GET /t/{slug}/api/messages/

A browsable, OFFSET-paged list of library entries — so it carries a total, and has_more is derived from it. Filter by status and substring-match name with search. Each row is the entry metadata (no version content).

status string
search string
limit integer
offset integer

A page of Message objects, under data.

200

A page of library entries.

400

The request failed validation, or a referenced record was rejected.

404

The tenant slug, or the record the path names, does not exist.

Request
curl -X GET "https://api.sonarsend.com/t/acme/api/messages" \
  -H "X-API-Key: $SONARSEND_API_KEY"
Response 200
{
  "data": [
    {
      "id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
      "tenant_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
      "name": "Acme onboarding",
      "slug": "acme",
      "tags": [
        "string"
      ],
      "status": "draft"
    }
  ],
  "next_cursor": "eyJpZCI6IjljMjE4OTVkIn0",
  "has_more": true,
  "total": 0
}

Create a library entry

POST /t/{slug}/api/messages/

Creates a library entry. Only name is required. Supplying content creates version 1 at the same time; omitting it creates an entry with no versions yet (current_version: 0) to fill in later with PATCH. slug is derived from name when omitted and is unique per workspace. content accepts either raw content (subject + body_html/body_mjml) or template-mode (template_id + template_binding).

namestringRequired
contentobject
slugstring
statusenum
One of: draft, published, archived
tagsstring[]

The Message object.

201

The library entry was created.

400

The request failed validation, or a referenced record was rejected.

404

The tenant slug, or the record the path names, does not exist.

Request
curl -X POST "https://api.sonarsend.com/t/acme/api/messages" \
  -H "X-API-Key: $SONARSEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme onboarding",
    "slug": "acme",
    "tags": [
      "string"
    ],
    "status": "draft",
    "content": {}
  }'
Response 201
{
  "id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
  "tenant_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
  "name": "Acme onboarding",
  "slug": "acme",
  "tags": [
    "string"
  ],
  "status": "draft"
}

Get a library entry

GET /t/{slug}/api/messages/{id}

The entry plus current_content — the frozen content of its current version, or null when the entry has no version yet.

id string Required

The Message object.

200

The library entry, with its current version's content.

400

The request failed validation, or a referenced record was rejected.

404

The tenant slug, or the record the path names, does not exist.

Request
curl -X GET "https://api.sonarsend.com/t/acme/api/messages/9c21895d-57f0-4a15-a1df-4dc6835d4f80" \
  -H "X-API-Key: $SONARSEND_API_KEY"
Response 200
{
  "id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
  "tenant_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
  "name": "Acme onboarding",
  "slug": "acme",
  "tags": [
    "string"
  ],
  "status": "draft"
}

Update a library entry

PATCH /t/{slug}/api/messages/{id}

Updates metadata and/or content. Metadata (name, slug, tags, status) updates in place. Sending content APPENDS a new immutable version and moves current_version — it never modifies the existing version. You can send metadata, content, or both in one call; the two are applied atomically, so a bad version insert cannot leave metadata changed behind a 400.

id string Required
contentobject
namestring
slugstring
statusenum
One of: draft, published, archived
tagsstring[]

The Message object.

200

The updated entry.

400

The request failed validation, or a referenced record was rejected.

404

The tenant slug, or the record the path names, does not exist.

Request
curl -X PATCH "https://api.sonarsend.com/t/acme/api/messages/9c21895d-57f0-4a15-a1df-4dc6835d4f80" \
  -H "X-API-Key: $SONARSEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme onboarding",
    "slug": "acme",
    "tags": [
      "string"
    ],
    "status": "draft",
    "content": {}
  }'
Response 200
{
  "id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
  "tenant_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
  "name": "Acme onboarding",
  "slug": "acme",
  "tags": [
    "string"
  ],
  "status": "draft"
}

Delete a library entry

DELETE /t/{slug}/api/messages/{id}

Hard-deletes the entry and its versions. Refused with 409 IN_USE while ANYTHING still references it — including a sequence send node that points at it without pinning a version. Archive instead if it shipped. Returns 503 UNAVAILABLE when the usage resolver is not wired up in this environment (the server declines rather than risk an unchecked removal). Returns 204 on success.

id string Required
204

The entry was deleted.

400

The request failed validation, or a referenced record was rejected.

404

The tenant slug, or the record the path names, does not exist.

Request
curl -X DELETE "https://api.sonarsend.com/t/acme/api/messages/9c21895d-57f0-4a15-a1df-4dc6835d4f80" \
  -H "X-API-Key: $SONARSEND_API_KEY"

List an entry's versions

GET /t/{slug}/api/messages/{id}/versions

Every version of an entry, newest first. Versions are append-only and immutable. When usage resolution is configured, each version carries a usage array naming which sequences pin it. Not paginated.

id string Required
dataobject[]
contentobject

The frozen resolved content (subject, bodies, optional template provenance). Shape varies — modelled permissively.

created_atdate-time
iduuid
message_iduuid
usageobject[]

Present on the versions list when usage resolution is configured: which sequences/broadcasts committed this version ({ version, kind, name, status }).

versioninteger

Version number, 1-based and append-only.

has_moreboolean
next_cursorstring · nullable

Opaque. Pass back as cursor for the next page. Null when there is no continuation — which for most endpoints means the last page, and always means a collection that is not paginated. The account-wide event feed is the exception: it returns a resumable cursor even on its final page (see that endpoint).

200

The entry's versions, newest first.

400

The request failed validation, or a referenced record was rejected.

404

The tenant slug, or the record the path names, does not exist.

Request
curl -X GET "https://api.sonarsend.com/t/acme/api/messages/9c21895d-57f0-4a15-a1df-4dc6835d4f80/versions" \
  -H "X-API-Key: $SONARSEND_API_KEY"
Response 200
{
  "data": [
    {
      "id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
      "message_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
      "version": 0,
      "content": {},
      "usage": [
        {}
      ],
      "created_at": "2026-01-15T09:30:00.000Z"
    }
  ],
  "next_cursor": "eyJpZCI6IjljMjE4OTVkIn0",
  "has_more": true
}

Get one version

GET /t/{slug}/api/messages/{id}/versions/{version}

One immutable frozen version by its number. version must be a positive integer; 404 NOT_FOUND if that number does not exist.

id string Required
version string Required
contentobject

The frozen resolved content (subject, bodies, optional template provenance). Shape varies — modelled permissively.

created_atdate-time
iduuid
message_iduuid
usageobject[]

Present on the versions list when usage resolution is configured: which sequences/broadcasts committed this version ({ version, kind, name, status }).

versioninteger

Version number, 1-based and append-only.

200

The frozen version.

400

The request failed validation, or a referenced record was rejected.

404

The tenant slug, or the record the path names, does not exist.

Request
curl -X GET "https://api.sonarsend.com/t/acme/api/messages/9c21895d-57f0-4a15-a1df-4dc6835d4f80/versions/{version}" \
  -H "X-API-Key: $SONARSEND_API_KEY"
Response 200
{
  "id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
  "message_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
  "version": 0,
  "content": {},
  "usage": [
    {}
  ],
  "created_at": "2026-01-15T09:30:00.000Z"
}

Archive a library entry

POST /t/{slug}/api/messages/{id}/archive

Soft-hides the entry (status → archived) while keeping its full version history. The right move for "we're done with this but it shipped"; use delete for mistakes.

id string Required

The Message object.

200

The archived entry.

400

The request failed validation, or a referenced record was rejected.

404

The tenant slug, or the record the path names, does not exist.

Request
curl -X POST "https://api.sonarsend.com/t/acme/api/messages/9c21895d-57f0-4a15-a1df-4dc6835d4f80/archive" \
  -H "X-API-Key: $SONARSEND_API_KEY"
Response 200
{
  "id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
  "tenant_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
  "name": "Acme onboarding",
  "slug": "acme",
  "tags": [
    "string"
  ],
  "status": "draft"
}

Duplicate a library entry

POST /t/{slug}/api/messages/{id}/duplicate

Deep-copies the entry and all its versions into a fresh draft named "X (copy)". Returns 201 with the new entry.

id string Required

The Message object.

201

The new copy.

400

The request failed validation, or a referenced record was rejected.

404

The tenant slug, or the record the path names, does not exist.

Request
curl -X POST "https://api.sonarsend.com/t/acme/api/messages/9c21895d-57f0-4a15-a1df-4dc6835d4f80/duplicate" \
  -H "X-API-Key: $SONARSEND_API_KEY"
Response 201
{
  "id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
  "tenant_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
  "name": "Acme onboarding",
  "slug": "acme",
  "tags": [
    "string"
  ],
  "status": "draft"
}

Preview a template rebase

POST /t/{slug}/api/messages/{id}/rebase-preview

For entries built from a template: recomputes a version's stored template binding against the template's CURRENT schema and returns { merged_binding, merged_snapshot, diff } — what a template change would do to this email. Computes and returns; writes nothing (hence messages:read despite being a POST). Defaults to current_version when version is omitted. Returns 400 NO_CONTENT if the entry has no saved version, 404 NOT_FOUND if that version does not exist, and 400 NOT_TEMPLATE_MODE if the entry was not built from a template.

id string Required
versioninteger
200

The rebase preview: merged_binding, merged_snapshot, diff. Modelled permissively.

400

The request failed validation, or a referenced record was rejected.

404

The tenant slug, or the record the path names, does not exist.

Request
curl -X POST "https://api.sonarsend.com/t/acme/api/messages/9c21895d-57f0-4a15-a1df-4dc6835d4f80/rebase-preview" \
  -H "X-API-Key: $SONARSEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "version": 0
  }'
Response 200
{}