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
- Configure the company at
/companies/{companyId}/notificationsin the support console. - Enable the required channel and configure its provider.
- Issue the company's notification API key.
- Send that key as
X-API-Keyon 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:
| Template | Intended use |
|---|---|
emails/general | General notices with optional action link and notice block |
emails/welcome | Welcome/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:
| Variable | Description |
|---|---|
message | Rich HTML inserted into the message area |
link | Action URL and plain-text fallback URL |
buttonText | Action-button label |
notice | Optional 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:
attachmentis accepted for wire compatibility but ignored by both providers — it does not send MMS or media.messageTypeis accepted but not currently forwarded to TIARA or ADVANTA.callbackUrlis accepted by both email and SMS DTOs but callbacks are not dispatched.
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:
- omit it to let Authfy generate one;
- supply a unique UUID when the caller needs traceability;
- reusing an ID returns HTTP
409and does not queue a duplicate; - use a new ID for a new manual send attempt unless deliberately replaying a dead-letter message.
| Status | Meaning |
|---|---|
200 | Accepted and published to the queue |
400 | Channel disabled, provider incomplete, or request unusable |
401 | Missing or invalid X-API-Key |
403 | Company is not active |
409 | Duplicate email or SMS request ID |
Security checklist
- Store
X-API-Keyonly in a server-side secret store. - Use HTTPS outside local development.
- Generate a unique request ID for every logical notification.
- Sanitize untrusted values before inserting them into rich HTML.
- Use short-lived, recipient-authorized links for private documents.
- Never include SMTP, SES, TIARA, or ADVANTA credentials in request bodies.
- Do not publish directly to the RabbitMQ queues from company applications.