Messaging

Manage the tenant's transactional messages, from templates and letterheads to bindings, channels, sending, the delivery log, suppressions and push subscriptions.

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:

  • 422 means a write or a filter was refused. error.details maps 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 on error.code.
  • 429 means the rate limit was exceeded. Wait for the Retry-After header 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.

OperationPathWhat it does
List templatesGET /v1/messaging/templatesLists the tenant's templates, by key.
Create a templatePOST /v1/messaging/templatesCreates a template. The new row is a draft.
Read a templateGET /v1/messaging/templates/{id}Returns the draft, not what is live.
Change a templatePATCH /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 templateDELETE /v1/messaging/templates/{id}Deletes the template.
Publish a templatePOST /v1/messaging/templates/{templateId}/publishMakes the current draft live.
List versionsGET /v1/messaging/templates/{templateId}/versionsLists the publication history, newest first.
Read a versionGET /v1/messaging/templates/{templateId}/versions/{version}Returns one published version, whole.
Restore a versionPOST /v1/messaging/templates/{templateId}/versions/{version}/restorePuts an older version's content back into the draft.

{templateId} is the template's id.

Key request fields for POST /v1/messaging/templates:

FieldRequiredNotes
channelyesThe channel the template is sent on.
keyyesThe name the template is addressed by.
titlenoThe name shown to people. Without it, a list has to derive a name from the key.
subject, body_html, body_textnoMessage content. Send body_html and body_text yourself to skip compilation; a design sent in the same request overwrites them.
designnoA design document. The service compiles it against the template's layout.
layout_idnoThe 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.
localenoThe template's language.
marketsnoThe markets the template is browsed in. [], the default, means global.
message_classnotransactional or marketing. The default is transactional.
enablednoSwitch.
test_modenotrue: a send renders and logs the message with status test, and nothing reaches the provider. The call still answers 201.
valid_from, valid_untilnoThe window in which the template is in force.
variables, variable_defaultsnoPlaceholders, and fallbacks for placeholders an event did not fill.
content_sidnoThe Meta-approved template a WhatsApp message is sent as.
whatsapp_categorynomarketing, 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.details names the block that failed. If the compiler is unavailable, the answer is 503, which is worth retrying; a refused design is not.
  • PATCH and PUT on 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_html you wrote yourself is never recompiled.
  • Reclassifying message_class changes nothing already sent. Each message keeps the class it had when it was sent.
  • Recategorising whatsapp_category takes 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}/versions returns meta.has_unpublished_changes, which says whether the draft has moved on since the last publish.
  • Market filter on the list. With the X-Revenexx-Market header, the list shows the global templates and that market's templates. Without the header, it shows only the global ones. ?markets=all returns every row. The filter only narrows the browsing list. It does not change which tenant you can read.
  • Publish takes an optional note. It answers 201 when a new version is written, and 200 with 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: true to 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.

OperationPathWhat it does
List layoutsGET /v1/messaging/layoutsLists the tenant's letterheads, with the default first.
Create a layoutPOST /v1/messaging/layoutsCreates a letterhead.
Read a layoutGET /v1/messaging/layouts/{id}Returns one letterhead.
Change a layoutPATCH /v1/messaging/layouts/{id}Changes a letterhead.
Delete a layoutDELETE /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}:

FieldRequiredNotes
nameyes on createUp to 191 characters.
is_defaultnoMakes this layout the tenant's default.
enabled, valid_from, valid_untilnoSwitch and validity window. valid_until must not be before valid_from.
marketsnoThe markets the layout is browsed in. Never null.
logo_url, legal_name, postal_address, support_email, footer_note, sender_namenoLetterhead content.
social_links, menu_linksnoLink lists.
color_accent, color_bg, color_textnoHex colours.
width, font_familynoOne 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 422 with layout_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_default on the current default is 422 with layout_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 design is saved again. An audit entry is written only when something changed.
  • GET /v1/messaging/layouts is filtered by market, like the template list. GET /v1/messaging/layouts/{id} is not filtered.
  • Deleting a layout answers 200 with a body, not 204. The body is data with id, deleted and templates_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's design is saved again.
  • The default layout cannot be deleted (422, layout_is_default). Make another layout the default first.

