GET …/content
Docs / Sending
GET

/v1/messages/{id}/content

Decrypts and returns the retained body — subject, html, text, variables. Viewing content is a distinct, audit-logged act, never a side effect of listing messages.

Authentication

Requires messages:read-content. That ability is mutually exclusive with messages:send — the panel will not mint a key that has both, and resolution rejects one that does. A messages:read key without messages:read-content also gets 403.

Worked example

curl https://spoolway.com/api/v1/messages/2ee35675-6bae-42be-aa5e-6d4618d0c61d/content \
  -H "Authorization: Bearer spw_…"
200 OKcaptured LOCAL with messages:read-content
{
  "subject": "Reset password",
  "html": "<p>Reset <a href=\"https://basalt.app/reset/tk_9f2a81\">link</a> (30m)</p>",
  "text": "Reset https://basalt.app/reset/tk_9f2a81 (30m)",
  "variables": {
    "reset_url": "https://basalt.app/reset/tk_9f2a81",
    "expires_minutes": 30
  },
  "expires_at": null
}

Responses & failures

410body past retention
response body
{
  "message": "Content expired",
  "completed_at": null
}
What to do: stop retrying — the body is gone by policy. Metadata and delivery events remain on the detail endpoint.

Ciphertext present but undecryptable returns 503 with type: body_unreadable — never an empty successful body.

Audit

Every successful content read writes an audit event message.content_viewed with the API key id. That is why this endpoint is separate from list and show.