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 withctx.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_settingscapability. sentByis always the authenticated caller. A body that names another user is refused.- The sender is the deployment's configured sender. A
fromnaming another address is refused. Afromnaming the configured address is replaced by the configured value. Omitfromto 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 customFrom,SenderandReturn-Pathheaders. - 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.identityMailis 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: stringstatus: 'queued' | 'sent' | 'failed'messageId?: stringerror?: string
Typed Error Codes
sendTemplate may return/throw implementation errors including:
TEMPLATE_NOT_FOUNDTEMPLATE_INACTIVEMISSING_VARIABLESTEMPLATE_INVALID: the stored template has a hole the authoring rules refuse (a second formatter, or acurrencyargument that is not a literal ISO 4217 code). It is permanent: retrying fails until the template is re-published without that hole.renderTemplateraises it too. It is also raised when the template has acurrencyhole with no argument ({{ amount | currency }}) and the call carries nocurrency: such a hole formats in the organization's default currency (localization.currency), which the notification channels pass for you and a direct caller passes ascurrency. Nothing is sent until the default is set or the hole names its code.