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
| Method | Path | Scope | Role |
|---|---|---|---|
| GET | /v1/sub-accounts | sub_accounts:read | Sub-accounts, balances, usage and amount at the resale price over a period (30 days by default, 400 at most). |
| POST | /v1/sub-accounts | sub_accounts:write | Create a sub-account; the reference makes the call idempotent. |
| PATCH | /v1/sub-accounts/{id} | sub_accounts:write | Suspend, reactivate, change the resale price or the balance alert (one change per call). |
| POST | /v1/sub-accounts/{id}/transfers | sub_accounts:write | Give (allocation) or take back (reclaim) SMS; Idempotency-Key required. |
| GET | /v1/sub-accounts/{id}/statement | sub_accounts:read | Day-by-day statement (Africa/Kinshasa time zone). |
| GET | /v1/sub-accounts/{id}/api-keys | sub_accounts:read | The sub-account’s active keys (masked). |
| POST | /v1/sub-accounts/{id}/api-keys | sub_accounts:write | Create a sending key for the sub-account; the secret is shown once. |
| DELETE | /v1/sub-accounts/{id}/api-keys/{apiKeyId} | sub_accounts:write | Revoke a sub-account key. |
| GET | /v1/sub-accounts/{id}/messages | sub_accounts:read | The sub-account’s SMS with their delivery status (DLR), filters by period, status and number. |
| GET | /v1/sub-accounts/{id}/webhooks | sub_accounts:read | DLR webhooks set up for the sub-account. |
| POST | /v1/sub-accounts/{id}/webhooks | sub_accounts:write | Receive the sub-account’s delivery events; signing secret shown once. |
| DELETE | /v1/sub-accounts/{id}/webhooks/{webhookEndpointId} | sub_accounts:write | Disable a sub-account webhook. |
| GET | /v1/sub-accounts/{id}/sender-ids | sub_accounts:read | The sub-account’s Sender IDs. |
| POST | /v1/sub-accounts/{id}/sender-ids | sub_accounts:write | Request 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.
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.
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.
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.
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.
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.
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.
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
| HTTP | Code | Meaning |
|---|---|---|
| 403 | reseller_required | The key does not belong to a reseller account. |
| 403 | reseller_manager_required | The key does not have the sub_accounts:write scope. |
| 403 | reseller_not_active | Your reseller account is suspended. |
| 404 | sub_account_not_found | This sub-account does not exist or does not belong to you. |
| 409 | insufficient_reseller_balance | Your balance does not cover this transfer. |
| 409 | insufficient_sub_account_balance | The sub-account does not have enough unused SMS that you gave it. |
| 409 | sub_account_status_locked | Suspension decided by Coussema: contact support. |
| 409 | sub_account_limit_reached | Maximum number of sub-accounts reached. |
| 409 | idempotency_key_conflict | This Idempotency-Key was already used for another transfer. |