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
| Channel | Provider | Status |
|---|---|---|
| Platform SendGrid | Production | |
| Email (BYOP) | Your SendGrid or G Suite / Gmail | Production |
| SMS | Twilio | Production |
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 chargeOn 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({ /* ... */ }),
})@insureco/relay package handles token acquisition, caching, and refresh automatically. See the SDK section below.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
| Field | Type | Required | Description |
|---|---|---|---|
template | string | Conditional | Template name. Required if content is not provided. |
content | object | Conditional | Raw content. Required if template is not provided. See raw content mode. |
channel | string | Yes | email or sms |
recipient | object | Yes | Must include email (for email) or phone (for SMS) |
recipient.email | string | Conditional | Required when channel is email |
recipient.phone | string | Conditional | Required when channel is sms (E.164 format) |
recipient.name | string | No | Recipient display name (available as recipientName in templates) |
data | object | No | Template variables (string, number, boolean, or null values only) |
options.fromEmail | string | No | Which 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.fromName | string | No | Sender display name. Wraps the provider's address. e.g. "Grey PPay" produces Grey PPay <[email protected]> (platform) or Grey PPay <[email protected]> (BYOP) |
options.replyTo | string | No | Reply-to address (email only) |
options.priority | string | No | high, normal, or low |
metadata | object | No | Tracking info: sourceService, sourceAction, correlationId |
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
| Field | Type | Required | Description |
|---|---|---|---|
content.subject | string | Yes (email) | Email subject line. Ignored for SMS. |
content.html | string | No | HTML body for email. If omitted, text is used. |
content.text | string | Yes | Plain 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
| Template | Channel | Description | Required Data |
|---|---|---|---|
welcome | New user welcome with email verification link | firstName, verificationUrl | |
invitation | Organization invitation with setup link | inviterName, organizationName, inviteUrl | |
password-reset | Password reset with one-time link | firstName, 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/:nameCustom 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.
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.
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:
| Method | Path | Description |
|---|---|---|
| GET | /api/relay/providers/email | List connected providers (credentials redacted) |
| POST | /api/relay/providers/email/sendgrid | Connect a SendGrid account |
| GET | /api/relay/providers/email/gsuite/connect | Start G Suite OAuth flow (returns consent URL) |
| POST | /api/relay/providers/email/verify | Check DNS TXT verification for a SendGrid sender domain |
| DELETE | /api/relay/providers/email/:id | Remove 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/:messageIdExtracted entities
InsureRelay automatically extracts structured data from every inbound message:
| Field | Description |
|---|---|
claimNumbers | Claim references matching common patterns (e.g. CLM-10042) |
policyNumbers | Policy references (e.g. POL-10042, PL1234567) |
dates | Dates mentioned in the email body |
emails | Email addresses found in the body |
Inbox API
| Method | Path | Description |
|---|---|---|
| POST | /api/relay/inboxes | Claim an @insuremail.io inbox address |
| GET | /api/relay/inboxes | List your org's claimed inboxes |
| DELETE | /api/relay/inboxes/:id | Release an inbox address |
| GET | /api/relay/inbound | List received messages (most recent first, 50 per page) |
| GET | /api/relay/inbound/:id | Get a single received message |
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',
},
}),
})+15551234567).Gas Pricing
Gas is charged when using transport: gateway (through Janus). Direct transport calls are not gas-metered.
| Route | Gas Cost | Charged When |
|---|---|---|
/api/relay/send | 1 token | Response is 2xx (message sent) |
/api/relay/templates | 1 token | Response is 2xx |
/api/relay/status/:id | 1 token | Response is 2xx |
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 Status | Error Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Request body failed schema validation (includes providing both template and content) |
| 400 | TEMPLATE_NOT_FOUND | The requested template does not exist |
| 401 | UNAUTHORIZED | Missing or invalid authentication |
| 402 | INSUFFICIENT_GAS | Wallet balance too low (gateway transport only) |
| 403 | INSUFFICIENT_SCOPE | Bearer token missing the relay:send scope |
| 404 | NOT_FOUND | Message ID does not exist (status endpoint) |
| 502 | PROVIDER_ERROR | SendGrid or Twilio returned an error |
Error response format
{
"success": false,
"error": {
"code": "TEMPLATE_NOT_FOUND",
"message": "Template 'signup' not found"
}
}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/relaySetup
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.providerMessageIdSend 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-2026The 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
| Variable | Mode | Description |
|---|---|---|
RELAY_URL | Production | Injected by builder when spec.dependencies includes relay |
BIO_CLIENT_ID | Production | Auto-provisioned OAuth client ID |
BIO_CLIENT_SECRET | Production | Auto-provisioned OAuth client secret |
BIO_ID_URL | Production | Bio-ID base URL (default: https://bio.tawa.insureco.io) |
RELAY_DIRECT_URL | Local dev | Direct relay URL (skips OAuth) |
RELAY_INTERNAL_KEY | Local dev | API key for direct relay access |
github.com/insurecosys/iec-relay-sdk. PRs welcome.