comms
Sending email and SMS through your own providers, from templates you own.
Server client only. comms is not available on createClient — a publishable
key must never be able to send messages.
Your code names a template and a language. Which provider carries it, and what the message actually says, are configuration — so changing either is not a deploy.
Sending is queued
A call that resolves without an error means the message was accepted for delivery, not delivered. The call publishes; a worker sends. Anything that happens after that point is recorded on the message, not returned to you — see Sending is queued.
email.send
email.send(params) → null
const { data, error } = await nulvless.comms.email.send({
to: '[email protected]',
template: 'grants-invite',
language: 'pt-BR',
params: { organization: 'Acme Inc', role: 'admin' },
});tostringRequiredRecipient address.
templatestringRequiredThe slug. Must exist for this channel and this language.
languageLanguageOptionalWhich language variant to render. Falls back to the environment default when omitted.
paramsRecord<string, unknown>OptionalVariables injected into the template. A template that expects a variable you did not supply fails at delivery, not at this call.
sms.send
sms.send(params) → null
The same shape, with phone instead of to.
await nulvless.comms.sms.send({
phone: { country_code: '+55', number: '11987654321' },
template: 'phone-otp-verification',
language: 'en-US',
params: { code: '492013' },
});phonePhoneRequiredCountry code and number, not a formatted string.
templatestringRequiredlanguageLanguageOptionalparamsRecord<string, unknown>OptionalSends are quota-limited per channel. Exhausting it answers 429 rate_limit_exceeded — with no retryAfter, unlike the auth service, so keep
your own backoff.
email.test and sms.test
test(recipient) → null
Sends a probe so you can confirm the provider is wired before a real message depends on it.
await nulvless.comms.email.test('[email protected]');Worth running from your deploy pipeline against a fresh environment — a misconfigured provider is otherwise invisible until the first user needs a verification code.
Built-in templates
Every environment starts with the templates the platform itself needs. These are the slugs, and each is editable:
| SMS | |
|---|---|
welcome-user | phone-otp-verification |
email-otp-verification | phone-link-verification |
email-link-verification | phone-otp-enable-mfa-request |
email-otp-enable-mfa-request | change-phone-identity |
change-email-identity | |
reset-password-email | |
grants-invite |
A template is per channel: the email and SMS versions of the same idea are separate slugs, because wording that works in an inbox does not survive 160 characters.
templates
list · get · create · update · delete
await nulvless.comms.templates.list({ channel: 'email' });
await nulvless.comms.templates.get('email', 'welcome-user', 'pt-BR');
await nulvless.comms.templates.create(params);
await nulvless.comms.templates.update('email', 'welcome-user', params, 'pt-BR');
await nulvless.comms.templates.delete('email', 'welcome-user', 'pt-BR');Everything but create and list is addressed by channel + slug +
language. That triple is the identity of a template — there is no id to hold
onto, and the same slug in two languages is two records.
Most teams edit these in the console. The API exists for when you version them alongside your application.
create
slugstringRequiredWhat your code will name.
channelNotificationChannelRequiredemailRequires subject.smsBody only.webhookRequires url — posts instead of rendering a message.languageTemplateLanguageRequireden-US or pt-BR.
contentstringRequiredThe body.
subjectstringOptionalRequired for the email channel.
urlstringOptionalRequired for the webhook channel.
namestringOptionaldescriptionstringOptionalconfigRecord<string, unknown>Optionalupdate takes the same optional fields minus slug, channel and language —
those identify the template rather than describe it.
The webhook channel
Besides email and SMS a template can be a webhook: instead of rendering a
message for a person, it posts to a url you own. The call site is identical —
same slug, same params — so a notification can move from email to a webhook as
configuration, without touching the code that triggers it.
The Template object
idstringRequiredslugstringRequiredchannelNotificationChannelRequiredlanguageTemplateLanguageRequiredcontentstringRequiredsubjectstringOptionalurlstringOptionalcommonbooleanRequiredtrue for a platform-level built-in, false for one you created. Useful for
showing which templates came with the environment.
last_submissionstringOptionalcreated_atstringRequiredupdated_atstringRequiredWhy name a template
Naming a template instead of composing a message is what makes the rest work:
- Per language — the same call renders
pt-BRoren-USfrom one slug. - Per environment — development points at a sandbox provider, so a test never reaches a real inbox.
- Per channel — email and SMS versions of one message are edited side by side.
None of that requires the calling code to change.