AAuthfy Docs

Company notification API

How a company integration submits email and SMS requests to Authfy. Provider credentials, sender identities, branding, and enabled channels are configured by your Authfy administrator; callers only submit message content and recipients.

Before sending

  1. Configure the company at /companies/{companyId}/notifications in the support console.
  2. Enable the required channel and configure its provider.
  3. Issue the company's notification API key.
  4. Send that key as X-API-Key on every request:
X-API-Key: afk_<company-api-key>
Content-Type: application/json

Do not put the company ID or provider credentials in the request body — Authfy resolves the company from X-API-Key. For a company-scoped request, the configured company provider overrides emailClient or messageClient in the body.

Send one email

POST /api/v1/email/new

Plain or rich HTML body

When templateName is omitted, body is sent directly as the HTML email body. Use this when the calling application has already prepared the complete email:

curl --request POST 'https://authfy-api.example.com/api/v1/email/new' \
  --header 'Content-Type: application/json' \
  --header 'X-API-Key: afk_REPLACE_WITH_COMPANY_KEY' \
  --data-raw '{
    "id": "5c35578f-f236-441d-9ee7-8e8cded07e67",
    "subject": "Your monthly statement is ready",
    "body": "<!doctype html><html><body><h1>Your statement is ready</h1><p>Hello Jane, your March statement is now available.</p><p><a href=\"https://portal.example.com/statements/123\">View statement</a></p></body></html>",
    "toAddresses": ["jane@example.com"],
    "count": 1
  }'

body is treated as trusted HTML. Never interpolate unsanitized user input into it — escape or sanitize externally supplied values before sending.

Use Authfy's branded email shell

Set templateName to emails/general to place the supplied message inside the company-branded layout. The company name, logo, URL, support details, address, phone number, and primary colour are resolved by Authfy:

curl --request POST 'https://authfy-api.example.com/api/v1/email/new' \
  --header 'Content-Type: application/json' \
  --header 'X-API-Key: afk_REPLACE_WITH_COMPANY_KEY' \
  --data-raw '{
    "id": "4bdb7382-70a6-4e7b-bf83-bf7ddd1b9616",
    "subject": "Your payment was received",
    "body": "<p>Hello Jane,</p><p>We received your payment of <strong>KES 2,500.00</strong>.</p>",
    "templateName": "emails/general",
    "templateVariables": {
      "link": "https://portal.example.com/payments/8172",
      "buttonText": "View receipt",
      "notice": "If you do not recognise this payment, contact support immediately."
    },
    "toAddresses": ["jane@example.com"],
    "count": 1
  }'

Built-in templates:

TemplateIntended use
emails/generalGeneral notices with optional action link and notice block
emails/welcomeWelcome/onboarding messages

An unknown template name falls back to emails/general. The request body is made available to a template as message, unless templateVariables.message is explicitly supplied. Useful variables for emails/general:

VariableDescription
messageRich HTML inserted into the message area
linkAction URL and plain-text fallback URL
buttonTextAction-button label
noticeOptional rich HTML warning or supplementary note

Callers may technically override branding variables through templateVariables, but company integrations should not — company notification settings are the authoritative branding source.

TypeScript example

interface SendEmailInput {
  apiBaseUrl: string;
  apiKey: string;
  recipient: string;
  statementUrl: string;
}

export async function sendStatementEmail(input: SendEmailInput): Promise<void> {
  const response = await fetch(`${input.apiBaseUrl}/api/v1/email/new`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": input.apiKey,
    },
    body: JSON.stringify({
      id: crypto.randomUUID(),
      subject: "Your statement is ready",
      body: "<p>Your latest statement is available.</p>",
      templateName: "emails/general",
      templateVariables: {
        link: input.statementUrl,
        buttonText: "View statement",
      },
      toAddresses: [input.recipient],
      count: 1,
    }),
  });

  if (!response.ok) {
    throw new Error(`Notification request failed with status ${response.status}`);
  }
}

Keep API keys server-side. Do not call Authfy directly from browser JavaScript — doing so exposes the company API key.

Email attachments

Email attachments are not currently supported. The endpoint accepts JSON only, and the SMTP/SES strategies do not create MIME attachment parts. Do not send base64 files or an attachments property — Jackson may ignore the unknown property, but no file will be delivered.

