Convert API
Send WhatsApp & SMS messages, manage contacts, and receive delivery events over a
simple REST API. All requests are authenticated with a Bearer API key and return JSON.
An in-app version of these docs is available at /docs in the dashboard.
-
Base URL:
https://<your-domain>/api/v1 -
Auth:
Authorization: Bearer <api_key> -
Content type:
application/json
Authentication
Create a key under Account → API keys (/api_keys). The full key (rk_live_…) is
shown once at creation — store it securely; only a hashed digest is kept server-side.
Authorization: Bearer rk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Requests without a valid, active key return 401 Unauthorized.
Send a message
POST /api/v1/messages — send a single transactional message (OTP, alert, notification).
| Field | Type | Description |
|--------------|--------|-------------|
| to | string | Recipient phone, international format e.g. +2348012345678. Required. |
| message | string | Text to send. Required. |
| channel | string | whatsapp, sms, or smart (default smart). |
| first_name | string | Optional; stored on the contact, usable as {{first_name}}. |
| last_name | string | Optional. |
| session | string | Optional WhatsApp session id (defaults to your connected number). |
WhatsApp only
Delivers strictly over WhatsApp. Errors if no number is connected or the recipient
isn't reachable on WhatsApp.
curl -X POST https://<your-domain>/api/v1/messages \
-H "Authorization: Bearer rk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "to": "+2348012345678", "message": "Your code is 123456", "channel": "whatsapp" }'
SMS only
Always sends over SMS via the configured provider, using your approved Sender ID.
curl -X POST https://<your-domain>/api/v1/messages \
-H "Authorization: Bearer rk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "to": "+2348012345678", "message": "Your code is 123456", "channel": "sms" }'
Smart routing (default)
Sends over WhatsApp when a number is connected and the recipient is on WhatsApp;
otherwise automatically falls back to SMS.
curl -X POST https://<your-domain>/api/v1/messages \
-H "Authorization: Bearer rk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "+2348012345678",
"message": "Hi {{first_name}}, your order shipped!",
"channel": "smart",
"first_name": "Ada"
}'
Response — 201 Created
{
"success": true,
"channel": "whatsapp",
"status": "sent",
"message_id": "wamid.XXXX",
"cost": 5.0,
"to": "+2348012345678"
}
Contacts
Add a single contact
POST /api/v1/contacts
curl -X POST https://<your-domain>/api/v1/contacts \
-H "Authorization: Bearer rk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+2348012345678",
"first_name": "Ada",
"last_name": "Obi",
"email": "[[email protected]](mailto:[email protected])"
}'
-
Update:
PATCH /api/v1/contacts/:id -
Delete:
DELETE /api/v1/contacts/:id
Bulk upload
For large lists, import a CSV or Excel (.xlsx) file from **Contacts → a list →
Add contacts**. Map your columns to fields (phone, first/last name, email, custom fields);
Convert imports them in the background and checks each number against WhatsApp.
To bulk-load programmatically, loop the single-contact endpoint:
const contacts = [
{ phone_number: "+2348010000001", first_name: "Ada" },
{ phone_number: "+2348010000002", first_name: "Uche" }
];
for (const c of contacts) {
await fetch("https://<your-domain>/api/v1/contacts", {
method: "POST",
headers: { "Authorization": "Bearer rk_live_xxx", "Content-Type": "application/json" },
body: JSON.stringify(c)
});
}
Custom fields
Attach your own attributes via custom_fields. They become merge tags (e.g. {{order_id}})
in messages.
{
"phone_number": "+2348012345678",
"first_name": "Ada",
"custom_fields": { "order_id": "ORD-1042", "tier": "gold" }
}
Campaigns
-
GET /api/v1/campaigns— list recent campaigns -
GET /api/v1/campaigns/:id— one campaign with its contact list
{
"id": 12,
"name": "October promo",
"status": "sent",
"routing_strategy": "smart_fallback",
"sent_count": 940,
"failed_count": 6,
"actual_cost": 4700.0,
"contact_list": { "id": 3, "name": "Customers" }
}
Routing strategies
| Strategy | Behaviour |
|------------------|-----------|
| whatsapp_only | WhatsApp only; contacts not on WhatsApp are skipped. |
| sms_only | SMS to everyone via your Sender ID. |
| whatsapp_first | Try WhatsApp; fall back to SMS for the rest. |
| smart_fallback | WhatsApp for registered contacts, SMS for everyone else (recommended). |
Webhooks
Add an endpoint under Webhooks (/webhook_endpoints) and choose which events to
receive. Convert sends a POST with a JSON body for each event. Leave all events
unchecked to receive every event.
Events
| Event | Fires when… |
|----------------------|-------------|
| message.sent | A single message was accepted by the provider. |
| message.delivered | A message was delivered to the recipient's device. |
| message.failed | A message could not be delivered. |
| campaign.completed | A campaign finished sending to its whole list. |
| contact.created | A new contact was added (UI, import, or API). |
Payload
{
"event": "message.delivered",
"created_at": "2026-06-29T12:00:00Z",
"data": {
"message_id": 8841,
"to": "+2348012345678",
"channel": "whatsapp",
"status": "delivered",
"provider_message_id": "wamid.XXXX",
"cost": 5.0
}
}
Verifying signatures
Each request carries X-Convert-Signature: sha256=<hmac> — an HMAC-SHA256 of the raw
request body using the endpoint's signing secret (shown on the endpoint page).
expected = "sha256=" + OpenSSL::HMAC.hexdigest(
"SHA256", ENV["CONVERT_WEBHOOK_SECRET"], request.raw_post
)
verified = ActiveSupport::SecurityUtils.secure_compare(
expected, request.headers["X-Convert-Signature"].to_s
)
Respond with any 2xx to acknowledge. Non-2xx responses (or timeouts) are retried with
backoff (up to 5 attempts).
Errors
Failed requests return a JSON body with error and a stable error_code:
{
"success": false,
"error": "Insufficient wallet balance to send this message.",
"error_code": "insufficient_balance"
}
| HTTP | error_code | Meaning |
|------|-----------------------------|---------|
| 401 | unauthorized | Missing, invalid, or revoked API key. |
| 402 | insufficient_balance | Wallet balance too low to send. |
| 422 | invalid_channel | channel must be whatsapp, sms, or smart. |
| 422 | invalid_phone | The to number isn't a valid phone number. |
| 422 | missing_message | No message body provided. |
| 422 | whatsapp_not_connected | WhatsApp requested but no number is connected. |
| 422 | recipient_not_on_whatsapp | Recipient not reachable on WhatsApp (use sms/smart). |
| 502 | send_failed / sms_failed| The provider rejected or failed to send. |