Get application credentials
Ask the CommServer administrator to create your application. Save the app slug and API key immediately—the raw key is shown once.
Send email and SMS without building provider integrations into every product. CommServer owns the queue, retries, provider failover, delivery tracking, and opt-outs.
A successful API response means queued, not delivered.
The minimum integration is three server-side steps. New applications start in sandbox mode, so you can complete this flow without sending a real message.
Ask the CommServer administrator to create your application. Save the app slug and API key immediately—the raw key is shown once.
COMMS_URL="http://localhost:3008"
COMMS_APP_ID="your-app-slug"
COMMS_API_KEY="cs_test_..."Keep credentials in server-only code. Never expose the CommServer API key in a browser, mobile bundle, or public repository.
curl -X POST "$COMMS_URL/api/v1/messages" \
-H "X-App-ID: $COMMS_APP_ID" \
-H "X-API-Key: $COMMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "SMS",
"to": "233241234567",
"body": "Hello from CommServer",
"idempotencyKey": "welcome-user-42"
}'CommServer responds immediately. Keep the ID with your business record when you need delivery traceability.
{
"id": "cmr...",
"status": "QUEUED"
}In sandbox mode the worker logs the message and marks it sent. Configure a real provider and disable sandbox mode only after this request succeeds end to end.
For deployment automation, a consuming app can create its own CommServer app and get a one-time API key without opening the admin dashboard.
Configure one bootstrap secret on CommServer. This is not the admin key and it is not an app send key. Give it only to trusted server-side setup jobs.
COMMS_PROVISIONING_KEY="long-random-bootstrap-secret"curl -X POST "$COMMS_URL/api/provision/apps" \
-H "X-Provisioning-Key: $COMMS_PROVISIONING_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "FlowBase",
"slug": "flowbase",
"ratePerMinute": 120,
"scopes": ["SEND", "READ", "PROVIDERS", "TEMPLATES"]
}'{
"app": {
"slug": "flowbase",
"sandboxMode": true
},
"apiKey": "cs_test_...",
"alreadyRegistered": false
}import { CommsProvisioningClient } from "@/lib/comms-client";
const provisioning = new CommsProvisioningClient({
baseUrl: process.env.COMMS_URL!,
provisioningKey: process.env.COMMS_PROVISIONING_KEY!,
});
const result = await provisioning.provisionApp({
name: "FlowBase",
slug: "flowbase",
scopes: ["SEND", "READ", "PROVIDERS", "TEMPLATES"],
});
// Persist result.apiKey into your server secret store when it is not null.If the app already exists, CommServer returns apiKey: null because raw keys are never stored. Send rotateKey: true to intentionally issue a new key, and updateExisting: true to refresh rate limit or webhook settings.
Requests are scoped to one application. Its messages, templates, suppressions, and provider configurations are isolated from every other app.
Send the app ID or slug with its matching API key. Bearer authentication is accepted as an alternative to X-API-Key. Keys are scoped to sending, reading, templates, providers, or suppressions; request only what an integration needs.
X-App-ID: your-app-slug
X-API-Key: cs_live_...
Content-Type: application/json
# Alternative key header:
Authorization: Bearer cs_live_...Put these values in your deployment secret store. Rotate a compromised key from the admin console; rotation is isolated to that application.
Use an inline body for one-off content or a template key for reusable, versioned messages.
/api/v1/messagesQueue a message| Field | Type | Requirement |
|---|---|---|
channel | EMAIL | SMS | Required. |
to | string | Email address or international phone number such as 233…. |
body | string | HTML for email or plain text for SMS. Required without templateKey. |
subject | string | Required for email, inline or supplied by its template. |
templateKey | string | Renders a registered template instead of an inline body. |
variables | object | Values for template placeholders. |
idempotencyKey | string | Recommended. Prevents duplicate sends for the same business event. |
scheduledAt | ISO 8601 date | Defers worker delivery until the requested time. |
from | string | Optional email sender or SMS sender ID override. |
metadata | object | Your correlation data; returned in status webhooks. |
/api/v1/messages/{id}Returns one app-owned message, its template version, and complete event log.
/api/v1/messagesLists recent messages. Filters: status, channel, and limit (maximum 100). Pass the returned nextCursor back as cursor for the next page.
Build idempotencyKey from the event you are notifying about—for examplerequest-42-approved-email. Replaying it returns the original message with deduplicated: true and does not send twice.
Use PATCH /api/v1/messages/{id} with { "action": "CANCEL" }for queued work or { "action": "RETRY" } for failed and cancelled messages. The machine-readable OpenAPI contract is available at /api/openapi.
Copy sdk/comms-client.ts into your application and initialize one server-side client. Raw fetch remains fully supported.
import { CommsClient } from "@/lib/comms-client";
export const comms = new CommsClient({
baseUrl: process.env.COMMS_URL!,
appId: process.env.COMMS_APP_ID!,
apiKey: process.env.COMMS_API_KEY!,
});const result = await comms.sendEmail({
to: user.email,
subject: "Request approved",
body: "<p>Your request REQ-0042 was approved.</p>",
idempotencyKey: request.id + "-approved-email",
metadata: { requestId: request.id },
});
// result: { id: "cmr...", status: "QUEUED" }Catch delivery API errors without breaking the business transaction that triggered the notification. Persist the event first; enqueue the notification second.
Templates are app-scoped and channel-specific. Placeholders use double braces; a send with missing values is rejected before it reaches the queue.
POST /api/v1/templates
{
"key": "request-approved",
"channel": "SMS",
"name": "Request approved",
"body": "Hi {{firstName}}, request {{reference}} was approved."
}POST /api/v1/messages
{
"channel": "SMS",
"to": "233241234567",
"templateKey": "request-approved",
"variables": {
"firstName": "Ama",
"reference": "REQ-0042"
}
}Updating template content increments its version. Use GET /api/v1/templatesto list templates and GET | PATCH | DELETE /api/v1/templates/{id} to manage one.
Each app can store its own encrypted provider credentials. Lower priority numbers are attempted first, creating the failover order within that app.
apiKey, from?host, port?, secure?, username?, password?, from?apiKey, senderId?clientId, clientSecret, senderId?, baseUrl?authKey or username + password, senderId?, baseUrl?apiKey, senderId?, baseUrl?POST /api/v1/provider-configs
{
"providerType": "SMTP",
"name": "Company SMTP",
"priority": 1,
"config": {
"host": "mail.example.com",
"port": 587,
"username": "[email protected]",
"password": "...",
"from": "MyApp <[email protected]>"
}
}PATCH /api/v1/app
{
"sandboxMode": false
}Sandbox mode always uses the logger. Outside sandbox, CommServer uses the app's active provider chain when one exists; otherwise it uses the server-level chain. If neither is configured, it falls back to sandbox delivery.
The API accepts work; the worker and provider decide the final outcome. Poll the message endpoint when the UI needs current status, or use webhooks for event-driven updates.
Retries were exhausted, a permanent provider error occurred, or a DLR failed.
The recipient is on the app's opt-out list. No provider call is made.
SENT means provider accepted. DELIVERED requires a configured provider callback.
Manage opt-outs with POST | GET /api/v1/suppressions. Remove one with DELETE /api/v1/suppressions?channel=SMS&address=233….
Ask the platform admin to set your app's webhook URL and secret. CommServer posts message status updates and includes your original metadata for correlation.
{
"event": "message.status",
"message": {
"id": "cmr...",
"channel": "SMS",
"status": "DELIVERED",
"toAddress": "233...",
"providerName": "arkesel",
"providerMessageId": "...",
"errorMessage": null,
"attempts": 1,
"sentAt": "2026-07-03T10:00:00.000Z",
"metadata": { "requestId": "REQ-0042" }
}
}import { createHmac, timingSafeEqual } from "node:crypto";
const rawBody = await request.text();
const received = request.headers.get("x-comms-signature") ?? "";
const timestamp = request.headers.get("x-comms-timestamp") ?? "";
const expected = createHmac("sha256", process.env.COMMS_WEBHOOK_SECRET!)
.update(timestamp + "." + rawBody)
.digest("hex");
const valid = received.length === expected.length && timingSafeEqual(
Buffer.from(received),
Buffer.from(expected),
);Verify the raw request body before parsing JSON. Return a 2xx response quickly and make your webhook handler idempotent with X-Comms-Event-Id. Reject stale timestamps to prevent replay. CommServer retries failures with exponential backoff.
Every error is JSON in the shape { "error": "...", "details"?: {} }.
| Status | Meaning | Application action |
|---|---|---|
200 | Idempotent replay | Use the returned original message. |
202 | Accepted and queued | Store the ID; do not wait for delivery. |
400 | Invalid body | Fix the request; inspect details. |
401 | Missing or invalid credentials | Check app ID, key, activity, and expiry. |
404 | Resource or template not found | Check app ownership and identifiers. |
409 | Template key conflict | Use the existing key or choose another. |
422 | Missing template variables | Supply every listed placeholder. |
429 | Per-app rate limit exceeded | Retry later with backoff. |
Move from a successful sandbox test to real delivery deliberately.
Credentials are private. CommServer variables exist only in server and deployment environments.
Every business event has an idempotency key. Application retries cannot duplicate notifications.
A provider is configured and tested. Sender IDs, sending domains, and credentials are approved.
Status handling is intentional. Polling or a verified, idempotent webhook is implemented.
Failures do not break core flows. Notification errors are logged and observed separately.
Sandbox is disabled by an admin. Run one controlled real send and confirm provider delivery.