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

```bash

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.

```bash

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.

```bash

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`

```json

{

  "success": true,

  "channel": "whatsapp",

  "status": "sent",

  "message_id": "wamid.XXXX",

  "cost": 5.0,

  "to": "+2348012345678"

}

```

---

## Contacts

### Add a single contact

`POST /api/v1/contacts`

```bash

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": "[ada@example.com](mailto:ada@example.com)"

  }'

```

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

```js

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.

```json

{

  "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

```json

{

  "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

```json

{

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

```ruby

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`:

```json

{

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


