Reseller API and sub-accounts

Manage your own customers from your platform: create one sub-account per customer, give it SMS from your balance, create its sending keys and follow its usage at the price you charge it.

Two kinds of keys

  • Your reseller key, with the sub_accounts:read and sub_accounts:write scopes, manages your sub-accounts. It does not send SMS for them.
  • Each sub-account has its own API keys. They send with that sub-account’s balance and Sender IDs, exactly like an ordinary key, through POST /v1/sms/send.
  • A sub-account key can never manage other sub-accounts, and a leaked key exposes only one of your customers.
  • The sub_accounts scopes are never granted by default: create a dedicated reseller key from Settings, profile “Reseller: sub-account management”.

Endpoints

MethodPathScopeRole
GET/v1/sub-accountssub_accounts:readSub-accounts, balances, usage and amount at the resale price over a period (30 days by default, 400 at most).
POST/v1/sub-accountssub_accounts:writeCreate a sub-account; the reference makes the call idempotent.
PATCH/v1/sub-accounts/{id}sub_accounts:writeSuspend, reactivate, change the resale price or the balance alert (one change per call).
POST/v1/sub-accounts/{id}/transferssub_accounts:writeGive (allocation) or take back (reclaim) SMS; Idempotency-Key required.
GET/v1/sub-accounts/{id}/statementsub_accounts:readDay-by-day statement (Africa/Kinshasa time zone).
GET/v1/sub-accounts/{id}/api-keyssub_accounts:readThe sub-account’s active keys (masked).
POST/v1/sub-accounts/{id}/api-keyssub_accounts:writeCreate a sending key for the sub-account; the secret is shown once.
DELETE/v1/sub-accounts/{id}/api-keys/{apiKeyId}sub_accounts:writeRevoke a sub-account key.
GET/v1/sub-accounts/{id}/messagessub_accounts:readThe sub-account’s SMS with their delivery status (DLR), filters by period, status and number.
GET/v1/sub-accounts/{id}/webhookssub_accounts:readDLR webhooks set up for the sub-account.
POST/v1/sub-accounts/{id}/webhookssub_accounts:writeReceive the sub-account’s delivery events; signing secret shown once.
DELETE/v1/sub-accounts/{id}/webhooks/{webhookEndpointId}sub_accounts:writeDisable a sub-account webhook.
GET/v1/sub-accounts/{id}/sender-idssub_accounts:readThe sub-account’s Sender IDs.
POST/v1/sub-accounts/{id}/sender-idssub_accounts:writeRequest a Sender ID for the end customer; checked by Coussema.

Create a sub-account

Call this endpoint from your server, never from a browser. The resale price is used for your statements; it does not change what you pay Coussema.

javascript
const resellerKey = process.env.COUSSEMA_RESELLER_API_KEY ?? '';
const response = await fetch('https://api.coussema.com/v1/sub-accounts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${resellerKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Client A',
    reference: 'client-a', // votre identifiant, qui sert aussi de clé d'idempotence
    resale_price_usd: 0.035,
  }),
});

const { outcome, sub_account_id } = await response.json();
// outcome = 'created' (201) ou 'existing' (200) si la référence existe déjà

Give or take back SMS

A transfer moves SMS between your balance and a sub-account’s, with nothing created or lost. You can only take back SMS you gave it that it has not used.

Reuse the same Idempotency-Key if the response does not reach you: the transfer will never be applied twice.

javascript
const response = await fetch(`https://api.coussema.com/v1/sub-accounts/${subAccountId}/transfers`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${resellerKey}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': `allocation-${invoiceId}`, // obligatoire, stable entre deux essais
  },
  body: JSON.stringify({ kind: 'allocation', units: 10000 }),
});

const transfer = await response.json();
// 201 : transfert appliqué ; 200 : rejeu du même transfert, rien n’est appliqué deux fois
console.log(transfer.reseller_remaining_units, transfer.sub_account_remaining_units);

Create a customer’s sending key

The sub-account sends only with its own approved Sender IDs and cannot send if its balance is empty.

