Send email, push, Slack, Discord, Telegram and webhook notifications and manage templates with Ray.
Maintain this server? Claim it
Call this server's tools from your browser. Nothing to install.
Bearer token with your Ray API key, created at https://ray.gege.mn under API keys.
Requests go through mcptry because browsers can't call most MCP servers directly. Header values are sent to the server for that request only and are never stored or logged.
Tool list from our last check. Connect live to refresh it.
Archive a template (POST /templates/{id}/archive; write scope). Sends that reference it fail until it is restored with unarchive_template; its name stays reserved meanwhile. Confirm with the user first if it may be in production use.
The template id (UUID).
The server marks this tool as destructive. Running it may delete or overwrite data on the server or in any account you connected with your headers.
Archive a template (POST /templates/{id}/archive; write scope). Sends that reference it fail until it is restored with unarchive_template; its name stays reserved meanwhile. Confirm with the user first if it may be in production use.
| Argument | Type | Description |
|---|---|---|
| templateId* | string | The template id (UUID). |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"templateId"
],
"properties": {
"templateId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The template id (UUID)."
}
}
}Create a template (POST /templates; write scope). `channelKind` is permanent and must match the channels you'll send through (list_channels `templateKind`). It is saved as an unpublished draft unless `publish: true`; sending by templateId needs a published version (publish_template). Names are unique per workspace including archived templates: a collision returns 409, so unarchive_template the old one instead. Returns `{ id, requiredParams }`. Designed (drag-and-drop) email templates can only be made in the dashboard.
| Argument | Type | Description |
|---|---|---|
| name* | string | Template name, unique per workspace (archived templates included). |
| folder | string | Optional folder path for organisation, e.g. "billing". |
| content* | object | Channel-shaped template body; the shape is fixed by `channelKind`: email_html → `{ subject, bodyHtml, bodyText }`; fcm_basic → `{ title, body, imageUrl?, data?: { [key]: string } }`; slack_text → `{ text }`; discord_text → `{ content }` (max 2000 chars); telegram_text → `{ text, disableLinkPreview? }`; sms_text → `{ text }` (plain text, max 1600; long text is split into several billed SMS — on sendsms_mn into parts of 159 plain-Latin / 69 Cyrillic chars); webhook_json → `{ title, body, data?: { [key]: string } }`. Use `{{name}}` placeholders and `{{#items}}…{{/items}}` sections; the required params are derived from them automatically. |
| publish | boolean | true = publish this version immediately so sends by templateId use it. Default false (saved as draft). |
| logTitle* | string | Short title shown in the in-app feed and delivery logs, e.g. "Invoice {{number}} is due". Placeholders allowed. |
| channelKind* | string | Which channel family the template renders for. Must match the `templateKind` of the channels you will send through (list_channels). Can't be changed after creation. |
| logDescription* | string | One-line description for the in-app feed and delivery logs. Placeholders allowed. |
| paramOverrides |
Create an outbound webhook that Ray POSTs signed event payloads to (POST /tenant-webhooks; write scope; Pro plan or higher, otherwise 403). The URL must be public HTTPS. The response contains the signing `secret` exactly once: show it to the user to store (e.g. as an env var), since it can't be read again, only rotated with update_webhook. Read the `webhooks` docs page for the signature scheme.
| Argument | Type | Description |
|---|---|---|
| url* | string | Public HTTPS endpoint that will receive the events. |
| name* | string | Label for the webhook, e.g. "Production delivery events". |
| events* | array | Events to receive: "notification.delivered", "notification.failed_terminal" (one per recipient), and "send.completed" (once per send, with status totals). |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"url",
"events"
],
"properties": {
"url": {
"type": "string",
"description": "Public HTTPS endpoint that will receive the events."
},
"name": {
"type": "string",
"maxLength": 100,
"minLength": 1,
"description": "Label for the webhook, e.g. \"Production delivery events\"."
},
"events": {
"type": "array",
"items": {
"enum": [
"notification.delivered",
"notification.failed_terminal",
"send.completed"
],
"type": "string"
},
"minItems": 1,
"description": "Events to receive: \"notification.delivered\", \"notification.failed_terminal\" (one per recipient), and \"send.completed\" (once per send, with status totals)."
}
}
}Delete (archive) a webhook so it receives no more events (DELETE /tenant-webhooks/{id}; write scope). It cannot be restored through the API; confirm with the user first.
| Argument | Type | Description |
|---|---|---|
| webhookId* | string | The webhook id (UUID). |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"webhookId"
],
"properties": {
"webhookId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The webhook id (UUID)."
}
}
}Email link click counts grouped by destination URL (GET /clicks) for a `sendId`, a `campaignId`, or both (at least one is required). Only links from email sends made with `trackClicks: true` are counted.
| Argument | Type | Description |
|---|---|---|
| sendId | string | A sendId from send_notification. |
| campaignId | string | The `campaignId` passed to send_notification, to total clicks across many sends. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"sendId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "A sendId from send_notification."
},
"campaignId": {
"type": "string",
"maxLength": 256,
"minLength": 1,
"description": "The `campaignId` passed to send_notification, to total clicks across many sends."
}
}
}Delivery status of a send (GET /sends/{id}): `aggregate` counts per status over the whole send (pending, claimed, delivered, failed_retryable, failed_terminal, suppressed) plus one page of per-recipient delivery rows including provider errors. Right after sending, rows are usually `pending`; check again after a few seconds. Feed-only sends have no rows. For more rows, pass the returned `nextCursor` as `cursor`.
| Argument | Type | Description |
|---|---|---|
| limit | integer | Rows per page (default 50, max 200). |
| cursor | string | `nextCursor` from the previous page. |
| sendId* | string | The `sendId` returned by send_notification or test_send_template. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"sendId"
],
"properties": {
"limit": {
"type": "integer",
"maximum": 200,
"minimum": 1,
"description": "Rows per page (default 50, max 200)."
},
"cursor": {
"type": "string",
"minLength": 1,
"description": "`nextCursor` from the previous page."
},
"sendId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The `sendId` returned by send_notification or test_send_template."
}
}
}Get one template with its published version and its current draft (GET /templates/{id}): content, logTitle/logDescription, `requiredParams` a send must supply, and paramOverrides. Read this before update_template_draft, which needs the full body.
| Argument | Type | Description |
|---|---|---|
| templateId* | string | The template id (UUID). |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"templateId"
],
"properties": {
"templateId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The template id (UUID)."
}
}
}The workspace's plan, subscription status and this calendar month's usage against the monthly quota (GET /billing/subscription). `used`/`remaining` are null on unmetered plans. On a hard-capped plan, sends beyond the quota fail with HTTP 402 quota_exceeded.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Get one outbound webhook (GET /tenant-webhooks/{id}) including its delivery health (consecutiveFailures, lastDeliveryAt, lastFailureAt, lastError).
| Argument | Type | Description |
|---|---|---|
| webhookId* | string | The webhook id (UUID). |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"webhookId"
],
"properties": {
"webhookId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The webhook id (UUID)."
}
}
}List the delivery channels configured in this workspace (GET /channels). Call this FIRST before sending: each entry's `id` is the `channelConfigId` for send_notification and test_send_template, `kind` is the channel type (ses_email, smtp_email, fcm_push, slack_webhook, discord_webhook, telegram_bot, twilio_sms, sendsms_mn, generic_webhook), `templateKind` is the template channelKind it accepts, and `recipientSchema` is the exact JSON Schema of the `recipient` it expects. Channels and their provider credentials are set up in the Ray dashboard, not through this API; if the list is empty, ask the user to add one there.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Read one end user's in-app notification feed (GET /notifications): the entries created by sends with `feed` or `showInFeed`. This is what an in-app inbox would show, not delivery status (use get_send_status for that). Paginate by passing `nextCursor` back as `cursor`.
| Argument | Type | Description |
|---|---|---|
| after | string | Only entries created after this ISO 8601 UTC datetime (e.g. "2026-09-01T00:00:00Z"). |
| limit | integer | Entries per page (default 50, max 200). |
| before | string | Only entries created before this ISO 8601 UTC datetime. |
| cursor | string | `nextCursor` from the previous page. |
| templateId | string | Only entries from this template. |
| externalUserId* | string | The end user whose feed to read (the `externalUserId` used when sending). |
| channelConfigId | string | Only entries whose first delivery used this channel. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"externalUserId"
],
"properties": {
"after": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "Only entries created after this ISO 8601 UTC datetime (e.g. \"2026-09-01T00:00:00Z\")."
},
"limit": {
"type": "integer",
"maximum": 200,
"minimum": 1,
"description": "Entries per page (default 50, max 200)."
},
"before": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "Only entries created before this ISO 8601 UTC datetime."
},
"cursor": {
"type": "string",
"minLength": 1,
"description": "`nextCursor` from the previous page."
},
"templateId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Only entries from this template."
},
"externalUserId": {
"type": "string",
"maxLength": 256,
"minLength": 1,
"description": "The end user whose feed to read (the `externalUserId` used when sending)."
},
"channelConfigId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Only entries whose first delivery used this channel."
}
}
}List message templates (GET /templates): id, name, folder, channelKind, and `publishedVersionId` (null = never published, so it cannot be sent by templateId yet). Archived templates are hidden unless `includeArchived` is true. Use get_template for content and required params.
| Argument | Type | Description |
|---|---|---|
| folder | string | Only templates in this folder. |
| includeArchived | boolean | Include archived templates. Default false. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"folder": {
"type": "string",
"maxLength": 200,
"description": "Only templates in this folder."
},
"includeArchived": {
"type": "boolean",
"description": "Include archived templates. Default false."
}
}
}List the workspace's outbound event webhooks (GET /tenant-webhooks): url, subscribed events, enabled flag and delivery health (consecutiveFailures, lastError). Signing secrets are never returned.
| Argument | Type | Description |
|---|---|---|
| includeArchived | boolean | Include deleted (archived) webhooks. Default false. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"includeArchived": {
"type": "boolean",
"description": "Include deleted (archived) webhooks. Default false."
}
}
}Publish a template's current draft as its new live version (POST /templates/{id}/publish; write scope). Sends by templateId use it immediately. Fails if there is no draft or the template is archived.
| Argument | Type | Description |
|---|---|---|
| templateId* | string | The template id (UUID). |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"templateId"
],
"properties": {
"templateId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The template id (UUID)."
}
}
}Read Ray's documentation as markdown. With no `slug` it returns the index (llms.txt) listing every page; then pass a page slug, i.e. the path after /docs/ without `.md`, such as `sending`, `templates`, `idempotency`, `errors`, `rate-limits`, `webhooks`, `status-and-feeds` or `channels/telegram`. Use it for anything the tool descriptions don't cover.
| Argument | Type | Description |
|---|---|---|
| slug | string | Page slug like "sending" or "channels/push-fcm". Omit for the index of all pages. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"slug": {
"type": "string",
"maxLength": 100,
"description": "Page slug like \"sending\" or \"channels/push-fcm\". Omit for the index of all pages."
}
}
}Send a notification (POST /send; needs a `write`-scoped key). Returns `{ sendId }` with HTTP 202: delivery is asynchronous, so check the outcome with get_send_status. Use exactly one mode: 1. Single: `channelConfigId` + `recipient`. 2. Fan-out: `channelConfigId` + `targets` (1-1000 `{ recipient, externalUserId? }`), one channel, one sendId. 3. Multi-channel: `deliveries` (1-10, each with its own `channelConfigId`, `recipient` and `templateId` or `content`) for ONE person over several channels; add `feed` + `externalUserId` for a single in-app feed entry. 4. Feed-only: `feed` (with `title`) + `externalUserId` and no channel: an in-app notification only. In modes 1-2 give exactly one of `templateId` (a published template, plus `params`) or inline `content`; in mode 3 that choice is made per delivery. Get channel ids and each recipient shape from list_channels first. Pass `idempotencyKey` whenever a retry must not deliver twice.
| Argument | Type | Description |
|---|---|---|
| feed | object | Create exactly ONE in-app feed entry for `externalUserId` (read back with list_feed_notifications). `title`/`description` default to the rendered template text; `title` is required for a feed-only send (mode 4). |
| params | object | Values for the template's `{{placeholders}}`. Any JSON: strings, numbers, booleans, arrays and nested objects (arrays/objects drive `{{#section}}…{{/section}}` blocks). Missing required params are rejected with a 400 naming them. |
| content | object | Inline message content, used INSTEAD of `templateId` (give exactly one of the two). Shape by channel kind: ses_email / smtp_email → `{ subject, bodyHtml, bodyText }`; fcm_push → `{ title, body, imageUrl?, data?: { [key]: string } }`; slack_webhook → `{ text }` (Slack mrkdwn); discord_webhook → `{ content }` (max 2000 chars); telegram_bot → `{ text, disableLinkPreview? }` (Telegram HTML subset); twilio_sms / sendsms_mn → `{ text }` (plain text; up to 1600 chars on Twilio, sendsms_mn splits long text into SMS of 159 plain-Latin / 69 Cyrillic chars, each billed); generic_webhook → `{ title, body, data?: { [key]: string } }`. `{{name}}` placeholders are filled from `params` and escaped for the channel. |
Send a real test of a template's PUBLISHED version to one recipient (POST /templates/{id}/test-send; write scope). It goes through the provider for real, so use a recipient the user controls. Test sends don't count toward the monthly quota (a small daily allowance applies instead) and never appear in feeds. Returns `{ sendId }` for get_send_status.
| Argument | Type | Description |
|---|---|---|
| params | object | Values for the template's `{{placeholders}}`. Any JSON: strings, numbers, booleans, arrays and nested objects (arrays/objects drive `{{#section}}…{{/section}}` blocks). Missing required params are rejected with a 400 naming them. |
| recipient* | object | Channel-specific delivery target. Its shape depends on the kind of the chosen channel (list_channels returns each channel's exact `recipientSchema`): ses_email / smtp_email → `{ email, name?, cc?: [{ email, name? }], bcc?: [...], attachments?: [{ filename, content (base64), contentType?, disposition?: "attachment"|"inline", contentId? }] }`; fcm_push → `{ deviceToken }` OR `{ topic }` (exactly one; Ray keeps no device registry); telegram_bot → `{ chatId }` (numeric id as a string, or "@channelname"); twilio_sms → `{ phoneNumber }` (international E.164, e.g. "+97699112233"); sendsms_mn → `{ phoneNumber }` (Mongolian 8-digit, e.g. "99112233"); slack_webhook, discord_webhook, generic_webhook → `{}` (the destination is part of the channel config). |
| templateId* | string | The template id (UUID). |
| channelConfigId* | string | Channel to test through (from list_channels); its templateKind must match the template's channelKind. |
Restore an archived template (POST /templates/{id}/unarchive; write scope). This is how to reclaim a name held by an archived template instead of creating a new one.
| Argument | Type | Description |
|---|---|---|
| templateId* | string | The template id (UUID). |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"templateId"
],
"properties": {
"templateId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The template id (UUID)."
}
}
}Replace a template's draft (PATCH /templates/{id}; write scope). This is a full replacement, not a partial patch: pass content, logTitle, logDescription and paramOverrides as they should end up (get_template first). `name`, `folder` and `channelKind` are required for validation but not changed (channelKind must equal the original). Live sends keep using the published version until you publish, either with `publish: true` here or publish_template. Fails on archived templates.
| Argument | Type | Description |
|---|---|---|
| name* | string | Template name, unique per workspace (archived templates included). |
| folder | string | Optional folder path for organisation, e.g. "billing". |
| content* | object | Channel-shaped template body; the shape is fixed by `channelKind`: email_html → `{ subject, bodyHtml, bodyText }`; fcm_basic → `{ title, body, imageUrl?, data?: { [key]: string } }`; slack_text → `{ text }`; discord_text → `{ content }` (max 2000 chars); telegram_text → `{ text, disableLinkPreview? }`; sms_text → `{ text }` (plain text, max 1600; long text is split into several billed SMS — on sendsms_mn into parts of 159 plain-Latin / 69 Cyrillic chars); webhook_json → `{ title, body, data?: { [key]: string } }`. Use `{{name}}` placeholders and `{{#items}}…{{/items}}` sections; the required params are derived from them automatically. |
| publish | boolean | true = publish this version immediately so sends by templateId use it. Default false (saved as draft). |
| logTitle* | string | Short title shown in the in-app feed and delivery logs, e.g. "Invoice {{number}} is due". Placeholders allowed. |
Change a webhook (PATCH /tenant-webhooks/{id}; write scope). Only the fields you pass change; pass at least one. `rotateSecret: true` issues a new signing secret, returned once in the response, and the old secret stops verifying immediately, so the receiving service must be updated at the same time.
| Argument | Type | Description |
|---|---|---|
| url | string | New public HTTPS endpoint. |
| name | string | New label. |
| events | array | Replaces the subscribed events. Events to receive: "notification.delivered", "notification.failed_terminal" (one per recipient), and "send.completed" (once per send, with status totals). |
| enabled | boolean | false pauses deliveries without deleting the webhook. |
| webhookId* | string | The webhook id (UUID). |
| rotateSecret | boolean | true rotates the signing secret (new secret returned once; old one invalid immediately). |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"webhookId"
],
"properties": {
"url": {
"type": "string",
"description": "New public HTTPS endpoint."
},
"name": {
"type": "string",
"maxLength": 100,
"minLength": 1,
"description": "New label."
},
"events": {
"type": "array",
"items": {
"enum": [
"notification.delivered",
"notification.failed_terminal",
"send.completed"
],
"type": "string"
},
"minItems": 1,
"description": "Replaces the subscribed events. Events to receive: \"notification.delivered\", \"notification.failed_terminal\" (one per recipient), and \"send.completed\" (once per send, with status totals)."
},
"enabled": {
"type": "boolean",
"description": "false pauses deliveries without deleting the webhook."
},
"webhookId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The webhook id (UUID)."
},
"rotateSecret": {
"type": "boolean",
"description": "true rotates the signing secret (new secret returned once; old one invalid immediately)."
}
}
}Identify the API key this connection uses (GET /me): `tenantId` (the workspace), `apiKeyId` and `scopes`. Write tools need the `write` scope; call this to check before attempting them.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}| object |
| Per-param metadata keyed by param name: `{ optional?: boolean, description?: string }`. Mark a placeholder optional so sends may omit it. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"channelKind",
"content",
"logTitle",
"logDescription"
],
"properties": {
"name": {
"type": "string",
"maxLength": 100,
"minLength": 1,
"description": "Template name, unique per workspace (archived templates included)."
},
"folder": {
"type": "string",
"maxLength": 200,
"description": "Optional folder path for organisation, e.g. \"billing\"."
},
"content": {
"type": "object",
"description": "Channel-shaped template body; the shape is fixed by `channelKind`: email_html → `{ subject, bodyHtml, bodyText }`; fcm_basic → `{ title, body, imageUrl?, data?: { [key]: string } }`; slack_text → `{ text }`; discord_text → `{ content }` (max 2000 chars); telegram_text → `{ text, disableLinkPreview? }`; sms_text → `{ text }` (plain text, max 1600; long text is split into several billed SMS — on sendsms_mn into parts of 159 plain-Latin / 69 Cyrillic chars); webhook_json → `{ title, body, data?: { [key]: string } }`. Use `{{name}}` placeholders and `{{#items}}…{{/items}}` sections; the required params are derived from them automatically.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"publish": {
"type": "boolean",
"description": "true = publish this version immediately so sends by templateId use it. Default false (saved as draft)."
},
"logTitle": {
"type": "string",
"maxLength": 500,
"minLength": 1,
"description": "Short title shown in the in-app feed and delivery logs, e.g. \"Invoice {{number}} is due\". Placeholders allowed."
},
"channelKind": {
"enum": [
"email_html",
"fcm_basic",
"slack_text",
"discord_text",
"telegram_text",
"sms_text",
"webhook_json"
],
"type": "string",
"description": "Which channel family the template renders for. Must match the `templateKind` of the channels you will send through (list_channels). Can't be changed after creation."
},
"logDescription": {
"type": "string",
"maxLength": 2000,
"minLength": 1,
"description": "One-line description for the in-app feed and delivery logs. Placeholders allowed."
},
"paramOverrides": {
"type": "object",
"description": "Per-param metadata keyed by param name: `{ optional?: boolean, description?: string }`. Mark a placeholder optional so sends may omit it.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"optional": {
"type": "boolean"
},
"description": {
"type": "string",
"maxLength": 500
}
}
}
}
}
}| targets | array | Mode 2: fan-out to 1-1000 recipients over ONE channel, sharing one sendId. Mutually exclusive with `recipient`. |
| logTitle | string | Feed/log title for inline `content` sends only (template sends take it from the template). Placeholders allowed. |
| priority | string | "high" (default) for transactional messages; "low" for marketing/bulk so it never delays transactional sends. |
| notBefore | string | Schedule delivery for later: ISO 8601 datetime with offset, e.g. "2026-09-15T09:00:00+08:00". Omit to send now. |
| recipient | object | Mode 1: the single recipient. Mutually exclusive with `targets`. Channel-specific delivery target. Its shape depends on the kind of the chosen channel (list_channels returns each channel's exact `recipientSchema`): ses_email / smtp_email → `{ email, name?, cc?: [{ email, name? }], bcc?: [...], attachments?: [{ filename, content (base64), contentType?, disposition?: "attachment"|"inline", contentId? }] }`; fcm_push → `{ deviceToken }` OR `{ topic }` (exactly one; Ray keeps no device registry); telegram_bot → `{ chatId }` (numeric id as a string, or "@channelname"); twilio_sms → `{ phoneNumber }` (international E.164, e.g. "+97699112233"); sendsms_mn → `{ phoneNumber }` (Mongolian 8-digit, e.g. "99112233"); slack_webhook, discord_webhook, generic_webhook → `{}` (the destination is part of the channel config). |
| campaignId | string | Free-form campaign id used to group click stats across sends. |
| deliveries | array | Mode 3: the same notification to ONE person over several channels (e.g. push + email), max 10. Each entry has its own channel, recipient and content source. Do not combine with top-level channelConfigId/recipient/targets/templateId/content. |
| showInFeed | boolean | Legacy modes 1-2 flag: also add a feed entry for each target that has an externalUserId. Prefer `feed`. Not valid with `deliveries`. |
| templateId | string | Modes 1-2: a PUBLISHED template to render (see list_templates / publish_template). Exactly one of `templateId` or `content`. Its channelKind must match the channel's `templateKind`. |
| trackClicks | boolean | Email channels only: rewrite links in bodyHtml so clicks are counted (read with get_click_stats). Default false. |
| externalUserId | string | Your own id for the end user. Required with `feed`; keys their in-app feed. |
| idempotencyKey | string | Sent as the Idempotency-Key header. Retrying with the same key and same arguments returns the original sendId without sending again (24h window); the same key with different arguments fails with 409. Use a fresh unique value (e.g. a UUID) per logical notification. |
| logDescription | string | Feed/log description for inline `content` sends only. Placeholders allowed. |
| channelConfigId | string | Modes 1-2: the channel to send through — an `id` from list_channels. Not allowed together with `deliveries`. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"feed": {
"type": "object",
"properties": {
"title": {
"type": "string",
"maxLength": 500
},
"description": {
"type": "string",
"maxLength": 2000
}
},
"description": "Create exactly ONE in-app feed entry for `externalUserId` (read back with list_feed_notifications). `title`/`description` default to the rendered template text; `title` is required for a feed-only send (mode 4)."
},
"params": {
"type": "object",
"description": "Values for the template's `{{placeholders}}`. Any JSON: strings, numbers, booleans, arrays and nested objects (arrays/objects drive `{{#section}}…{{/section}}` blocks). Missing required params are rejected with a 400 naming them.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"content": {
"type": "object",
"description": "Inline message content, used INSTEAD of `templateId` (give exactly one of the two). Shape by channel kind: ses_email / smtp_email → `{ subject, bodyHtml, bodyText }`; fcm_push → `{ title, body, imageUrl?, data?: { [key]: string } }`; slack_webhook → `{ text }` (Slack mrkdwn); discord_webhook → `{ content }` (max 2000 chars); telegram_bot → `{ text, disableLinkPreview? }` (Telegram HTML subset); twilio_sms / sendsms_mn → `{ text }` (plain text; up to 1600 chars on Twilio, sendsms_mn splits long text into SMS of 159 plain-Latin / 69 Cyrillic chars, each billed); generic_webhook → `{ title, body, data?: { [key]: string } }`. `{{name}}` placeholders are filled from `params` and escaped for the channel.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"targets": {
"type": "array",
"items": {
"type": "object",
"required": [
"recipient"
],
"properties": {
"recipient": {
"type": "object",
"description": "Channel-specific delivery target. Its shape depends on the kind of the chosen channel (list_channels returns each channel's exact `recipientSchema`): ses_email / smtp_email → `{ email, name?, cc?: [{ email, name? }], bcc?: [...], attachments?: [{ filename, content (base64), contentType?, disposition?: \"attachment\"|\"inline\", contentId? }] }`; fcm_push → `{ deviceToken }` OR `{ topic }` (exactly one; Ray keeps no device registry); telegram_bot → `{ chatId }` (numeric id as a string, or \"@channelname\"); twilio_sms → `{ phoneNumber }` (international E.164, e.g. \"+97699112233\"); sendsms_mn → `{ phoneNumber }` (Mongolian 8-digit, e.g. \"99112233\"); slack_webhook, discord_webhook, generic_webhook → `{}` (the destination is part of the channel config).",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"externalUserId": {
"type": "string",
"maxLength": 256,
"minLength": 1,
"description": "Your own id for this recipient. With `feed`/`showInFeed` they also get an in-app feed entry."
}
}
},
"maxItems": 1000,
"minItems": 1,
"description": "Mode 2: fan-out to 1-1000 recipients over ONE channel, sharing one sendId. Mutually exclusive with `recipient`."
},
"logTitle": {
"type": "string",
"maxLength": 500,
"description": "Feed/log title for inline `content` sends only (template sends take it from the template). Placeholders allowed."
},
"priority": {
"enum": [
"high",
"low"
],
"type": "string",
"description": "\"high\" (default) for transactional messages; \"low\" for marketing/bulk so it never delays transactional sends."
},
"notBefore": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
"description": "Schedule delivery for later: ISO 8601 datetime with offset, e.g. \"2026-09-15T09:00:00+08:00\". Omit to send now."
},
"recipient": {
"type": "object",
"description": "Mode 1: the single recipient. Mutually exclusive with `targets`. Channel-specific delivery target. Its shape depends on the kind of the chosen channel (list_channels returns each channel's exact `recipientSchema`): ses_email / smtp_email → `{ email, name?, cc?: [{ email, name? }], bcc?: [...], attachments?: [{ filename, content (base64), contentType?, disposition?: \"attachment\"|\"inline\", contentId? }] }`; fcm_push → `{ deviceToken }` OR `{ topic }` (exactly one; Ray keeps no device registry); telegram_bot → `{ chatId }` (numeric id as a string, or \"@channelname\"); twilio_sms → `{ phoneNumber }` (international E.164, e.g. \"+97699112233\"); sendsms_mn → `{ phoneNumber }` (Mongolian 8-digit, e.g. \"99112233\"); slack_webhook, discord_webhook, generic_webhook → `{}` (the destination is part of the channel config).",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"campaignId": {
"type": "string",
"maxLength": 256,
"minLength": 1,
"description": "Free-form campaign id used to group click stats across sends."
},
"deliveries": {
"type": "array",
"items": {
"type": "object",
"required": [
"channelConfigId",
"recipient"
],
"properties": {
"params": {
"type": "object",
"description": "Params for this delivery; falls back to the top-level `params`.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"content": {
"type": "object",
"description": "Inline message content, used INSTEAD of `templateId` (give exactly one of the two). Shape by channel kind: ses_email / smtp_email → `{ subject, bodyHtml, bodyText }`; fcm_push → `{ title, body, imageUrl?, data?: { [key]: string } }`; slack_webhook → `{ text }` (Slack mrkdwn); discord_webhook → `{ content }` (max 2000 chars); telegram_bot → `{ text, disableLinkPreview? }` (Telegram HTML subset); twilio_sms / sendsms_mn → `{ text }` (plain text; up to 1600 chars on Twilio, sendsms_mn splits long text into SMS of 159 plain-Latin / 69 Cyrillic chars, each billed); generic_webhook → `{ title, body, data?: { [key]: string } }`. `{{name}}` placeholders are filled from `params` and escaped for the channel.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"logTitle": {
"type": "string",
"maxLength": 500,
"description": "Feed/log title; only valid with inline `content`."
},
"recipient": {
"type": "object",
"description": "Channel-specific delivery target. Its shape depends on the kind of the chosen channel (list_channels returns each channel's exact `recipientSchema`): ses_email / smtp_email → `{ email, name?, cc?: [{ email, name? }], bcc?: [...], attachments?: [{ filename, content (base64), contentType?, disposition?: \"attachment\"|\"inline\", contentId? }] }`; fcm_push → `{ deviceToken }` OR `{ topic }` (exactly one; Ray keeps no device registry); telegram_bot → `{ chatId }` (numeric id as a string, or \"@channelname\"); twilio_sms → `{ phoneNumber }` (international E.164, e.g. \"+97699112233\"); sendsms_mn → `{ phoneNumber }` (Mongolian 8-digit, e.g. \"99112233\"); slack_webhook, discord_webhook, generic_webhook → `{}` (the destination is part of the channel config).",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"templateId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Published template for this delivery. Exactly one of `templateId` or `content`."
},
"logDescription": {
"type": "string",
"maxLength": 2000,
"description": "Feed/log description; only valid with inline `content`."
},
"channelConfigId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Channel for this delivery (from list_channels)."
}
}
},
"maxItems": 10,
"minItems": 1,
"description": "Mode 3: the same notification to ONE person over several channels (e.g. push + email), max 10. Each entry has its own channel, recipient and content source. Do not combine with top-level channelConfigId/recipient/targets/templateId/content."
},
"showInFeed": {
"type": "boolean",
"description": "Legacy modes 1-2 flag: also add a feed entry for each target that has an externalUserId. Prefer `feed`. Not valid with `deliveries`."
},
"templateId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Modes 1-2: a PUBLISHED template to render (see list_templates / publish_template). Exactly one of `templateId` or `content`. Its channelKind must match the channel's `templateKind`."
},
"trackClicks": {
"type": "boolean",
"description": "Email channels only: rewrite links in bodyHtml so clicks are counted (read with get_click_stats). Default false."
},
"externalUserId": {
"type": "string",
"maxLength": 256,
"minLength": 1,
"description": "Your own id for the end user. Required with `feed`; keys their in-app feed."
},
"idempotencyKey": {
"type": "string",
"maxLength": 255,
"minLength": 1,
"description": "Sent as the Idempotency-Key header. Retrying with the same key and same arguments returns the original sendId without sending again (24h window); the same key with different arguments fails with 409. Use a fresh unique value (e.g. a UUID) per logical notification."
},
"logDescription": {
"type": "string",
"maxLength": 2000,
"description": "Feed/log description for inline `content` sends only. Placeholders allowed."
},
"channelConfigId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Modes 1-2: the channel to send through — an `id` from list_channels. Not allowed together with `deliveries`."
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"templateId",
"channelConfigId",
"recipient"
],
"properties": {
"params": {
"type": "object",
"description": "Values for the template's `{{placeholders}}`. Any JSON: strings, numbers, booleans, arrays and nested objects (arrays/objects drive `{{#section}}…{{/section}}` blocks). Missing required params are rejected with a 400 naming them.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"recipient": {
"type": "object",
"description": "Channel-specific delivery target. Its shape depends on the kind of the chosen channel (list_channels returns each channel's exact `recipientSchema`): ses_email / smtp_email → `{ email, name?, cc?: [{ email, name? }], bcc?: [...], attachments?: [{ filename, content (base64), contentType?, disposition?: \"attachment\"|\"inline\", contentId? }] }`; fcm_push → `{ deviceToken }` OR `{ topic }` (exactly one; Ray keeps no device registry); telegram_bot → `{ chatId }` (numeric id as a string, or \"@channelname\"); twilio_sms → `{ phoneNumber }` (international E.164, e.g. \"+97699112233\"); sendsms_mn → `{ phoneNumber }` (Mongolian 8-digit, e.g. \"99112233\"); slack_webhook, discord_webhook, generic_webhook → `{}` (the destination is part of the channel config).",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"templateId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The template id (UUID)."
},
"channelConfigId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Channel to test through (from list_channels); its templateKind must match the template's channelKind."
}
}
}| templateId* | string | The template id (UUID). |
| channelKind* | string | Which channel family the template renders for. Must match the `templateKind` of the channels you will send through (list_channels). Can't be changed after creation. |
| logDescription* | string | One-line description for the in-app feed and delivery logs. Placeholders allowed. |
| paramOverrides | object | Per-param metadata keyed by param name: `{ optional?: boolean, description?: string }`. Mark a placeholder optional so sends may omit it. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"templateId",
"name",
"channelKind",
"content",
"logTitle",
"logDescription"
],
"properties": {
"name": {
"type": "string",
"maxLength": 100,
"minLength": 1,
"description": "Template name, unique per workspace (archived templates included)."
},
"folder": {
"type": "string",
"maxLength": 200,
"description": "Optional folder path for organisation, e.g. \"billing\"."
},
"content": {
"type": "object",
"description": "Channel-shaped template body; the shape is fixed by `channelKind`: email_html → `{ subject, bodyHtml, bodyText }`; fcm_basic → `{ title, body, imageUrl?, data?: { [key]: string } }`; slack_text → `{ text }`; discord_text → `{ content }` (max 2000 chars); telegram_text → `{ text, disableLinkPreview? }`; sms_text → `{ text }` (plain text, max 1600; long text is split into several billed SMS — on sendsms_mn into parts of 159 plain-Latin / 69 Cyrillic chars); webhook_json → `{ title, body, data?: { [key]: string } }`. Use `{{name}}` placeholders and `{{#items}}…{{/items}}` sections; the required params are derived from them automatically.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"publish": {
"type": "boolean",
"description": "true = publish this version immediately so sends by templateId use it. Default false (saved as draft)."
},
"logTitle": {
"type": "string",
"maxLength": 500,
"minLength": 1,
"description": "Short title shown in the in-app feed and delivery logs, e.g. \"Invoice {{number}} is due\". Placeholders allowed."
},
"templateId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The template id (UUID)."
},
"channelKind": {
"enum": [
"email_html",
"fcm_basic",
"slack_text",
"discord_text",
"telegram_text",
"sms_text",
"webhook_json"
],
"type": "string",
"description": "Which channel family the template renders for. Must match the `templateKind` of the channels you will send through (list_channels). Can't be changed after creation."
},
"logDescription": {
"type": "string",
"maxLength": 2000,
"minLength": 1,
"description": "One-line description for the in-app feed and delivery logs. Placeholders allowed."
},
"paramOverrides": {
"type": "object",
"description": "Per-param metadata keyed by param name: `{ optional?: boolean, description?: string }`. Mark a placeholder optional so sends may omit it.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"optional": {
"type": "boolean"
},
"description": {
"type": "string",
"maxLength": 500
}
}
}
}
}
}