InsureRelay

InsureRelay is the platform's multi-channel communication service. Send transactional emails and SMS messages through a single API, with Handlebars templates, raw content mode, delivery logging, and gas metering built in.

Overview

InsureRelay can be reached through the Janus gateway at /api/relay or directly via K8s internal DNS. All requests are authenticated via OAuth Bearer tokens and rate-limited. Gateway requests are gas-metered automatically by Janus.

Architecture

Your Service
    │
    ├─ transport: direct ──▶  Relay (K8s DNS)  ──▶  scope check
    │                                │
    └─ transport: gateway ──▶  Janus  ──▶  token validation + gas check
                                     │
                                  Relay  ──▶  Render template / raw content
                                     │
                                  SendGrid / Twilio
                                     │
                                  MongoDB log  (delivery record)

Supported channels

ChannelProviderStatus
EmailPlatform SendGridProduction
Email (BYOP)Your SendGrid or G Suite / GmailProduction
SMSTwilioProduction

Authentication

InsureRelay uses the platform's unified dependency model for authentication. Your service declares a dependency with the relay:send scope, and the builder handles OAuth client provisioning and scope grants automatically.

1. Declare the dependency

Add InsureRelay to your catalog-info.yaml:

spec:
  dependencies:
    - service: relay
      scopes: [relay:send]
      transport: direct          # pod-to-pod, no gas charge

On deploy, the builder creates a Bio-ID scope grant request. Since relay is a platform service in the same org, the grant is auto-approved and the relay:send scope is added to your service's OAuth client.

2. Get a Bearer token

Use OAuth client_credentials to get a token with the relay:send scope:

const tokenRes = await fetch(BIO_ID_URL + '/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'client_credentials',
    client_id: process.env.BIO_CLIENT_ID,
    client_secret: process.env.BIO_CLIENT_SECRET,
    scope: 'relay:send',
  }),
})
const { access_token } = await tokenRes.json()

3. Make requests

// Direct transport — use the injected RELAY_URL
const res = await fetch(process.env.RELAY_URL + '/api/relay/send', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${access_token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ /* ... */ }),
})
Use the SDK. The @insureco/relay package handles token acquisition, caching, and refresh automatically. See the SDK section below.
Legacy auth: The old JWT minting approach (signing with JWT_SECRET) is deprecated. Migrate to the spec.dependencies flow. During the migration window, both methods are accepted.

POST /api/relay/send

The primary endpoint for sending messages. Supports two modes: template mode (renders a Handlebars template) and raw content mode (send custom subject/HTML/text directly).

Request body

FieldTypeRequiredDescription
templatestringConditionalTemplate name. Required if content is not provided.
contentobjectConditionalRaw content. Required if template is not provided. See raw content mode.
channelstringYesemail or sms
recipientobjectYesMust include email (for email) or phone (for SMS)
recipient.emailstringConditionalRequired when channel is email
recipient.phonestringConditionalRequired when channel is sms (E.164 format)
recipient.namestringNoRecipient display name (available as recipientName in templates)
dataobjectNoTemplate variables (string, number, boolean, or null values only)
options.fromEmailstringNoWhich connected email account to send from (e.g. [email protected]). If omitted, the first active provider for the org is used, or the platform account. See Email Providers.
options.fromNamestringNoSender display name. Wraps the provider's address. e.g. "Grey PPay" produces Grey PPay <[email protected]> (platform) or Grey PPay <[email protected]> (BYOP)
options.replyTostringNoReply-to address (email only)
options.prioritystringNohigh, normal, or low
metadataobjectNoTracking info: sourceService, sourceAction, correlationId
Provide template or content, not both. If both are present, the request returns a VALIDATION_ERROR.

Response (200 OK)

{
  "success": true,
  "data": {
    "messageId": "7a929734-ecf5-4885-8470-22c357ffa3ca",
    "channel": "email",
    "status": "sent",
    "providerMessageId": "An7Uh39WRei2Rz7CvhsH6g"
  }
}

