Ray
PricingDocsSign inStart free
Browse the docs

Getting started

Ray documentationAuthenticationUsing Ray with AI coding agentsMCP serverTypeScript SDK

Guides

Sending notificationsIdempotencyTemplatesScheduling and test sendsStatus and feedsClick trackingDelivery webhooks

Channels

Email with Amazon SESEmail with SMTPPush notifications with Firebase Cloud MessagingSlack notificationsDiscord notificationsTelegram notificationsSMS with TwilioSMS with sendsms.mnHTTPS webhook channel

Reference

Rate limits and quotasErrors

Getting started

Ray documentationAuthenticationUsing Ray with AI coding agentsMCP serverTypeScript SDK

Guides

Sending notificationsIdempotencyTemplatesScheduling and test sendsStatus and feedsClick trackingDelivery webhooks

Channels

Email with Amazon SESEmail with SMTPPush notifications with Firebase Cloud MessagingSlack notificationsDiscord notificationsTelegram notificationsSMS with TwilioSMS with sendsms.mnHTTPS webhook channel

Reference

Rate limits and quotasErrors

Guides

Scheduling and test sends

Schedule Ray notifications for later with notBefore, and test a published template against a real recipient with POST /templates/{id}/test-send.

View as Markdownllms.txtAPI reference

Two tools for controlling when and how a notification goes out: notBefore delays a normal send, and test sends deliver a template to yourself without touching customer feeds, webhooks or your monthly quota. All requests go to https://ray-api.gege.mn with Authorization: Bearer $RAY_API_KEY (write scope).

Scheduled sends

Add notBefore (an ISO 8601 datetime) to any POST /send request.

curl -X POST https://ray-api.gege.mn/send \
  -H "Authorization: Bearer $RAY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: trial-ending-user_123" \
  -d '{
    "channelConfigId": "9f1c2a7e-3b4d-4e5f-8a6b-1c2d3e4f5a6b",
    "templateId": "1d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f5a",
    "params": { "name": "Ada", "daysLeft": 3 },
    "recipient": { "email": "[email protected]" },
    "externalUserId": "user_123",
    "showInFeed": true,
    "notBefore": "2026-09-20T09:00:00+08:00"
  }'
const inThreeDays = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString();

const res = await fetch("https://ray-api.gege.mn/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RAY_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "trial-ending-user_123",
  },
  body: JSON.stringify({
    channelConfigId: "9f1c2a7e-3b4d-4e5f-8a6b-1c2d3e4f5a6b",
    templateId: "1d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f5a",
    params: { name: "Ada", daysLeft: 3 },
    recipient: { email: "[email protected]" },
    notBefore: inThreeDays,
  }),
});
const { sendId } = (await res.json()) as { sendId: string };

How scheduling behaves:

  • The request is validated, rendered and stored immediately, and returns 202 with a sendId. Validation errors surface now, not at delivery time.
  • Content is rendered at request time. Publishing a new template version later does not change a scheduled send.
  • Delivery rows stay pending (with notBefore set) until the time arrives, then dispatch normally.
  • A notBefore in the past dispatches right away.
  • Both Z and numeric offsets (+08:00) are accepted.
  • Feed entries of a scheduled send are hidden from GET /notifications until notBefore.
  • Notifications are counted toward your quota when the request is accepted.
  • If the channel config or the workspace is archived before the send is due, the rows end as failed_terminal (channel config archived or workspace archived).
  • There is no API to cancel or reschedule a scheduled send. For reminders that may become obsolete, schedule them in your own system and call POST /send when due.

Test sends

POST /templates/{id}/test-send delivers a template's published version through a real channel config to a real recipient. It runs the full pipeline (render, suppression check, provider call), so it proves credentials, content and recipient all work.

A test send differs from a normal send:

  • The delivery row is marked isTest: true.
  • It never creates a feed entry and never fires delivery webhooks.
  • It does not count toward your monthly quota. It has its own daily allowance instead (below).
  • It respects suppressions: a suppressed address is recorded as suppressed and not sent.
  • It takes only a template. To test inline content, make a normal POST /send.
  • It does not accept Idempotency-Key, notBefore, priority or fan-out.

The dashboard's Send test button on a template uses the same endpoint.

Request

Field Type Notes
channelConfigId uuid Required. Must be a channel compatible with the template kind.
recipient object Required. Channel-specific shape, see the channel guides.
params object Values for the template's required params. Default {}.
curl -X POST https://ray-api.gege.mn/templates/1d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f5a/test-send \
  -H "Authorization: Bearer $RAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channelConfigId": "9f1c2a7e-3b4d-4e5f-8a6b-1c2d3e4f5a6b",
    "recipient": { "email": "[email protected]" },
    "params": { "name": "Ada", "code": "000000" }
  }'

202 Accepted:

{ "sendId": "6a7b8c9d-0e1f-4a2b-9c3d-4e5f6a7b8c9d" }
async function testTemplate(templateId: string, channelConfigId: string, recipient: object, params: object) {
  const res = await fetch(`https://ray-api.gege.mn/templates/${templateId}/test-send`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.RAY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ channelConfigId, recipient, params }),
  });
  if (res.status !== 202) throw new Error(`test send failed: ${res.status} ${await res.text()}`);
  const { sendId } = (await res.json()) as { sendId: string };
  return sendId; // poll GET /sends/{sendId} to see the provider result
}

Check the outcome with GET /sends/{sendId}. A failed provider call shows up there as failed_terminal with providerError.

Daily allowance

Test sends are limited per workspace per UTC day:

Plan Test sends per day
Free 50
Starter 250
Pro 500
Scale 500

Every test-send response carries RayTestQuota-Limit, RayTestQuota-Used and RayTestQuota-Remaining headers. Past the allowance the call fails with 402:

{
  "error": "quota_exceeded",
  "message": "daily test-send allowance exceeded (50/day on the free plan)",
  "limit": 50,
  "used": 50
}

Errors

Status error message
400 validation_error missing required params: ..., recipient: <reason>, template kind '...' is not compatible with channel '...', write scope required
402 quota_exceeded daily test-send allowance exceeded (...)
404 not_found channel config not found, template not found or not published
PreviousTemplatesNextStatus and feeds

On this page

  • Scheduled sends
  • Test sends
  • Request
  • Daily allowance
  • Errors
Ray

Notification delivery for teams that already have providers.

Product

PricingDocsAPI referenceMCP serverTypeScript SDKAgent skillsllms.txt

Legal

TermsPrivacyRefunds

Contact

[email protected]

© 2026 L’Atelier Gege LLC