Library

OperationPathWhat it does
List library templatesGET /v1/messaging/libraryLists 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.

OperationPathWhat it does
List bindingsGET /v1/messaging/bindingsLists the tenant's bindings, in fallback order. Narrow with event_topic.
Create a bindingPOST /v1/messaging/bindingsBinds an event topic to a template.
Read a bindingGET /v1/messaging/bindings/{id}Returns one binding.
Change a bindingPATCH /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 bindingDELETE /v1/messaging/bindings/{id}Deletes a binding. The event then does nothing.

Key request fields for POST /v1/messaging/bindings:

FieldRequiredNotes
event_topicyesThe event that triggers the binding.
template_keyyesThe key of the template to send.
channelyesThe channel to send on.
recipientyesA 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.)
enablednoSet false to pause the binding.
fallback_ordernoThe position of the binding in the list.
localenoThe 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.
  • locale is 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's locale, then the tenant's default_locale, then en. Leave it null when nobody has made that decision.
  • Pause with enabled: false instead of deleting. A deleted binding must be typed again from memory later.
  • PUT and PATCH on 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.

OperationPathWhat it does
List channelsGET /v1/messaging/channelsLists the channels that can be sent on, and whether each is switched on.
Read channel credentialsGET /v1/messaging/channel-credentialsShows, 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 credentialsPATCH /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 credentialsDELETE /v1/messaging/channel-credentials/{channel}Removes the credentials of a channel, or of one market.
Verify channel credentialsPOST /v1/messaging/channel-credentials/{channel}/verifyChecks 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-credentials can take ?market= for one market, or ?markets=all to get every market in one answer. Each channel then also carries an overrides object, 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.
  • PATCH and PUT write 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.
  • driver switches 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.
  • market is a lowercase code that starts with a letter and has at most 63 characters. Any other value is 422. An empty ?market= means the global credentials.
  • DELETE with ?market= removes only that market's override. The market then uses the global credentials again. DELETE without a market removes the channel's credentials, overrides included. It answers 204 whether or not anything was there. A channel that cannot be configured, such as push, is 422 channel_not_configurable.
  • POST .../verify answers 200 with data.ok and data.message. Wrong credentials give ok: false, not a 4xx. A channel with no credential fields, such as push or in-app, gives ok: true without any check, because there is nothing to check. An invalid ?market= is 422.

Configuration

OperationPathWhat it does
Read configurationGET /v1/messaging/configReturns the tenant's messaging configuration.
Change configurationPATCH /v1/messaging/configChanges the configuration. Only the fields you send are written.
Change configuration (PUT)PUT /v1/messaging/configSame 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_key is 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 needs user or admin.
  • Provider credentials cannot be written here. Use the channel credentials operations.
  • defaults.brand is 422, not ignored. The letterhead is now set through the layouts operations.
  • A half-written quiet_hours is 422. 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

OperationPathWhat it does
Send a messagePOST /v1/messaging/sendRenders 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 templatePOST /v1/messaging/previewRenders a template and returns the subject, HTML and text. Sends nothing.

Key request fields for POST /v1/messaging/send:

