Authentication
The platform supports two authentication methods depending on your use case.
| Method | Use Case | Header |
|---|---|---|
| API Key | Application-to-application email sending | x-api-key: kf_xx_... |
| JWT Bearer | Dashboard, admin, tenant management | Authorization: Bearer <token> |
For sending emails from external applications, always use API Key authentication.
Request Body:
{
"email": "admin@yourcompany.com",
"password": "your-password"
}Response: 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "uuid",
"email": "admin@yourcompany.com",
"name": "Admin User",
"role": "TENANT_ADMIN",
"tenantId": "uuid"
}
}Requires Authorization: Bearer <token> header. Returns the authenticated user's profile and tenant information.
Send Email
The primary endpoint for all email sending from external applications. Accepts the email payload, validates it, stores it, and enqueues it for background delivery.
Authentication: x-api-key header (API Key). No JWT required for sending.
| Header | Required | Description |
|---|---|---|
x-api-key | Yes | Your application API key (kf_xx_...) |
Content-Type | Yes | Must be application/json |
| Field | Type | Required | Description |
|---|---|---|---|
| to | string | Required | Recipient email address |
| subject | string | Required | Email subject line (max 998 chars) |
| serviceType | enum | Required | OTP, TRANSACTIONAL, or MARKETING |
| htmlBody | string | Conditional | HTML email body — required if no templateId |
| textBody | string | Optional | Plain text fallback body |
| templateId | string | Conditional | UUID of a saved template — required if no htmlBody |
| templateVariables | object | Optional | Key-value pairs substituted into template {{var}} placeholders |
| from | string | Optional | Override sender email (must be a verified domain) |
| fromName | string | Optional | Override sender display name |
| urgency | enum | Optional | HIGH, NORMAL, LOW — affects queue priority tier |
| referenceType | string | Optional | Your internal reference type (e.g. INVOICE, OTP) |
| referenceId | string | Optional | Your internal reference ID — stored and returned in history |
| metadata | object | Optional | Custom metadata stored with the message record |
| attachments | array | Optional | File attachments — see Section 3 |
Transactional Email
curl -X POST https://your-domain.com/api/email/send \
-H "x-api-key: kf_em_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"subject": "Your Invoice #INV-2024-001",
"serviceType": "TRANSACTIONAL",
"htmlBody": "Invoice Ready
Your invoice for $299.00 is ready.
",
"urgency": "HIGH",
"referenceType": "INVOICE",
"referenceId": "INV-2024-001"
}'OTP Email
curl -X POST https://your-domain.com/api/email/send \
-H "x-api-key: kf_em_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"subject": "Your Login OTP",
"serviceType": "OTP",
"htmlBody": "Your OTP is: 483921
This code expires in 5 minutes.
",
"urgency": "HIGH"
}'Template-based Email
curl -X POST https://your-domain.com/api/email/send \
-H "x-api-key: kf_em_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"subject": "Welcome to Our Platform",
"serviceType": "TRANSACTIONAL",
"templateId": "template-uuid-here",
"templateVariables": {
"name": "John Doe",
"company": "Acme Inc",
"activationLink": "https://app.example.com/activate/abc123"
}
}'Success Response: 202 Accepted
{
"messageId": "8b33601b-9c94-45f1-8613-6b10973b31b0",
"status": "queued"
}| Status | Code | Description |
|---|---|---|
| 400 | GLOBAL_SUPPRESSION | Recipient is on the global block list |
| 400 | TENANT_SUPPRESSION | Recipient unsubscribed or bounced for your tenant |
| 400 | WARMUP_LIMIT | Domain warmup daily limit reached |
| 401 | API_KEY_INVALID | API key missing, revoked, or scope mismatch |
| 403 | TENANT_SUSPENDED | Your tenant account is suspended |
| 403 | SENDING_PAUSED | Email sending is paused by tenant admin |
| 403 | REPUTATION_SUSPENDED | Sending blocked due to low reputation score |
| 429 | RATE_LIMITED | Daily, hourly, or per-minute limit exceeded |
Email Attachments
Attach files to any email by including an attachments array in the send request.
| Field | Type | Required | Description |
|---|---|---|---|
| filename | string | Required | File name shown to recipient (e.g. invoice.pdf) |
| content | string | Required | Base64-encoded file content |
| contentType | string | Optional | MIME type — defaults to application/octet-stream |
{
"attachments": [
{
"filename": "invoice.pdf",
"contentType": "application/pdf",
"content": "JVBERi0xLjQKJcfs..."
}
]
}| Constraint | Limit |
|---|---|
| Max files per email | 10 files |
| Total attachment size (all files combined) | 10 MB decoded |
| Single file size | Counted towards 10 MB total |
| Encoding | Base64 — content must be a valid base64 string |
The 10 MB limit covers the decoded file size. Base64 encoding adds ~33% overhead in transit, but the limit is enforced on decoded bytes. AWS SES imposes this as a hard platform limit.
Node.js: Attach a File from Disk
import fs from 'fs';
const base64Content = fs.readFileSync('./invoice.pdf').toString('base64');
await fetch('https://your-domain.com/api/email/send', {
method: 'POST',
headers: { 'x-api-key': 'kf_em_xxxxxxxxxxxx', 'Content-Type': 'application/json' },
body: JSON.stringify({
to: 'user@example.com',
subject: 'Your Invoice',
serviceType: 'TRANSACTIONAL',
htmlBody: 'See attached invoice.
',
attachments: [{ filename: 'invoice.pdf', contentType: 'application/pdf', content: base64Content }],
}),
});| Provider | Attachment Support | Implementation |
|---|---|---|
| SendGrid | ✅ Full | Native attachments API field |
| SMTP | ✅ Full | Nodemailer multipart/mixed |
| Brevo | ✅ Full | Native attachment API field |
| Mailgun | ✅ Full | FormData multipart (auto-switched) |
| Mailchimp (Mandrill) | ✅ Full | Native attachments API field |
| AWS SES | ✅ Full | Raw MIME via SendRawEmail |
Disaster Recovery: If the server crashes after DB write but before queue enqueue, the message sweeper re-enqueues the email within 2 minutes — including all attachments stored as JSON in the DB row.
Email Status & History
Requires Authorization: Bearer <token>. Returns the current delivery state for a specific message ID.
{
"id": "uuid",
"status": "DELIVERED",
"serviceType": "TRANSACTIONAL",
"provider": "SENDGRID",
"retryCount": 0,
"errorCode": null,
"createdAt": "2026-06-24T08:46:18.929Z",
"deliveredAt": "2026-06-24T08:46:21.102Z"
}| Status | Description |
|---|---|
QUEUED | Accepted and waiting in the delivery queue |
SENDING | Worker picked it up, actively sending to provider |
DELIVERED | Provider confirmed successful delivery |
BOUNCED | Recipient mailbox rejected the message |
FAILED | Permanent failure after all retries exhausted |
DROPPED | Silently dropped (suppression, spam filter, etc.) |
Returns paginated email history for the authenticated tenant.
| Param | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 50 | Results per page (max 100) |
serviceType | enum | — | Filter: OTP, TRANSACTIONAL, MARKETING |
status | enum | — | Filter: QUEUED, DELIVERED, BOUNCED, FAILED |
{
"data": [
{
"id": "uuid",
"serviceType": "TRANSACTIONAL",
"subject": "Your Invoice #INV-2024-001",
"status": "DELIVERED",
"provider": "SENDGRID",
"retryCount": 0,
"createdAt": "2026-06-24T08:46:18.929Z",
"deliveredAt": "2026-06-24T08:46:21.102Z"
}
],
"pagination": { "page": 1, "limit": 50, "total": 142, "totalPages": 3 }
}Templates
Manage reusable email templates with {{variableName}} placeholders. Pass values via templateVariables at send time.
Query Parameters: category (OTP, WORKFLOW, INVOICE, ALERT, MARKETING, NOTIFICATION), status (DRAFT, ACTIVE, ARCHIVED)
{
"templateCode": "welcome_email",
"name": "Welcome Email",
"subject": "Welcome to {{company}}, {{name}}!",
"htmlBody": "Welcome, {{name}}!
Thank you for joining {{company}}.
",
"textBody": "Welcome, {{name}}! Thank you for joining {{company}}.",
"category": "NOTIFICATION",
"variables": {
"name": "Recipient full name",
"company": "Company name"
}
}Update an existing template. Only provided fields are changed.
Archives the template (soft delete). Archived templates cannot be used for new sends but history is preserved.
Template variables use {{variableName}} syntax in subject, htmlBody, and textBody. Pass values via templateVariables in the send request.
Dashboard & Analytics
Returns today's aggregated delivery metrics for the authenticated tenant.
{
"sentToday": 42100,
"deliveredToday": 41890,
"bouncedToday": 148,
"complaintsToday": 4,
"deliveryRate": "99.50",
"bounceRate": "0.351",
"complaintRate": "0.0095",
"openRate": "20.1",
"clickRate": "2.9",
"reputationScore": 92,
"reputationStatus": "HEALTHY",
"warmupActive": false,
"dailyQuotaUsed": 42100,
"dailyQuotaLimit": 100000,
"otpP99Latency": 1.2
}Returns per-provider delivery success rates and volume breakdown over the specified number of days.
Returns a daily activity trend — sent, delivered, bounced counts per day for the last N days.
Returns volume breakdown per API key / application, useful for auditing which service is sending the most.
Tenant Self-Service
{ "reason": "Maintenance window" }Returns current sending state: whether sending is active or paused, the reason, and who paused it.
Route specific emails to specific providers or override the sender address based on recipient domain, service type, or other criteria.
{
"name": "Gmail recipients via Brevo",
"description": "Route all emails to gmail.com via Brevo",
"priority": 10,
"recipientDomain": "gmail.com",
"providerName": "BREVO",
"fromEmail": "notifications@yourdomain.com"
}| Field | Description |
|---|---|
recipientDomain | Match emails going to this domain (e.g. gmail.com) |
serviceType | Match by type: OTP, TRANSACTIONAL, MARKETING |
providerName | Override provider: SENDGRID, BREVO, MAILGUN, SES, SMTP |
fromEmail | Override sender email for matched emails |
fromName | Override sender display name for matched emails |
priority | Lower number = higher priority (evaluated first) |
Paginated audit trail of all actions taken by the tenant — emails sent, settings changed, keys created/revoked, etc.
{
"dataRetentionDays": 45,
"auditRetentionDays": 45,
"autoPurgeEnabled": true
}Manual purge via POST /purge/execute is irreversible. Data older than the retention window is permanently deleted.
Webhooks
The platform accepts delivery event webhooks from providers. Configure these URLs in your provider dashboards to receive real-time delivery events (delivered, bounced, complained, etc.).
| Provider | Webhook URL | Security |
|---|---|---|
| SendGrid | POST /api/webhooks/sendgrid/events | Ed25519 signature validation |
| Brevo | POST /api/webhooks/brevo/events | IP allowlist + payload check |
| Mailgun | POST /api/webhooks/mailgun/events | HMAC-SHA256 signature |
SendGrid webhooks are verified using Ed25519 signature validation. Events with invalid or missing signatures are automatically rejected with 403 Forbidden. No unsigned events are ever processed.
Error Codes
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 202 | Accepted — email queued for delivery |
| 400 | Bad request / validation error |
| 401 | Authentication failed |
| 403 | Forbidden — permissions, suspension, or paused |
| 404 | Resource not found |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
Application Error Codes
| Code | Description | Recommended Action |
|---|---|---|
API_KEY_INVALID | Key missing, revoked, or wrong scope | Check API key status and allowed categories in Dashboard |
TENANT_NOT_FOUND | Tenant does not exist | Contact platform admin |
TENANT_SUSPENDED | Tenant account suspended | Contact platform admin |
SENDING_PAUSED | Sending paused by tenant admin | Resume via dashboard or POST /sending/resume |
REPUTATION_SUSPENDED | Reputation score below 50 | Review bounce and complaint rates |
GLOBAL_SUPPRESSION | Email on platform-wide block list | Do not retry this recipient |
TENANT_SUPPRESSION | Email bounced or unsubscribed for this tenant | Do not retry this recipient |
RATE_LIMITED | Sending limits exceeded | Wait and retry, or request a limit increase |
WARMUP_LIMIT | Domain warmup daily cap reached | Wait until next calendar day |
INTERNAL_ERROR | Platform error | Retry with exponential backoff |
Rate Limits
API Rate Limits
| Endpoint | Limit |
|---|---|
| All API endpoints | 100 requests / minute per IP |
POST /api/email/send | Subject to tenant sending limits (see below) |
Tenant Sending Limits
| Window | Default | Configurable |
|---|---|---|
| Per minute | 60 | Yes |
| Per hour | 1,000 | Yes |
| Per day | 10,000 | Yes |
When rate limited, all responses include:
{
"error": "Daily sending limit reached",
"code": "RATE_LIMITED"
}Limits are configurable per tenant by the platform admin. Enterprise plans support per-minute rates of 5,000+ and daily volumes of 1,000,000+. See Platform Billing Plans for the full tier breakdown.
Integration Examples
const KODEFAST_EMAIL_URL = 'https://your-domain.com/api';
const API_KEY = 'kf_em_xxxxxxxxxxxx';
async function sendTransactionalEmail(
to: string,
subject: string,
htmlBody: string,
referenceId?: string,
) {
const response = await fetch(`${KODEFAST_EMAIL_URL}/email/send`, {
method: 'POST',
headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
to, subject, serviceType: 'TRANSACTIONAL', htmlBody,
referenceType: 'NOTIFICATION', referenceId,
}),
});
if (!response.ok) {
const err = await response.json();
throw new Error(`Email failed: ${err.code} - ${err.error}`);
}
const result = await response.json();
return result.messageId; // use to track delivery status
}
await sendTransactionalEmail(
'user@example.com',
'Order Confirmed',
'Order #12345 Confirmed
Thank you!
',
'ORDER-12345',
);import requests
KODEFAST_EMAIL_URL = "https://your-domain.com/api"
API_KEY = "kf_em_xxxxxxxxxxxx"
def send_email(to, subject, html_body, service_type="TRANSACTIONAL"):
response = requests.post(
f"{KODEFAST_EMAIL_URL}/email/send",
headers={"x-api-key": API_KEY, "Content-Type": "application/json"},
json={"to": to, "subject": subject, "serviceType": service_type, "htmlBody": html_body},
)
response.raise_for_status()
return response.json()["messageId"]
message_id = send_email(
to="user@example.com",
subject="Your Login OTP: 483921",
html_body="Your OTP is: 483921
Expires in 5 minutes.
",
service_type="OTP",
)
print(f"Queued: {message_id}")import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
public class KodefastEmailClient {
private static final String BASE_URL = "https://your-domain.com/api";
private final String apiKey;
private final RestTemplate restTemplate = new RestTemplate();
public KodefastEmailClient(String apiKey) { this.apiKey = apiKey; }
public String sendEmail(String to, String subject, String htmlBody, String serviceType) {
HttpHeaders headers = new HttpHeaders();
headers.set("x-api-key", apiKey);
headers.setContentType(MediaType.APPLICATION_JSON);
Map body = Map.of(
"to", to, "subject", subject, "serviceType", serviceType, "htmlBody", htmlBody
);
ResponseEntity function sendKodefastEmail(string $to, string $subject, string $htmlBody): string {
$data = json_encode([
'to' => $to, 'subject' => $subject,
'serviceType' => 'TRANSACTIONAL', 'htmlBody' => $htmlBody,
]);
$ch = curl_init('https://your-domain.com/api/email/send');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_POSTFIELDS => $data,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['x-api-key: kf_em_xxxxxxxxxxxx', 'Content-Type: application/json'],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 202) throw new Exception("Email send failed: " . $response);
return json_decode($response, true)['messageId'];
}using System.Net.Http;
using System.Text;
using System.Text.Json;
public class KodefastEmailClient {
private readonly HttpClient _client;
private const string BaseUrl = "https://your-domain.com/api";
public KodefastEmailClient(string apiKey) {
_client = new HttpClient();
_client.DefaultRequestHeaders.Add("x-api-key", apiKey);
}
public async Task SendEmailAsync(string to, string subject, string htmlBody, string serviceType = "TRANSACTIONAL") {
var payload = new { to, subject, serviceType, htmlBody };
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var response = await _client.PostAsync($"{BaseUrl}/email/send", content);
response.EnsureSuccessStatusCode();
var result = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync());
return result.GetProperty("messageId").GetString()!;
}
} curl -X POST https://your-domain.com/api/email/send \
-H "x-api-key: kf_em_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"to": "recipient@example.com",
"subject": "Test Email",
"serviceType": "TRANSACTIONAL",
"htmlBody": "Hello!
This is a test email from Kodefast.
"
}'Platform Docs API
Two public endpoints return this platform documentation as JSON. No authentication required.
{
"title": "Kodefast Email Platform — API Documentation",
"slug": "api-documentation",
"content": "# Kodefast Email Platform...",
"lastModified": "2026-07-16T10:00:00.000Z"
}Both endpoints also accept Accept: text/markdown header to receive the raw markdown content directly without the JSON wrapper.
Quick Start Checklist
- Get API Key — Tenant admin creates one via Dashboard → Settings → API Keys
- Set allowed categories — OTP, TRANSACTIONAL, or both (MARKETING requires separate key)
- Send your first email —
POST /api/email/sendwithx-api-keyheader - Track delivery —
GET /api/email/status/:messageIdor check the Dashboard activity page - Monitor health — Dashboard shows delivery rate, reputation score, and quota usage in real time
Security Notes
| Area | Mechanism |
|---|---|
| API Keys at rest | Hashed with SHA-256 — never stored in plaintext |
| Provider credentials | Encrypted with AES-256-GCM at rest |
| Sensitive fields in transit | RSA-OAEP encrypted from frontend to backend |
| Webhook events | Ed25519 signature verification (SendGrid) — unsigned events rejected |
| Audit trail | Every action logged — HIPAA/GDPR compliant |
| Recipient PII | Recipient emails stored as encrypted + hashed — never in plaintext |