Raw content mode

Instead of referencing a template, you can send raw content directly. This is useful for dynamic messages that don't fit a predefined template, such as custom notifications or generated reports.

Content fields

FieldTypeRequiredDescription
content.subjectstringYes (email)Email subject line. Ignored for SMS.
content.htmlstringNoHTML body for email. If omitted, text is used.
content.textstringYesPlain text body. Used as the SMS message, or as the email fallback if html is provided.

Raw email example

{
  "channel": "email",
  "content": {
    "subject": "Your monthly report is ready",
    "html": "<h1>Monthly Report</h1><p>Your report for January is attached.</p>",
    "text": "Your monthly report for January is ready."
  },
  "recipient": {
    "email": "[email protected]",
    "name": "Jane Doe"
  },
  "metadata": {
    "sourceService": "report-worker",
    "sourceAction": "monthly_report"
  }
}

Raw SMS example

{
  "channel": "sms",
  "content": {
    "text": "Your verification code is 482910. Expires in 5 minutes."
  },
  "recipient": {
    "phone": "+15551234567"
  }
}

Templates

Templates are Handlebars (.hbs) files with YAML frontmatter defining the subject line. InsureRelay ships with built-in templates and also supports custom templates stored in the database.

Built-in templates

TemplateChannelDescriptionRequired Data
welcomeEmailNew user welcome with email verification linkfirstName, verificationUrl
invitationEmailOrganization invitation with setup linkinviterName, organizationName, inviteUrl
password-resetEmailPassword reset with one-time linkfirstName, resetUrl

Template format

---
subject: "Welcome to InsurEco, {{firstName}}!"
---
<h1>Welcome, {{firstName}}!</h1>
<p>Click below to verify your email:</p>
<a href="{{verificationUrl}}">Verify Email</a>

Listing available templates

GET /api/relay/templates

// Response
{
  "success": true,
  "data": {
    "templates": ["welcome", "invitation", "password-reset", "my-custom-template"]
  }
}

Automatic variables

All templates automatically receive recipientName, recipientEmail, and recipientPhone from the recipient field. You don't need to pass these in data.

Custom templates

In addition to the built-in file-based templates, InsureRelay supports custom templates stored in the database. These can be created, updated, and deleted via the API without redeploying Relay.

Create a custom template

POST /api/relay/templates

{
  "name": "policy-renewal",
  "subject": "Your policy {{policyNumber}} is up for renewal",
  "html": "<h1>Renewal Notice</h1><p>Hi {{firstName}}, your policy {{policyNumber}} expires on {{expiryDate}}.</p>",
  "text": "Hi {{firstName}}, your policy {{policyNumber}} expires on {{expiryDate}}.",
  "channel": "email"
}

Update a custom template

PUT /api/relay/templates/:name

{
  "subject": "Updated: Your policy {{policyNumber}} renewal",
  "html": "<h1>Renewal Reminder</h1><p>Hi {{firstName}}, renew policy {{policyNumber}} before {{expiryDate}}.</p>",
  "text": "Hi {{firstName}}, renew policy {{policyNumber}} before {{expiryDate}}."
}

Delete a custom template

DELETE /api/relay/templates/:name

Custom templates are resolved with the same priority as built-in templates. If a custom template has the same name as a built-in template, the custom template takes precedence.

Template storage: Custom templates are stored in InsureRelay's MongoDB database and are available immediately after creation — no redeploy required.

Sending Email

With a template

const response = await fetch(process.env.RELAY_URL + '/api/relay/send', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    template: 'welcome',
    channel: 'email',
    recipient: {
      email: '[email protected]',
      name: 'Jane Doe',
    },
    data: {
      firstName: 'Jane',
      verificationUrl: 'https://bio.insureco.io/verify?token=abc123',
    },
    metadata: {
      sourceService: 'bio-id',
      sourceAction: 'user_registered',
    },
  }),
})