Until attachment support exists, upload the file to authenticated object storage and put a short-lived download URL in templateVariables.link:

{
  "id": "49f3df72-84bf-416d-8298-abd797256cc9",
  "subject": "Your receipt is ready",
  "body": "<p>Your receipt is ready to download.</p>",
  "templateName": "emails/general",
  "templateVariables": {
    "link": "https://files.example.com/signed/receipt.pdf?token=SHORT_LIVED_TOKEN",
    "buttonText": "Download receipt"
  },
  "toAddresses": ["jane@example.com"],
  "count": 1
}

The signed URL should expire quickly and must authorize only the intended file. Do not place private, permanent storage URLs in an email.

Send bulk email

POST /api/v1/email/bulk

The body is an array of the same email objects accepted by /email/new:

curl --request POST 'https://authfy-api.example.com/api/v1/email/bulk' \
  --header 'Content-Type: application/json' \
  --header 'X-API-Key: afk_REPLACE_WITH_COMPANY_KEY' \
  --data-raw '[
    {
      "id": "35ae2a8e-cd94-4384-8d08-0a9be13e8937",
      "subject": "Welcome",
      "body": "<p>Welcome, Jane.</p>",
      "templateName": "emails/welcome",
      "toAddresses": ["jane@example.com"],
      "count": 1
    },
    {
      "id": "507d50f6-a7a4-4429-9d58-68b04881ef94",
      "subject": "Welcome",
      "body": "<p>Welcome, John.</p>",
      "templateName": "emails/welcome",
      "toAddresses": ["john@example.com"],
      "count": 1
    }
  ]'

count repeats each recipient and defaults to 1. It is normally unnecessary — use separate uniquely identified requests rather than intentionally delivering duplicate copies.

Send SMS

POST /api/v1/sms/new

Use international-format phone numbers expected by the configured provider:

curl --request POST 'https://authfy-api.example.com/api/v1/sms/new' \
  --header 'Content-Type: application/json' \
  --header 'X-API-Key: afk_REPLACE_WITH_COMPANY_KEY' \
  --data-raw '{
    "id": "3d69f916-a694-4ea8-8fe7-c8a368807d1a",
    "message": "Your Authfy verification code is 482913. It expires in 10 minutes.",
    "messageType": "TEXT",
    "phoneNumbers": ["254712345678"]
  }'

Authfy sends the message to every number in phoneNumbers. The company setting chooses TIARA or ADVANTA and supplies the sender ID or shortcode and provider credentials.

Legacy SMS field limitations:

For a clickable resource, put a short HTTPS URL directly in message.

What Authfy queues

Callers submit the public DTO; they never publish directly to RabbitMQ. Authfy wraps the request with trusted routing context before queueing it.

Email queue shape:

{
  "appName": "authfy",
  "companyId": "a3328ccf-0b64-4b2a-b735-886c330d57d3",
  "email": {
    "id": "4bdb7382-70a6-4e7b-bf83-bf7ddd1b9616",
    "subject": "Your payment was received",
    "body": "<p>Payment received.</p>",
    "templateName": "emails/general",
    "toAddresses": ["jane@example.com"],
    "count": 1
  }
}

SMS queue shape:

{
  "appName": "authfy",
  "companyId": "a3328ccf-0b64-4b2a-b735-886c330d57d3",
  "sms": {
    "id": "3d69f916-a694-4ea8-8fe7-c8a368807d1a",
    "message": "Your verification code is 482913.",
    "phoneNumbers": ["254712345678"]
  }
}

companyId comes from the authenticated API key, not from caller JSON. Provider credentials are never placed on the queue — the consumer resolves the latest company configuration after receiving the message.

Responses and delivery semantics

A successful intake response:

{
  "status": 200,
  "message": "SUCCESS",
  "data": "Received Successfully"
}

HTTP 200 means the request passed the initial company/channel checks and was published to RabbitMQ — it does not guarantee final delivery. Provider failures are logged and the queue listener dead-letters the message.

Each id is an idempotency key:

StatusMeaning
200Accepted and published to the queue
400Channel disabled, provider incomplete, or request unusable
401Missing or invalid X-API-Key
403Company is not active
409Duplicate email or SMS request ID

Security checklist