FieldRequiredNotes
templateyesThe template to send.
channelyesThe channel to send on.
toyesThe recipient.
datanoThe render model: a map of variable names to values. Nested objects and arrays are allowed.
localenoThe language the caller asks for. Absent means the recipient's language decides.
marketnoThe market the send belongs to. Absent means global.
send_atnoA time to send later. A time in the past means now.
draftnotrue sends a test of the draft instead of the published version.
attachmentsnoA 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-Key header. 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. 201 means this call created the message. 200 means the key was used before, and the response is the message that key produced. The same key with a different body is 422.
  • A suppressed recipient is not sent to when an unexpired suppression on the same channel covers the message's class: scope all blocks every message, scope marketing only marketing messages. The call still answers 201, with the message's status set to suppressed and a suppression_reason. A scheduled send is checked again when it is delivered, so it can become suppressed later.
  • 404 means no template for that key and channel is in force: none exists, none is published, it is switched off (enabled: false), or it is outside valid_from/valid_until.
  • 502 with send_failed means the provider refused or failed. A temporary failure is kept with status retrying and 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 locale comes first, then the recipient's own language from data (the first non-empty of locale, language, customer.locale, customer.language, recipient.locale), then the tenant's default_locale, then en. Each language also tries its base language (de-AT, then de). 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, html and text, exactly as a send would produce them. Each is null where the template has no such body. An SMS has no subject and no HTML.
  • locale in 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

OperationPathWhat it does
List messagesGET /v1/messaging/messagesLists the delivery log, newest first.
Read a messageGET /v1/messaging/messages/{id}Returns one message, with the render model it was sent with.
  • Filters and paging. channel and status narrow the list. limit defaults to 50 and is capped at 200.
  • In-app inbox. ?channel=inapp returns 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

OperationPathWhat it does
Read statsGET /v1/messaging/statsReturns 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 both from and to as YYYY-MM-DD. Giving only one of them is refused.
  • Retention clamps the window. The window is cut to the tenant's retention. The window block says so with clamped_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.
  • previous is the same length of window, immediately before this one. It is null when 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.

OperationPathWhat it does
List suppressionsGET /v1/messaging/suppressionsLists who is not being mailed, newest first.
Add a suppressionPOST /v1/messaging/suppressionsSuppresses an address by hand.
Read a suppressionGET /v1/messaging/suppressions/{id}Returns one suppression.
Lift a suppressionDELETE /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:

FieldRequiredNotes
addressyesThe address to suppress.
channelyesThe channel it applies to.
reasonyeshard_bounce, complaint, unsubscribe or manual.
scopenoall or marketing. For every reason except manual, the scope is fixed by the reason.
expires_atnoWhen the suppression ends.
notenoA free-text note.
  • 201 means this call created the suppression. 200 means the address was already on the list for that channel and scope; the existing row is overwritten with this request.
  • A wrong scope is 422 with suppression_scope_fixed. The request is not corrected silently.
  • DELETE is audited. Lifting a suppression is the one action that makes the service mail an address it had decided not to mail.
  • address can be null on a read. That is an erased person. The row stays, so the address is still blocked.

Push subscriptions

OperationPathWhat it does
List subscriptionsGET /v1/messaging/push/subscriptionsLists the browsers one person has registered.
Register a browserPOST /v1/messaging/push/subscriptionsRegisters a browser for push.
Remove a browserDELETE /v1/messaging/push/subscriptionsUnregisters one browser, by its endpoint.
  • GET needs subscriber_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.
  • POST takes what PushManager.subscribe() returned, plus the id you know the person by. The fields are endpoint (required), keys (required, with auth and p256dh), subscriber_id (required) and user_agent. The public VAPID key comes from GET /v1/messaging/config.
  • POST is idempotent by endpoint. 201 means a new browser. 200 means one already registered.
  • DELETE takes endpoint as a required query parameter, not an id. It answers 204 whether or not the browser was registered.

Privacy erasure

OperationPathWhat it does
Erase one personPOST /v1/messaging/privacy/erasuresErases 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

OperationPathWhat it does
List the change logGET /v1/messaging/auditLists recorded changes, newest first.
  • Filters: resource_type (template, layout or suppression), resource_id, subject and limit. Channel-credential changes are logged too, but resource_type cannot select them.
  • subject is 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

Was this page helpful?