---
title: "Trigger Notification"
method: POST
path: "/v1/notifications"
tags: ["notifications"]
---

# Trigger Notification

`POST /v1/notifications`

## Request

- Base URL: `https://api.elaan.io` (the document declares no server; this is the origin it was published from)
- URL: `https://api.elaan.io/v1/notifications`
- Auth: none declared

The tenant front door: record one triggered event per recipient and accept
(202). The request does no per-channel work — channel resolution, rendering
and delivery happen asynchronously in the fan-out worker, so one channel's
(or one recipient's) failure can't roll back another. Each recipient's
`event_id` correlates its accepted event for status.

An archived account can't send: reject up front (403) rather than record
events that fan-out would only skip. (A None tenant — a tenant_id that
authenticated but has no row — is a data-integrity impossibility, not an
archived account, so it's deliberately allowed through rather than 403'd.)

The archived check runs *before* the idempotency replay deliberately: an
archived account gets the same 403 whether or not it happens to be resending
a token, rather than one answer for a fresh trigger and another for a retry.

## Headers

- `Idempotency-Key` string, nullable: Optional dedupe token for this request. Send the same token again and the original event ids come back without sending anything a second time — for callers driven by an at-least-once source (a queue, a webhook, a retried job), where a redelivery is routine. Reusing a token for a *different* payload is a 409. Scoped to your account, at most 255 characters.
- `authorization` string, nullable

## Request body

- TriggerNotificationRequest: A tenant triggering an event for one or more recipients (their own `external_id`s — e.g. every subscriber of a resource). One event is recorded per recipient, so each fans out, retries, and reports status independently. The branding key is optional — omit it (or send an unknown one) and fan-out falls back to the tenant's default branding, unless `strict` is set, which rejects an unknown one instead. Recorded + accepted (202); channel resolution + delivery happen asynchronously in the fan-out worker.
  - `notification_type_key` string, required
  - `external_ids` string[], required
  - `branding_key` string, nullable
  - `strict` boolean: Refuse the trigger rather than substituting a different brand for the one it named. By default a `branding_key` that does not exist falls back to the account's default brand and the send goes out, so a typo cannot stop your mail. With `strict` that same trigger is a **422** and nothing is queued. Covers the branding key only; it is not a general validation mode, and an unknown `notification_type_key` is still accepted here and reported on the event. Leave it off unless you would rather send nothing than send under the wrong brand.
  - `variables` object: Values for the template's `{{ slot }}` markers. Any JSON is accepted: strings, numbers, booleans, nested objects (read with `{{ order.customer.name }}`) and lists a template repeats over with `{% for line in lines %} … {% endfor %}` — invoice lines, order items, digest entries. A name you don't send renders empty rather than failing, and a list you don't send repeats zero times.
  - `secret_variables` object: Values that must reach the recipient but must not be readable back afterwards — a one-time code, a password-reset link, a signed URL. They fill `{{ slot }}` markers exactly like `variables` and are sent in full, but the activity log shows them masked: the raw value is hidden, and the message body shown there is a second render in which the secret is replaced. Flat strings only (no repeat-block lists), and a name used here must not also appear in `variables`.

## Response `202`

Successful Response

- NotificationAcceptedResponse: 202 response: one event was recorded per recipient and will be delivered asynchronously. `events` maps each recipient to its event id so the tenant can correlate and query per-recipient delivery status.
  - `events` AcceptedEvent[], required
    - `external_id` string, required
    - `event_id` string, required

## Other responses

- `409`: The Idempotency-Key was already used for a request with a different payload (permanent — use a new key), or a concurrent copy of this same request is holding it (transient — retry for the replay)
- `422`: Validation Error

---

[API](https://skmtc.dev/elaan/apis/elaan-api.md) · [All operations](https://skmtc.dev/elaan/apis/elaan-api/llms.txt) · [OpenAPI document](https://skmtc.dev/elaan/apis/elaan-api/revisions/c96a7e6399aa?raw)