javascript
const response = await fetch(`https://api.coussema.com/v1/sub-accounts/${subAccountId}/api-keys`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${resellerKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'Production', scopes: ['account:read', 'sender_ids:read', 'sms:send'] }),
});

const { apiKey } = await response.json();
// apiKey.plainTextKey n’est affichée qu’une fois : stockez-la côté serveur pour ce client.

Track delivery (DLR)

Create one webhook per sub-account with your reseller key, pointing all your sub-accounts to the same URL: each event contains organization_id, which is the sub-account’s identifier, and client_reference if you gave it when sending. Events are signed like any Coussema webhook (see the Webhooks guide).

For reconciliation or a retry, GET /v1/sub-accounts/{id}/messages lists the SMS with their delivery status, without provider identifiers or raw errors.

bash
curl -X POST "https://api.coussema.com/v1/sub-accounts/$SUB_ACCOUNT_ID/webhooks" \
  -H "Authorization: Bearer $COUSSEMA_RESELLER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://votre-plateforme.example/webhooks/coussema","subscribed_events":["message.sent","message.delivered","message.failed"]}'

Get notified of transfers

Subscribe a webhook of your own reseller account to the sub_account.transfer.created event with POST /v1/webhooks. The key used must have the webhooks:write and sub_accounts:read scopes.

Each recorded transfer sends a single event, even if you replay the same Idempotency-Key. It contains the transfer identifier, the sub-account, the direction (allocation or reclaim), the number of SMS and the reseller’s and sub-account’s balances right after the transfer.

bash
curl -X POST "https://api.coussema.com/v1/webhooks" \
  -H "Authorization: Bearer $COUSSEMA_RESELLER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://votre-plateforme.example/webhooks/coussema","subscribed_events":["sub_account.transfer.created"]}'

Balance alerts

Set a threshold in SMS for each sub-account with PATCH /v1/sub-accounts/{id} and the low_balance_threshold_units field (null to turn it off). The threshold appears in the sub-account list.

When the balance drops below the threshold, you receive a single email and the sub_account.balance.low event (subscribe your reseller webhook to it with a key that has sub_accounts:read). The alert re-arms when the balance goes back above the threshold, for example after a transfer.

Each account can also set its own alert from the SMS quota page and receive the balance.low event.

bash
curl -X PATCH "https://api.coussema.com/v1/sub-accounts/$SUB_ACCOUNT_ID" \
  -H "Authorization: Bearer $COUSSEMA_RESELLER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"low_balance_threshold_units":1000}'

Customers’ Sender IDs

  • Each sub-account sends only with its own approved Sender IDs; the reseller’s are not shared.
  • Request the Sender ID on behalf of the end customer (its brand, its activity, a sample SMS) with POST /v1/sub-accounts/{id}/sender-ids or from the reseller space.
  • Coussema checks every request before approval. The sub-account must have a paid balance: transfer SMS to it before the request.

A sub-account’s statement

Each SMS is valued at the resale price in force when it is billed. SMS used before you set a price appear as unpriced_units.

bash
curl "https://api.coussema.com/v1/sub-accounts/$SUB_ACCOUNT_ID/statement?from=2026-10-01T00:00:00%2B01:00&to=2026-11-01T00:00:00%2B01:00" \
  -H "Authorization: Bearer $COUSSEMA_RESELLER_API_KEY"

Reseller-specific errors

HTTPCodeMeaning
403reseller_requiredThe key does not belong to a reseller account.
403reseller_manager_requiredThe key does not have the sub_accounts:write scope.
403reseller_not_activeYour reseller account is suspended.
404sub_account_not_foundThis sub-account does not exist or does not belong to you.
409insufficient_reseller_balanceYour balance does not cover this transfer.
409insufficient_sub_account_balanceThe sub-account does not have enough unused SMS that you gave it.
409sub_account_status_lockedSuspension decided by Coussema: contact support.
409sub_account_limit_reachedMaximum number of sub-accounts reached.
409idempotency_key_conflictThis Idempotency-Key was already used for another transfer.