Notifications REST API Integration Guide¶
Last Updated: March 2026
Overview¶
Services that don't participate in the NATS event bus can send notifications via this REST endpoint instead — today that's mainly TruLoad (.NET) and ISP Billing (Python/FastAPI). Every Go service, including the ERP and Ordering services (both fully Go, no longer their original Django/Python incarnations), should prefer the NATS event-driven pattern documented in event-architecture.md and only fall back to this REST endpoint for a genuine one-off, non-event-driven send.
Endpoint¶
POST https://notifications-api.notifications.svc.cluster.local:4000/{tenantId}/notifications/messages
Production (via ingress):
POST https://notificationsapi.codevertexafrica.com/{tenantId}/notifications/messages
Authentication¶
Either:
- Authorization: Bearer <JWT> — user JWT from auth-api
- X-API-Key: <key> — service API key
Request Body¶
{
"channel": "email",
"template": "truload/weight_ticket",
"to": ["recipient@example.com"],
"data": {
"name": "John Doe",
"ticket_number": "WT-2026-001",
"vehicle_reg": "KDA 123A"
},
"metadata": {
"subject": "Your Weight Ticket is Ready"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
channel |
string | Yes | email, sms, push, whatsapp |
template |
string | Yes | Template path (e.g. truload/weight_ticket) — without channel prefix |
to |
string[] | Yes | Recipients (emails, phone numbers, or device tokens) |
data |
object | Yes | Template variables |
metadata |
object | No | subject for emails, provider override, push_title for push |
Response¶
{
"status": "accepted",
"requestId": "uuid-string"
}
Status code: 202 Accepted
Idempotency¶
- Pass
Idempotency-Keyheader (optional) — 24-hour deduplication window - If omitted, a key is derived from payload hash
Rate Limiting¶
- Per-channel rate limits (configurable per tenant)
- Email: limited by
max_emails_per_dayfrom JWT subscription limits - Returns
429 Too Many Requestswhen exceeded
Available Templates by Service¶
TruLoad (.NET/Hangfire)¶
| Template | Variables | Use Case |
|---|---|---|
truload/weight_ticket |
name, ticket_number, vehicle_reg, gross_weight, net_weight, compliance_status, download_link | Weight ticket generated |
truload/compliance_certificate |
name, certificate_number, vehicle_reg, expiry_date, download_link | Certificate available |
truload/special_release |
name, release_number, vehicle_reg, reason, authorized_by, view_link | Special release issued |
ERP / Reports¶
| Template | Variables | Use Case |
|---|---|---|
reports/report_ready |
name, report_name, report_type, generated_at, date_range, download_link, retention_days | Report generated |
reports/activity_report_submitted |
name, project_name, activity_title, submitted_by, submission_date, review_link | Activity report submitted |
reports/activity_report_approved |
name, project_name, activity_title, approved_by, approval_date, action_link | Report approved |
reports/activity_report_rejected |
name, project_name, activity_title, rejected_by, rejection_reason, action_link | Report rejected |
ERP is now a Go service and follows the standard NATS event pattern rather than calling this REST endpoint directly — these templates are kept here for reference in case any legacy call site still exists.
Ordering (Contact Form)¶
| Template | Variables | Use Case |
|---|---|---|
cafe/cafe_contact_form |
name, email, message, submitted_at | Contact form submission |
Sent from ordering-backend (the Go service that superseded the original "cafe" backend). The template ID still carries the old cafe/ prefix — a rename is cosmetic and hasn't been prioritized since the current name doesn't collide with anything.
ISP Billing (ispbilling/*)¶
Triggered via the isp.* events (see event-architecture.md), not direct REST calls. SMS is gated on the tenant's SMS-credit balance and WhatsApp on an active WhatsApp subscription (both centralized in notifications-api); when the tenant lacks credits/subscription that channel is skipped.
| Template | Variables | Use Case |
|---|---|---|
ispbilling/subscription_credentials |
customer_name, username, password, package_name, package_type, expiry_at | Send hotspot/PPPoE login credentials after purchase (sms + whatsapp + email) |
ispbilling/payment_received |
customer_name, amount, currency, package_name, expiry_at | Payment confirmation (sms + email) |
ispbilling/subscription_renewal |
customer_name, package_name, package_type, expiry_at | Renewal confirmation |
ispbilling/isp_subscription_expiring |
customer_name, package_name, expiry_at, days_remaining | Expiry reminder |
ispbilling/low_credit_alert |
tenant_name, balance, threshold | Tenant SMS-credit low-balance alert |
Generic (Any Service)¶
| Template | Variables | Use Case |
|---|---|---|
shared/generic_notification |
name, title, message, action_link | Any notification |
shared/system_alert |
name, alert_title, alert_message, severity, action_link | System alerts |
shared/approval_required |
name, item_type, item_id, requester, action_link | Approval requests |
Integration Examples¶
Python (requests) — e.g. from ISP Billing's FastAPI backend¶
import requests
def send_notification(tenant_id: str, template: str, to: list, data: dict, subject: str, api_key: str):
resp = requests.post(
f"https://notificationsapi.codevertexafrica.com/{tenant_id}/notifications/messages",
json={
"channel": "email",
"template": template,
"to": to,
"data": data,
"metadata": {"subject": subject},
},
headers={
"X-API-Key": api_key,
"Content-Type": "application/json",
},
timeout=10,
)
resp.raise_for_status()
return resp.json()
# Example: send weight ticket notification
send_notification(
tenant_id="uuid-here",
template="truload/weight_ticket",
to=["driver@example.com"],
data={
"name": "John Doe",
"ticket_number": "WT-2026-001",
"vehicle_reg": "KDA 123A",
"gross_weight": "45,000 kg",
"net_weight": "30,000 kg",
"compliance_status": "Compliant",
"download_link": "https://app.example.com/tickets/WT-2026-001",
},
subject="Your Weight Ticket is Ready",
api_key="your-api-key",
)
C# (.NET/Hangfire)¶
public class NotificationClient
{
private readonly HttpClient _http;
private readonly string _baseUrl;
private readonly string _apiKey;
public NotificationClient(string baseUrl, string apiKey)
{
_baseUrl = baseUrl;
_apiKey = apiKey;
_http = new HttpClient();
_http.DefaultRequestHeaders.Add("X-API-Key", apiKey);
}
public async Task SendAsync(string tenantId, string template, string[] to,
Dictionary<string, object> data, string subject)
{
var payload = new
{
channel = "email",
template,
to,
data,
metadata = new { subject }
};
var resp = await _http.PostAsJsonAsync(
$"{_baseUrl}/{tenantId}/notifications/messages", payload);
resp.EnsureSuccessStatusCode();
}
}
// Usage in Hangfire job:
[AutomaticRetry(Attempts = 3)]
public void SendWeightTicketNotification(string tenantId, string email, WeightTicket ticket)
{
var client = new NotificationClient(
"https://notificationsapi.codevertexafrica.com",
Environment.GetEnvironmentVariable("NOTIFICATIONS_API_KEY"));
client.SendAsync(tenantId, "truload/weight_ticket", new[] { email },
new Dictionary<string, object>
{
["name"] = ticket.DriverName,
["ticket_number"] = ticket.TicketNumber,
["vehicle_reg"] = ticket.VehicleReg,
["gross_weight"] = ticket.GrossWeight,
["net_weight"] = ticket.NetWeight,
["compliance_status"] = ticket.ComplianceStatus,
["download_link"] = $"https://app.example.com/tickets/{ticket.TicketNumber}",
},
"Your Weight Ticket is Ready"
).GetAwaiter().GetResult();
}
Environment Variables¶
| Variable | Description | Example |
|---|---|---|
NOTIFICATIONS_API_URL |
Base URL for notifications-api | https://notificationsapi.codevertexafrica.com |
NOTIFICATIONS_API_KEY |
API key for service-to-service auth | sk_notif_... |
Error Handling¶
| Status | Meaning | Action |
|---|---|---|
202 |
Accepted — notification queued | Success |
400 |
Invalid request (missing fields) | Fix payload |
401 |
Authentication failed | Check JWT/API key |
429 |
Rate limit exceeded | Retry after delay |
500 |
Server error | Retry with backoff |