Messaging
The Messaging operations are for developers who send messages to their customers, or who manage how a tenant's messages look and where they go. A tenant's messages are built from templates and layouts (letterheads). A template is saved as a draft and goes live only when you publish it, and each publish keeps a version. A ready-made library gives you starting points. A binding routes an event topic to a template. Channels carry the messages, and each channel has its own credentials. You can send a message or preview one without sending it. The delivery log, stats and suppressions show what happened. Push subscriptions register browsers. Privacy erasure removes one person from the delivery log. The change log records changes to templates, layouts, suppressions and channel credentials, and it cannot be edited.
There are 45 operations, all under /v1/messaging. The channels are email, sms, whatsapp, push, webhook and inapp.
A message goes out in one of two ways. You call POST /v1/messaging/send with a template key, a channel and a recipient address. Or a binding sends it when an event arrives on its topic. Either way, the template must be published first: a template that only exists as a draft is not sent (a send answers 404), except as a test with draft: true.
Before you call
Every call needs one of the two credentials the gateway accepts: an API key in X-Revenexx-Api-Key, or a Bearer token. An API key must carry the messaging scope (or *); without it the call fails with 403 and the message api key missing scope: messaging. Send your tenant in X-Revenexx-Tenant on every call; without it the call fails with 400. See Authentication and API keys for how to get and send a credential.
Operations need one of two roles in the tenant. Reads, preview, send and push subscriptions need the user or admin role. Every write to templates, versions, layouts, bindings, suppressions, channel credentials and the configuration, every channel-credential operation (reads included), privacy erasure and the change log need admin. A credential without the role gets 403, with error.details.required_any_of naming the roles that would pass.
More responses you can meet:
422means a write or a filter was refused.error.detailsmaps each field that failed to its messages.- Two error bodies. A refusal by the gateway (missing tenant, missing scope, rate limit) has the flat shape described on Errors. An answer from the messaging operations themselves nests it:
{"error": {"code", "message", "details"}}. Branch onerror.code. 429means the rate limit was exceeded. Wait for theRetry-Afterheader before you retry. See Rate limits.
Templates
A template is addressed by its key and has an id. A template's content (subject, body_html, body_text, design, variables, variable_defaults, layout_id, message_class) is versioned: changes to it go into the draft, and customers receive the published version until you publish again. Every other field (enabled, test_mode, valid_from, valid_until, markets, title, content_sid, whatsapp_category) takes effect at once, without a publish: PATCH with enabled: false or test_mode: true on a live template stops real delivery immediately. key, channel and locale are set at create and cannot be changed.
| Operation | Path | What it does |
|---|---|---|
| List templates | GET /v1/messaging/templates | Lists the tenant's templates, by key. |
| Create a template | POST /v1/messaging/templates | Creates a template. The new row is a draft. |
| Read a template | GET /v1/messaging/templates/{id} | Returns the draft, not what is live. |
| Change a template | PATCH /v1/messaging/templates/{id} | Changes the draft. Only the fields you send are written. |
Change a template (PUT) | PUT /v1/messaging/templates/{id} | Same action as PATCH. |
| Delete a template | DELETE /v1/messaging/templates/{id} | Deletes the template. |
| Publish a template | POST /v1/messaging/templates/{templateId}/publish | Makes the current draft live. |
| List versions | GET /v1/messaging/templates/{templateId}/versions | Lists the publication history, newest first. |
| Read a version | GET /v1/messaging/templates/{templateId}/versions/{version} | Returns one published version, whole. |
| Restore a version | POST /v1/messaging/templates/{templateId}/versions/{version}/restore | Puts an older version's content back into the draft. |
{templateId} is the template's id.
Key request fields for POST /v1/messaging/templates:
| Field | Required | Notes |
|---|---|---|
channel | yes | The channel the template is sent on. |
key | yes | The name the template is addressed by. |
title | no | The name shown to people. Without it, a list has to derive a name from the key. |
subject, body_html, body_text | no | Message content. Send body_html and body_text yourself to skip compilation; a design sent in the same request overwrites them. |
design | no | A design document. The service compiles it against the template's layout. |
layout_id | no | The letterhead to use. null or absent means the tenant's current default layout, including a later change of default once the template is compiled again. |
locale | no | The template's language. |
markets | no | The markets the template is browsed in. [], the default, means global. |
message_class | no | transactional or marketing. The default is transactional. |
enabled | no | Switch. |
test_mode | no | true: a send renders and logs the message with status test, and nothing reaches the provider. The call still answers 201. |
valid_from, valid_until | no | The window in which the template is in force. |
variables, variable_defaults | no | Placeholders, and fallbacks for placeholders an event did not fill. |
content_sid | no | The Meta-approved template a WhatsApp message is sent as. |
whatsapp_category | no | marketing, utility, authentication or service. This sets the price Meta charges per message. |
How it behaves:
- A design the compiler refuses is
422, and nothing is written.error.detailsnames the block that failed. If the compiler is unavailable, the answer is503, which is worth retrying; a refused design is not. PATCHandPUTon a template write only the fields you send. An audit entry is written only when something changed.- Moving a template to another layout recompiles it, even when nothing else changed. A template whose
body_htmlyou wrote yourself is never recompiled. - Reclassifying
message_classchanges nothing already sent. Each message keeps the class it had when it was sent. - Recategorising
whatsapp_categorytakes effect on the next send. - Deleting a template leaves any binding that names its key with nothing to send when its event arrives.
GET /v1/messaging/templates/{id}returns the draft. To see what customers receive, read the published version.GET /v1/messaging/templates/{templateId}/versionsreturnsmeta.has_unpublished_changes, which says whether the draft has moved on since the last publish.- Market filter on the list. With the
X-Revenexx-Marketheader, the list shows the global templates and that market's templates. Without the header, it shows only the global ones.?markets=allreturns every row. The filter only narrows the browsing list. It does not change which tenant you can read. - Publish takes an optional
note. It answers201when a new version is written, and200with the version that is already live when there was nothing to publish. - Versions list returns summaries only, with no bodies. It sets
meta.published_version_id, which is the live version. Use the version read for the full snapshot. - Version read is addressed by the version number, the small integer on the history row, not by the snapshot's id. It returns what sends rendered while that version was live.
- Restore takes
publish: trueto make the restored content live in the same step. The response is the template, not the version.
Layouts
A layout is a letterhead. Every template that names no layout is framed by the tenant's default layout.
| Operation | Path | What it does |
|---|---|---|
| List layouts | GET /v1/messaging/layouts | Lists the tenant's letterheads, with the default first. |
| Create a layout | POST /v1/messaging/layouts | Creates a letterhead. |
| Read a layout | GET /v1/messaging/layouts/{id} | Returns one letterhead. |
| Change a layout | PATCH /v1/messaging/layouts/{id} | Changes a letterhead. |
| Delete a layout | DELETE /v1/messaging/layouts/{id} | Deletes a letterhead and reports what happened to the templates on it. |
Request fields for POST /v1/messaging/layouts and PATCH /v1/messaging/layouts/{id}:
| Field | Required | Notes |
|---|---|---|
name | yes on create | Up to 191 characters. |
is_default | no | Makes this layout the tenant's default. |
enabled, valid_from, valid_until | no | Switch and validity window. valid_until must not be before valid_from. |
markets | no | The markets the layout is browsed in. Never null. |
logo_url, legal_name, postal_address, support_email, footer_note, sender_name | no | Letterhead content. |
social_links, menu_links | no | Link lists. |
color_accent, color_bg, color_text | no | Hex colours. |
width, font_family | no | One of a fixed set of values. |
How it behaves:
- The first layout is always the default, whatever the request says. A tenant with no default cannot compile a template that names none.
- The default cannot have a validity window or be switched off. Asking for either is
422withlayout_default_always_in_force. The check looks at the outcome of the request, so promoting a layout and dating it in one request is also refused. - Clearing
is_defaulton the current default is422withlayout_default_required. To change the default, promote another layout. That clears this one. - A layout change reaches every template on it, including templates that were saved long ago. The structure (colours, width, font) is baked into each template's compiled body, so a template keeps its old structure until its
designis saved again. An audit entry is written only when something changed. GET /v1/messaging/layoutsis filtered by market, like the template list.GET /v1/messaging/layouts/{id}is not filtered.- Deleting a layout answers
200with a body, not204. The body isdatawithid,deletedandtemplates_reassigned, the number of templates that were on it, now moved onto the tenant's default layout. Their compiled body still carries the deleted letterhead's structure until each one'sdesignis saved again. - The default layout cannot be deleted (
422,layout_is_default). Make another layout the default first.
Library
| Operation | Path | What it does |
|---|---|---|
| List library templates | GET /v1/messaging/library | Lists the platform's ready-made templates. Filter with channel and locale. |
Library templates are read-only. When a tenant is set up, the platform copies them into the tenant's templates; from then on each copy is the tenant's own template and is changed with the template operations.
Bindings
A binding says: when an event on this topic arrives, send this template on this channel to this recipient.
| Operation | Path | What it does |
|---|---|---|
| List bindings | GET /v1/messaging/bindings | Lists the tenant's bindings, in fallback order. Narrow with event_topic. |
| Create a binding | POST /v1/messaging/bindings | Binds an event topic to a template. |
| Read a binding | GET /v1/messaging/bindings/{id} | Returns one binding. |
| Change a binding | PATCH /v1/messaging/bindings/{id} | Changes a binding. Only the fields you send are written. |
Change a binding (PUT) | PUT /v1/messaging/bindings/{id} | Same action as PATCH. |
| Delete a binding | DELETE /v1/messaging/bindings/{id} | Deletes a binding. The event then does nothing. |
Key request fields for POST /v1/messaging/bindings:
| Field | Required | Notes |
|---|---|---|
event_topic | yes | The event that triggers the binding. |
template_key | yes | The key of the template to send. |
channel | yes | The channel to send on. |
recipient | yes | A template for the address, not an address. For example, {{ customer.email }} is rendered against the event payload. (On POST /v1/messaging/send the recipient is to, a plain address.) |
enabled | no | Set false to pause the binding. |
fallback_order | no | The position of the binding in the list. |
locale | no | The language this route speaks. null means no opinion. |
How it behaves:
- An event whose recipient renders empty is skipped and logged. It is not sent to nobody.
localeis the operator's statement of the language this route speaks. For a send from a binding, the language order is the recipient's own language from the event payload, then the binding'slocale, then the tenant'sdefault_locale, thenen. Leave itnullwhen nobody has made that decision.- Pause with
enabled: falseinstead of deleting. A deleted binding must be typed again from memory later. PUTandPATCHon a binding do the same thing.
Channels and channel credentials
Channels are the ways a message can be delivered. Each channel has a provider, and some channels need credentials. A market can have its own credentials. Without one, the market uses the global credentials.
| Operation | Path | What it does |
|---|---|---|
| List channels | GET /v1/messaging/channels | Lists the channels that can be sent on, and whether each is switched on. |
| Read channel credentials | GET /v1/messaging/channel-credentials | Shows, per channel, the fields the provider needs and which are set. Secret fields never return their value; other fields (such as a sender number) do. |
| Write channel credentials | PATCH /v1/messaging/channel-credentials/{channel} | Writes the credentials of one channel, or, with ?market=, one market's override. market is a query parameter, never a body field. |
Write channel credentials (PUT) | PUT /v1/messaging/channel-credentials/{channel} | Same action as PATCH. |
| Remove channel credentials | DELETE /v1/messaging/channel-credentials/{channel} | Removes the credentials of a channel, or of one market. |
| Verify channel credentials | POST /v1/messaging/channel-credentials/{channel}/verify | Checks that the stored credentials work. Sends nothing. |
How it behaves:
- Read needs
admin. The answer includes the identifiers of the provider account and the webhook URL. That URL contains the tenant's callback token. Secret values are never returned. GET /channel-credentialscan take?market=for one market, or?markets=allto get every market in one answer. Each channel then also carries anoverridesobject, keyed by market code. The read also says which providers can be chosen, whether callbacks are arriving, and the URL to paste into the provider's console.PATCHandPUTwrite only the fields in the body. The answer says which fields actually changed.- Three refusals, all
422:unknown_credential_field: the provider has no field with that name.managed_credential: the platform issues this field, so it cannot be set.channel_not_configurable: the channel has nothing to configure. Push is one of these.
driverswitches the provider. The fields sent in the same request are validated against the new provider, and the previous provider's stored fields for that scope are removed.marketis a lowercase code that starts with a letter and has at most 63 characters. Any other value is422. An empty?market=means the global credentials.DELETEwith?market=removes only that market's override. The market then uses the global credentials again.DELETEwithout a market removes the channel's credentials, overrides included. It answers204whether or not anything was there. A channel that cannot be configured, such as push, is422channel_not_configurable.POST .../verifyanswers200withdata.okanddata.message. Wrong credentials giveok: false, not a4xx. A channel with no credential fields, such as push or in-app, givesok: truewithout any check, because there is nothing to check. An invalid?market=is422.
Configuration
| Operation | Path | What it does |
|---|---|---|
| Read configuration | GET /v1/messaging/config | Returns the tenant's messaging configuration. |
| Change configuration | PATCH /v1/messaging/config | Changes the configuration. Only the fields you send are written. |
Change configuration (PUT) | PUT /v1/messaging/config | Same action as PATCH. |
How it behaves:
- A tenant that was never set up still gets an answer, an empty shape and not
404. meta.push_public_keyis the public VAPID key. A browser needs it to subscribe to push. The private key is never returned.- Changes reach every message the tenant sends, including templates saved long ago, because placeholders are filled in at send time. Writing needs
admin. Reading needsuseroradmin. - Provider credentials cannot be written here. Use the channel credentials operations.
defaults.brandis422, not ignored. The letterhead is now set through the layouts operations.- A half-written
quiet_hoursis422. Setting a start without an end is refused.
Fields for PATCH and PUT: default_locale (the tenant's house language, a fallback in the send order), defaults (an object of saved modules), product, quiet_hours (an object: start and end as HH:MM, an optional IANA timezone and optional message classes), support_email.
Send and preview
| Operation | Path | What it does |
|---|---|---|
| Send a message | POST /v1/messaging/send | Renders a template and sends it now, at send_at, or at the end of quiet hours (which hold only marketing messages unless the configuration's quiet_hours.classes says otherwise). |
| Preview a template | POST /v1/messaging/preview | Renders a template and returns the subject, HTML and text. Sends nothing. |
Key request fields for POST /v1/messaging/send:
| Field | Required | Notes |
|---|---|---|
template | yes | The template to send. |
channel | yes | The channel to send on. |
to | yes | The recipient. |
data | no | The render model: a map of variable names to values. Nested objects and arrays are allowed. |
locale | no | The language the caller asks for. Absent means the recipient's language decides. |
market | no | The market the send belongs to. Absent means global. |
send_at | no | A time to send later. A time in the past means now. |
draft | no | true sends a test of the draft instead of the published version. |
attachments | no | A list of files, each with content (base64, required), filename and content_type; email only, sent only when the message goes out at once: a message held by send_at or quiet hours, or retried, goes out without them. A URL is not accepted. On another channel it is 422 channel_carries_no_attachments; a file too large or bad base64 is 422 attachment_rejected. |
Idempotency-Keyheader. This operation accepts it (most writes on the platform do not; see Retries & idempotency). Use it to make a send safe to retry. The key is opaque to the service. Only its length matters, and keys longer than 191 characters are truncated.201means this call created the message.200means the key was used before, and the response is the message that key produced. The same key with a different body is422.- A suppressed recipient is not sent to when an unexpired suppression on the same channel covers the message's class: scope
allblocks every message, scopemarketingonlymarketingmessages. The call still answers201, with the message'sstatusset tosuppressedand asuppression_reason. A scheduled send is checked again when it is delivered, so it can become suppressed later. 404means no template for that key and channel is in force: none exists, none is published, it is switched off (enabled: false), or it is outsidevalid_from/valid_until.502withsend_failedmeans the provider refused or failed. A temporary failure is kept with statusretryingand retried automatically, so look for the message in the delivery log before you send again with a new key, or the customer may get it twice.- Locale order. The caller's
localecomes first, then the recipient's own language fromdata(the first non-empty oflocale,language,customer.locale,customer.language,recipient.locale), then the tenant'sdefault_locale, thenen. Each language also tries its base language (de-AT, thende). The first language a template exists in wins; if none has one, any template for that key and channel is sent.
Key request fields for POST /v1/messaging/preview: template (required), channel (required), data, locale. The preview takes no market, send_at, draft or attachments, because a render does not pick a provider or a time.
- It renders the draft, so you can preview a change before you publish it.
- Nothing is stored. The preview creates no message row, makes no provider call and does not check suppressions.
- The answer has
subject,htmlandtext, exactly as a send would produce them. Each isnullwhere the template has no such body. An SMS has no subject and no HTML. localein the answer is the language the preview is in. It may differ from the one you asked for, if the template fell back to another language.
Delivery log
| Operation | Path | What it does |
|---|---|---|
| List messages | GET /v1/messaging/messages | Lists the delivery log, newest first. |
| Read a message | GET /v1/messaging/messages/{id} | Returns one message, with the render model it was sent with. |
- Filters and paging.
channelandstatusnarrow the list.limitdefaults to 50 and is capped at 200. - In-app inbox.
?channel=inappreturns the tenant's in-app inbox. Each row is the inbox item itself. - Not an archive. Rows are kept only for the retention period, and an erasure request removes them.
- Personal data. A message carries the data it was sent with. That is why it can be erased.
Stats
| Operation | Path | What it does |
|---|---|---|
| Read stats | GET /v1/messaging/stats | Returns delivery figures for one window and the window before it. |
- Choose the window with
days(a window ending now, 1 to 365, default 30), or with bothfromandtoasYYYY-MM-DD. Giving only one of them is refused. - Retention clamps the window. The window is cut to the tenant's retention. The
windowblock says so withclamped_by_retention. - Opens are counted for email only. The other channels are left out of the open rate.
- Delivery rate is sent ÷ (sent + failed). Suppressed messages are not counted as failures.
previousis the same length of window, immediately before this one. It isnullwhen that window is not entirely inside the retention period. An empty window inside retention gives zeros.
Suppressions
A suppression is an address that is not mailed, and the reason why.
| Operation | Path | What it does |
|---|---|---|
| List suppressions | GET /v1/messaging/suppressions | Lists who is not being mailed, newest first. |
| Add a suppression | POST /v1/messaging/suppressions | Suppresses an address by hand. |
| Read a suppression | GET /v1/messaging/suppressions/{id} | Returns one suppression. |
| Lift a suppression | DELETE /v1/messaging/suppressions/{id} | Removes a suppression, so the address can be mailed again. |
Filters for the list: channel, scope (all or marketing), reason (hard_bounce, complaint, unsubscribe or manual), address, limit. The address filter matches a fingerprint, so it still finds a person who has been erased. For a channel other than email, pass channel with it.
Key request fields for POST /v1/messaging/suppressions:
| Field | Required | Notes |
|---|---|---|
address | yes | The address to suppress. |
channel | yes | The channel it applies to. |
reason | yes | hard_bounce, complaint, unsubscribe or manual. |
scope | no | all or marketing. For every reason except manual, the scope is fixed by the reason. |
expires_at | no | When the suppression ends. |
note | no | A free-text note. |
201means this call created the suppression.200means the address was already on the list for that channel and scope; the existing row is overwritten with this request.- A wrong scope is
422withsuppression_scope_fixed. The request is not corrected silently. DELETEis audited. Lifting a suppression is the one action that makes the service mail an address it had decided not to mail.addresscan benullon a read. That is an erased person. The row stays, so the address is still blocked.
Push subscriptions
| Operation | Path | What it does |
|---|---|---|
| List subscriptions | GET /v1/messaging/push/subscriptions | Lists the browsers one person has registered. |
| Register a browser | POST /v1/messaging/push/subscriptions | Registers a browser for push. |
| Remove a browser | DELETE /v1/messaging/push/subscriptions | Unregisters one browser, by its endpoint. |
GETneedssubscriber_id. There is no operation that lists all subscribers.- Key material is never returned. A registration that could be read back could be used to push to it.
POSTtakes whatPushManager.subscribe()returned, plus the id you know the person by. The fields areendpoint(required),keys(required, withauthandp256dh),subscriber_id(required) anduser_agent. The public VAPID key comes fromGET /v1/messaging/config.POSTis idempotent by endpoint.201means a new browser.200means one already registered.DELETEtakesendpointas a required query parameter, not an id. It answers204whether or not the browser was registered.
Privacy erasure
| Operation | Path | What it does |
|---|---|---|
| Erase one person | POST /v1/messaging/privacy/erasures | Erases one address on one channel from the delivery log. |
Request fields: address (required) and channel (required). Erasure is per channel and address. For email, matching ignores capitalisation and surrounding spaces; for other channels, send the address in lowercase, because the stored address is compared in lowercase.
- Message rows and unsubscribe tokens are deleted.
- Change-log entries are not erased. A suppression's change-log entry is recorded under its address, and it stays.
- Suppressions are kept, with the clear-text address removed. The row can still block the address, but it no longer identifies the person.
- The answer gives the counts, including
suppressions_kept.
Change log
| Operation | Path | What it does |
|---|---|---|
| List the change log | GET /v1/messaging/audit | Lists recorded changes, newest first. |
- Filters:
resource_type(template,layoutorsuppression),resource_id,subjectandlimit. Channel-credential changes are logged too, butresource_typecannot select them. subjectis the name the row was recorded under, such as a template's key or a layout's name. It stays readable after the record is gone.- Append-only. There is no route to write to or delete from the log.
Next steps
- Authentication and API keys: get a credential with the
messagingscope. - Errors: the error body and what to do for each status.
- Rate limits: limits and the
Retry-Afterheader.