Raw schema
{
"type": "object",
"properties": {
"channelConfigId": {
"description": "Modes 1-2: the channel to send through — an `id` from list_channels. Not allowed together with `deliveries`.",
"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)$"
},
"recipient": {
"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\"); slack_webhook, discord_webhook, generic_webhook → `{}` (the destination is part of the channel config).",
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"targets": {
"description": "Mode 2: fan-out to 1-1000 recipients over ONE channel, sharing one sendId. Mutually exclusive with `recipient`.",
"minItems": 1,
"maxItems": 1000,
"type": "array",
"items": {
"type": "object",
"properties": {
"recipient": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {},
"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\"); slack_webhook, discord_webhook, generic_webhook → `{}` (the destination is part of the channel config)."
},
"externalUserId": {
"description": "Your own id for this recipient. With `feed`/`showInFeed` they also get an in-app feed entry.",
"type": "string",
"minLength": 1,
"maxLength": 256
}
},
"required": [
"recipient"
]
}
},
"deliveries": {
"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.",
"minItems": 1,
"maxItems": 10,
"type": "array",
"items": {
"type": "object",
"properties": {
"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)."
},
"recipient": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {},
"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\"); slack_webhook, discord_webhook, generic_webhook → `{}` (the destination is part of the channel config)."
},
"templateId": {
"description": "Published template for this delivery. Exactly one of `templateId` or `content`.",
"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)$"
},
"content": {
"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); generic_webhook → `{ title, body, data?: { [key]: string } }`. `{{name}}` placeholders are filled from `params` and escaped for the channel.",
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"params": {
"description": "Params for this delivery; falls back to the top-level `params`.",
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"logTitle": {
"description": "Feed/log title; only valid with inline `content`.",
"type": "string",
"maxLength": 500
},
"logDescription": {
"description": "Feed/log description; only valid with inline `content`.",
"type": "string",
"maxLength": 2000
}
},
"required": [
"channelConfigId",
"recipient"
]
}
},
"templateId": {
"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`.",
"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)$"
},
"content": {
"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); generic_webhook → `{ title, body, data?: { [key]: string } }`. `{{name}}` placeholders are filled from `params` and escaped for the channel.",
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"params": {
"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.",
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"logTitle": {
"description": "Feed/log title for inline `content` sends only (template sends take it from the template). Placeholders allowed.",
"type": "string",
"maxLength": 500
},
"logDescription": {
"description": "Feed/log description for inline `content` sends only. Placeholders allowed.",
"type": "string",
"maxLength": 2000
},
"feed": {
"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).",
"type": "object",
"properties": {
"title": {
"type": "string",
"maxLength": 500
},
"description": {
"type": "string",
"maxLength": 2000
}
}
},
"externalUserId": {
"description": "Your own id for the end user. Required with `feed`; keys their in-app feed.",
"type": "string",
"minLength": 1,
"maxLength": 256
},
"showInFeed": {
"description": "Legacy modes 1-2 flag: also add a feed entry for each target that has an externalUserId. Prefer `feed`. Not valid with `deliveries`.",
"type": "boolean"
},
"trackClicks": {
"description": "Email channels only: rewrite links in bodyHtml so clicks are counted (read with get_click_stats). Default false.",
"type": "boolean"
},
"campaignId": {
"description": "Free-form campaign id used to group click stats across sends.",
"type": "string",
"minLength": 1,
"maxLength": 256
},
"notBefore": {
"description": "Schedule delivery for later: ISO 8601 datetime with offset, e.g. \"2026-09-15T09:00:00+08:00\". Omit to send now.",
"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)))$"
},
"priority": {
"description": "\"high\" (default) for transactional messages; \"low\" for marketing/bulk so it never delays transactional sends.",
"type": "string",
"enum": [
"high",
"low"
]
},
"idempotencyKey": {
"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.",
"type": "string",
"minLength": 1,
"maxLength": 255
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}