Developer documentation

One API for every application message.

Send email and SMS without building provider integrations into every product. CommServer owns the queue, retries, provider failover, delivery tracking, and opt-outs.

EmailSMSAsync deliveryPer-app isolation
Request lifecycle
01AcceptYour app posts one authenticated request.
02QueuePostgres stores the job and API returns 202.
03DeliverThe worker retries and follows the provider chain.
04ReportPoll status or receive signed webhook updates.
202

A successful API response means queued, not delivered.

01 / Quickstart

Send your first message

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.

1

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.

.env
COMMS_URL="http://localhost:3008"
COMMS_APP_ID="your-app-slug"
COMMS_API_KEY="cs_test_..."
2

Post from your server

Keep credentials in server-only code. Never expose the CommServer API key in a browser, mobile bundle, or public repository.

Terminal
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"
  }'
3

Store the message ID

CommServer responds immediately. Keep the ID with your business record when you need delivery traceability.

202 Accepted
{
  "id": "cmr...",
  "status": "QUEUED"
}
Integration complete

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.

02 / Self-provisioning

Let trusted apps register themselves

For deployment automation, a consuming app can create its own CommServer app and get a one-time API key without opening the admin dashboard.

Provisioning key

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.

CommServer .env
COMMS_PROVISIONING_KEY="long-random-bootstrap-secret"
Create or discover app
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"]
  }'
One-time response
{
  "app": {
    "slug": "flowbase",
    "sandboxMode": true
  },
  "apiKey": "cs_test_...",
  "alreadyRegistered": false
}
Bootstrap script
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.
Idempotent by default

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.

03 / Authentication

Authenticate every request

Requests are scoped to one application. Its messages, templates, suppressions, and provider configurations are isolated from every other app.

Required headers

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.

HTTP headers
X-App-ID: your-app-slug
X-API-Key: cs_live_...
Content-Type: application/json

# Alternative key header:
Authorization: Bearer cs_live_...
Server-to-server only

Put these values in your deployment secret store. Rotate a compromised key from the admin console; rotation is isolated to that application.

04 / API reference

Send and inspect messages

Use an inline body for one-off content or a template key for reusable, versioned messages.

POST/api/v1/messagesQueue a message
FieldTypeRequirement
channelEMAIL | SMSRequired.
tostringEmail address or international phone number such as 233….
bodystringHTML for email or plain text for SMS. Required without templateKey.
subjectstringRequired for email, inline or supplied by its template.
templateKeystringRenders a registered template instead of an inline body.
variablesobjectValues for template placeholders.
idempotencyKeystringRecommended. Prevents duplicate sends for the same business event.
scheduledAtISO 8601 dateDefers worker delivery until the requested time.
fromstringOptional email sender or SMS sender ID override.
metadataobjectYour correlation data; returned in status webhooks.
GET/api/v1/messages/{id}

Returns one app-owned message, its template version, and complete event log.

GET/api/v1/messages

Lists recent messages. Filters: status, channel, and limit (maximum 100). Pass the returned nextCursor back as cursor for the next page.

Make retries safe

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.

05 / TypeScript SDK

Use the dependency-free client

Copy sdk/comms-client.ts into your application and initialize one server-side client. Raw fetch remains fully supported.

src/lib/comms.ts
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!,
});
Send from a server action or route
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.

06 / Templates

Keep reusable content consistent

Templates are app-scoped and channel-specific. Placeholders use double braces; a send with missing values is rejected before it reaches the queue.

Create once
POST /api/v1/templates

{
  "key": "request-approved",
  "channel": "SMS",
  "name": "Request approved",
  "body": "Hi {{firstName}}, request {{reference}} was approved."
}
Use when sending
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.

07 / Providers

Configure delivery per application

Each app can store its own encrypted provider credentials. Lower priority numbers are attempted first, creating the failover order within that app.

EmailResendapiKey, from?
EmailSMTPhost, port?, secure?, username?, password?, from?
SMSArkeselapiKey, senderId?
SMSHubtelclientId, clientSecret, senderId?, baseUrl?
SMSNaloauthKey or username + password, senderId?, baseUrl?
SMSTextCusapiKey, senderId?, baseUrl?
Register a provider
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]>"
  }
}
Switch the app to live delivery
PATCH /api/v1/app

{
  "sandboxMode": false
}
How provider resolution works

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.

08 / Delivery status

Treat delivery as asynchronous

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.

QUEUEDSENDINGSENTDELIVERED
FAILED

Retries were exhausted, a permanent provider error occurred, or a DLR failed.

SUPPRESSED

The recipient is on the app's opt-out list. No provider call is made.

SENT vs. DELIVERED

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….

09 / Webhooks

Receive signed status updates

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.

Webhook payload
{
  "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" }
  }
}
Verify X-Comms-Signature
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.

10 / Errors

Handle predictable API responses

Every error is JSON in the shape { "error": "...", "details"?: {} }.

StatusMeaningApplication action
200Idempotent replayUse the returned original message.
202Accepted and queuedStore the ID; do not wait for delivery.
400Invalid bodyFix the request; inspect details.
401Missing or invalid credentialsCheck app ID, key, activity, and expiry.
404Resource or template not foundCheck app ownership and identifiers.
409Template key conflictUse the existing key or choose another.
422Missing template variablesSupply every listed placeholder.
429Per-app rate limit exceededRetry later with backoff.
11 / Production

Go-live checklist

Move from a successful sandbox test to real delivery deliberately.

  1. 1

    Credentials are private. CommServer variables exist only in server and deployment environments.

  2. 2

    Every business event has an idempotency key. Application retries cannot duplicate notifications.

  3. 3

    A provider is configured and tested. Sender IDs, sending domains, and credentials are approved.

  4. 4

    Status handling is intentional. Polling or a verified, idempotent webhook is implemented.

  5. 5

    Failures do not break core flows. Notification errors are logged and observed separately.

  6. 6

    Sandbox is disabled by an admin. Run one controlled real send and confirm provider delivery.