ObjectStackObjectStack

services.email — the outbound mail API

Send email from a plugin or flow with services.email — send() and sendTemplate(), the result they return, and the typed error codes to handle.

  • Stability: stable
  • Canonical source: packages/spec/src/contracts/email-service.ts
  • Registry slot: email — resolve with ctx.getService('email').

Methods

services.email.send(input: SendEmailInput): Promise<SendEmailResult>
services.email.sendTemplate(input: SendTemplateInput): Promise<SendEmailResult>

services.email.getDefaultFrom?.() returns the configured sender, the from that send() uses when the input names none. It is undefined when no sender is configured.

Over HTTP

POST /api/v1/email/send (client.email.send) hands a message to send(). Plugins and flows call send() directly and are not affected by what follows. The HTTP door checks four things first, and refuses a failure with 403 PERMISSION_DENIED before anything is sent:

  • The caller must hold the manage_platform_settings capability.
  • sentBy is always the authenticated caller. A body that names another user is refused.
  • The sender is the deployment's configured sender. A from naming another address is refused. A from naming the configured address is replaced by the configured value. Omit from to send as the configured sender. headers, when sent, must be a plain object that maps each header name to a single-line string value. Any other shape is refused, and so are custom From, Sender and Return-Path headers.
  • The message is recorded under the caller's active organization, which the door sets as organizationId. A body that names another organization is refused, and so is one that names an organization when the caller has none. identityMail is refused: identity mail (verification, password reset, invitations) is sent by the server itself. Omit both.

replyTo is not bounded. It is the caller's choice of Reply-To address and makes no claim about the sender.

Returns

SendEmailResult:

  • id: string
  • status: 'queued' | 'sent' | 'failed'
  • messageId?: string
  • error?: string

Typed Error Codes

sendTemplate may return/throw implementation errors including:

  • TEMPLATE_NOT_FOUND
  • TEMPLATE_INACTIVE
  • MISSING_VARIABLES
  • TEMPLATE_INVALID: the stored template has a hole the authoring rules refuse (a second formatter, or a currency argument that is not a literal ISO 4217 code). It is permanent: retrying fails until the template is re-published without that hole. renderTemplate raises it too. It is also raised when the template has a currency hole with no argument ({{ amount | currency }}) and the call carries no currency: such a hole formats in the organization's default currency (localization.currency), which the notification channels pass for you and a direct caller passes as currency. Nothing is sent until the default is set or the hole names its code.

On this page