const result = await response.json()
// { success: true, data: { messageId: "...", channel: "email", status: "sent" } }

With raw content

const response = await fetch(process.env.RELAY_URL + '/api/relay/send', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    channel: 'email',
    content: {
      subject: 'Your invoice #1234',
      html: '<h1>Invoice #1234</h1><p>Amount due: $150.00</p>',
      text: 'Invoice #1234 - Amount due: $150.00',
    },
    recipient: {
      email: '[email protected]',
      name: 'Jane Doe',
    },
  }),
})

The default sender is InsurEco <[email protected]>. Connect your own email provider (SendGrid or G Suite) to send from your own domain — see Email Providers. Use options.fromName to set a display name and options.replyTo to direct replies to a different address.

Email Providers (BYOP)

By default, InsureRelay sends from the platform SendGrid account ([email protected]). You can connect your own SendGrid account or a G Suite / Gmail account so messages come from your own domain — while all traffic still routes through InsureRelay for compliance logging and bounce tracking.

Connect a provider

Go to Console → Deployments → Email Provider. You can connect:

  • SendGrid (BYOP) — paste your API key, the from address, and an optional display name. InsureRelay adds a DNS TXT verification token; once you add it to your domain and click Verify, the provider goes active.
  • G Suite / Gmail — click “Connect with Google” to start an OAuth2 flow. InsureRelay stores only a refresh token (encrypted at rest). The Gmail address becomes your from address immediately after connecting. You can connect multiple Gmail accounts — one per address.
Fallback: If no custom provider is connected (or if the provider fails), InsureRelay falls back to the platform SendGrid account automatically.

Send from a specific account

When you have multiple providers connected, pass options.fromEmail to select which one to use:

await relay.sendEmail({
  template: 'policy-renewal',
  to: { email: '[email protected]', name: 'Jane Doe' },
  data: { policyNumber: 'POL-10042', expiryDate: 'March 31, 2026' },
  options: {
    fromEmail: '[email protected]',   // use the connected SendGrid account
    fromName: 'Claims Team',
  },
})
await relay.sendEmail({
  template: 'meeting-confirmation',
  to: { email: '[email protected]' },
  data: { agentName: 'Derek', meetingTime: '2pm Thursday' },
  options: {
    fromEmail: '[email protected]',         // use the connected Gmail account
    fromName: 'Derek at Acme Insurance',
  },
})

If fromEmail is omitted, the first active provider for your org is used. If none is connected, the platform account sends the message.

Provider API

You can also manage providers programmatically using the relay:admin scope:

MethodPathDescription
GET/api/relay/providers/emailList connected providers (credentials redacted)
POST/api/relay/providers/email/sendgridConnect a SendGrid account
GET/api/relay/providers/email/gsuite/connectStart G Suite OAuth flow (returns consent URL)
POST/api/relay/providers/email/verifyCheck DNS TXT verification for a SendGrid sender domain
DELETE/api/relay/providers/email/:idRemove a provider

Inbound Email

InsureRelay can receive email on your behalf at @insuremail.io addresses. Incoming messages are parsed, stored, and available via API. Common uses: client replies, document submissions, adjuster correspondence.

Claim an inbox

Go to Console → Deployments → Email Provider → Inboxes, or use the API with the relay:admin scope:

POST /api/relay/inboxes
{
  "label": "claims",
  "description": "Client claims submissions"
}

// Response
{
  "success": true,
  "data": {
    "id": "...",
    "address": "[email protected]",
    "label": "claims",
    "orgSlug": "youragency"
  }
}

Once claimed, [email protected] is reserved for your org. Any email sent to it will be received and stored by InsureRelay.

Read received messages

// List messages for your org (most recent first)
GET /api/relay/inbound

