Convert Product Documentation

Ruut CSM

Ruut CSM

Last updated on Jul 14, 2026

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