// Response
{
  "success": true,
  "data": {
    "messages": [
      {
        "messageId": "7a929734-ecf5-4885-8470-22c357ffa3ca",
        "inboxAddress": "[email protected]",
        "from": { "email": "[email protected]", "name": "Jane Doe" },
        "subject": "Claim #CLM-10042 documents",
        "textBody": "Please find my supporting documents attached.",
        "extractedEntities": {
          "claimNumbers": ["CLM-10042"],
          "policyNumbers": [],
          "emails": ["[email protected]"]
        },
        "status": "received",
        "receivedAt": "2026-02-21T14:30:00.000Z"
      }
    ]
  }
}
// Get a single message
GET /api/relay/inbound/:messageId

Extracted entities

InsureRelay automatically extracts structured data from every inbound message:

FieldDescription
claimNumbersClaim references matching common patterns (e.g. CLM-10042)
policyNumbersPolicy references (e.g. POL-10042, PL1234567)
datesDates mentioned in the email body
emailsEmail addresses found in the body

Inbox API

MethodPathDescription
POST/api/relay/inboxesClaim an @insuremail.io inbox address
GET/api/relay/inboxesList your org's claimed inboxes
DELETE/api/relay/inboxes/:idRelease an inbox address
GET/api/relay/inboundList received messages (most recent first, 50 per page)
GET/api/relay/inbound/:idGet a single received message
Retention: Inbound messages are stored for 365 days (compliance hold), then automatically deleted.

Sending SMS

With a template

const response = await fetch(process.env.RELAY_URL + '/api/relay/send', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    template: 'password-reset',
    channel: 'sms',
    recipient: {
      phone: '+15551234567',
      name: 'Jane Doe',
    },
    data: {
      firstName: 'Jane',
      resetCode: '482910',
    },
  }),
})

With raw content

const response = await fetch(process.env.RELAY_URL + '/api/relay/send', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    channel: 'sms',
    content: {
      text: 'Your verification code is 482910. Expires in 5 minutes.',
    },
    recipient: {
      phone: '+15551234567',
    },
  }),
})
Phone format: Phone numbers must be in E.164 format (e.g. +15551234567).
SMS uses plain text. For template mode, the same templates work for both channels. For email, the HTML rendering is used. For SMS, the plain text rendering is sent (HTML tags are stripped automatically).

Gas Pricing

Gas is charged when using transport: gateway (through Janus). Direct transport calls are not gas-metered.

RouteGas CostCharged When
/api/relay/send1 tokenResponse is 2xx (message sent)
/api/relay/templates1 tokenResponse is 2xx
/api/relay/status/:id1 tokenResponse is 2xx
Use direct transport to avoid gas charges. Most services should declare transport: direct for relay. Gas is only charged when routing through the Janus gateway (transport: gateway).

See Gas & Wallets for more on how gas metering works.

Message Status

Look up the delivery status of a previously sent message using the messageId from the send response.

GET /api/relay/status/:messageId

// Response
{
  "success": true,
  "data": {
    "messageId": "7a929734-ecf5-4885-8470-22c357ffa3ca",
    "channel": "email",
    "template": "welcome",
    "status": "sent",
    "recipient": { "email": "[email protected]", "name": "Jane Doe" },
    "sender": "InsurEco <[email protected]>",
    "sentAt": "2026-02-14T03:37:21.815Z",
    "providerMessageId": "An7Uh39WRei2Rz7CvhsH6g"
  }
}

Error Handling

All errors follow the standard Tawa error envelope. InsureRelay returns these error codes:

HTTP StatusError CodeMeaning
400VALIDATION_ERRORRequest body failed schema validation (includes providing both template and content)
400TEMPLATE_NOT_FOUNDThe requested template does not exist
401UNAUTHORIZEDMissing or invalid authentication
402INSUFFICIENT_GASWallet balance too low (gateway transport only)
403INSUFFICIENT_SCOPEBearer token missing the relay:send scope
404NOT_FOUNDMessage ID does not exist (status endpoint)
502PROVIDER_ERRORSendGrid or Twilio returned an error

Error response format

{
  "success": false,
  "error": {
    "code": "TEMPLATE_NOT_FOUND",
    "message": "Template 'signup' not found"
  }
}
Provider errors are sanitized. If SendGrid or Twilio fail, InsureRelay returns a generic PROVIDER_ERROR message without leaking internal provider details.

SDK

The @insureco/relay package handles OAuth token acquisition, caching, refresh, retries, and error handling automatically. It supports both template and raw content modes.

Install

npm install @insureco/relay

Setup

import { RelayClient } from '@insureco/relay'

// Option 1: Explicit config
const relay = new RelayClient({
  relayUrl: process.env.RELAY_URL,
  bioIdUrl: process.env.BIO_ID_URL || 'https://bio.tawa.insureco.io',
  clientId: process.env.BIO_CLIENT_ID,
  clientSecret: process.env.BIO_CLIENT_SECRET,
})

// Option 2: Auto-detect from environment variables
// Reads RELAY_URL, BIO_ID_URL, BIO_CLIENT_ID, BIO_CLIENT_SECRET
const relay = RelayClient.fromEnv()

Send email (template)

const result = await relay.sendEmail({
  template: 'welcome',
  to: { email: '[email protected]', name: 'Jane Doe' },
  data: {
    firstName: 'Jane',
    verificationUrl: 'https://bio.insureco.io/verify?token=abc123',
  },
  options: {
    fromEmail: '[email protected]', // optional: use a connected provider
    fromName: 'Acme Insurance',
  },
})

// result.messageId, result.status, result.providerMessageId

Send raw email

const result = await relay.sendRawEmail({
  to: { email: '[email protected]', name: 'Jane Doe' },
  subject: 'Your monthly report is ready',
  html: '<h1>Monthly Report</h1><p>Your report for January is attached.</p>',
  text: 'Your monthly report for January is ready.',
})

Send SMS (template)

await relay.sendSMS({
  template: 'password-reset',
  to: { phone: '+15551234567', name: 'Jane' },
  data: { resetCode: '123456' },
})

Send raw SMS

await relay.sendRawSMS({
  to: { phone: '+15551234567' },
  text: 'Your verification code is 482910. Expires in 5 minutes.',
})

Fire-and-forget pattern

// Don't block the user — send async and log failures
relay.sendEmail({
  template: 'welcome',
  to: { email: user.email, name: user.firstName },
  data: { firstName: user.firstName, verificationUrl },
}).catch((err) => logger.error('Welcome email failed:', err.message))

Local development

For local dev, set RELAY_DIRECT_URL to bypass OAuth and hit InsureRelay directly:

# .env.local
RELAY_DIRECT_URL=http://localhost:3001
RELAY_INTERNAL_KEY=relay-internal-key-2026

The SDK switches to direct mode automatically when RELAY_DIRECT_URL is set. Uses X-Internal-Key header instead of OAuth Bearer auth.

Error handling

import { RelayClient, RelayError } from '@insureco/relay'

try {
  await relay.sendEmail({ /* ... */ })
} catch (err) {
  if (err instanceof RelayError) {
    // err.statusCode  — HTTP status (400, 401, 402, 403, 502)
    // err.code        — error code ('VALIDATION_ERROR', 'INSUFFICIENT_SCOPE', etc.)
    // err.message     — human-readable message
  }
}

Environment variables

VariableModeDescription
RELAY_URLProductionInjected by builder when spec.dependencies includes relay
BIO_CLIENT_IDProductionAuto-provisioned OAuth client ID
BIO_CLIENT_SECRETProductionAuto-provisioned OAuth client secret
BIO_ID_URLProductionBio-ID base URL (default: https://bio.tawa.insureco.io)
RELAY_DIRECT_URLLocal devDirect relay URL (skips OAuth)
RELAY_INTERNAL_KEYLocal devAPI key for direct relay access
Source code: The SDK is at github.com/insurecosys/iec-relay-sdk. PRs welcome.