API documentation
One endpoint, one key, one response shape. Errors carry a stable code you can branch on rather than a string that changes when we reword it.
Getting started
Every request goes to https://api.merdotconnect.com and carries your key as a bearer token. Create one on the API keys page. Test keys validate everything and stop short of sending, so they never touch a carrier or your wallet.
Authorization: Bearer mdc_live_...
# A test key looks the same but starts mdc_test_
# and will never send anything for real./v1/messagesSend a message
Send on any live channel. The same shape works everywhere, so switching channel means changing one field.
| Field | Type | Description | |
|---|---|---|---|
| channel | string | required | email, sms, whatsapp, voice, rcs or push. |
| to | string | required | An email address, a phone number in international format, or for push your own identifier for the person. |
| subject | string | optional | Email only. Required for email. |
| text | string | optional | The plain-text body. |
| html | string | optional | Email only. An HTML body. Give text as well where you can. |
| from | string | optional | Email only. An address on a domain you have verified. Leave it out and we send from noreply@ your verified domain, so you do not have to repeat it on every call. You can only send from a domain this account owns. |
| from_name | string | optional | Display name shown to the recipient. |
curl https://api.merdotconnect.com/v1/messages \
-H "Authorization: Bearer mdc_live_..." \
-H "Content-Type: application/json" \
-d '{
"channel": "email",
"to": "[email protected]",
"subject": "Your order has shipped",
"text": "Order MD-4192 is on its way."
}'{
"id": "msg_7RkQ2vT1nZ",
"channel": "email",
"to": "[email protected]",
"status": "submitted",
"price": { "amount": 12, "currency": "INR", "unit": "paise" },
"created_at": "2026-08-01T15:42:08.114Z",
"events": [
{ "status": "queued", "at": "2026-08-01T15:42:08.114Z" },
{ "status": "submitted", "at": "2026-08-01T15:42:09.502Z",
"detail": "Accepted by the mail server." }
]
}- A 201 means a provider accepted the message. That is not the same as delivered, and the status says so.
- A well-formed request we refuse returns 422 with a stable error code, not 400.
- A test key validates everything and returns 202 without sending or charging.
/v1/messages/:idFetch one message
The message and its whole event timeline.
curl https://api.merdotconnect.com/v1/messages/msg_7RkQ2vT1nZ \
-H "Authorization: Bearer mdc_live_..."{
"id": "msg_7RkQ2vT1nZ",
"status": "submitted",
"events": [ ... ]
}- An id belonging to another account returns 404, never another account's data.
/v1/messagesList messages
Most recent first.
| Field | Type | Description | |
|---|---|---|---|
| limit | number | optional | 1 to 100. Defaults to 25. |
| channel | string | optional | Filter to one channel. |
curl "https://api.merdotconnect.com/v1/messages?limit=25&channel=email" \
-H "Authorization: Bearer mdc_live_..."{ "data": [ { "id": "msg_...", "status": "delivered" } ], "has_more": false }/v1/otp/sendSend a verification code
We generate the code, deliver it, and fall through to the next channel when one cannot deliver.
| Field | Type | Description | |
|---|---|---|---|
| to | string | required | Where to send it. |
| channels | string[] | optional | Channels to try, in order. Defaults to whatsapp, sms, email. |
| brand | string | optional | Your name, used in the message. |
| template | string | optional | Your own wording. Use {{code}} where the code should go. |
curl https://api.merdotconnect.com/v1/otp/send \
-H "Authorization: Bearer mdc_live_..." \
-H "Content-Type: application/json" \
-d '{ "to": "+919876543210", "channels": ["whatsapp", "sms"] }'{
"id": "ver_9pQ2xL",
"to": "+919876543210",
"channel": "sms",
"expires_at": "2026-08-01T15:52:08.114Z",
"skipped": [
{ "channel": "whatsapp", "reason": "WhatsApp is not cleared for sending yet" }
]
}- skipped tells you which channels were tried and why each was passed over. A failover is never silent.
- You never see the code. It is stored hashed, so neither can we.
/v1/otp/verifyCheck a verification code
Five wrong attempts burn the code. A correct one is consumed and cannot be replayed.
| Field | Type | Description | |
|---|---|---|---|
| to | string | required | The same destination the code went to. |
| code | string | required | What the person typed. |
curl https://api.merdotconnect.com/v1/otp/verify \
-H "Authorization: Bearer mdc_live_..." \
-H "Content-Type: application/json" \
-d '{ "to": "+919876543210", "code": "418290" }'{ "verified": true }- A wrong code is a 200 with verified:false, not an HTTP error. It is a normal outcome of a successful request.
- attempts_left tells you how many tries remain before the code is burned.
Error codes
Branch on error.code, never on the message. Codes are stable; wording is not.
| Code | HTTP | Meaning |
|---|---|---|
| missing_key | 401 | No API key was presented. |
| invalid_key | 401 | The key is unknown or has been revoked. |
| invalid_json | 400 | The body did not parse as JSON. |
| invalid_channel | 400 | The channel is not one we support. |
| missing_to | 400 | No recipient was given. |
| channel-not-certified | 422 | That channel has not cleared certification yet. |
| channel-not-ready | 422 | No route is connected for that channel. |
| invalid-destination | 422 | The address or number is not usable. |
| suppressed | 422 | The recipient is on your suppression list. Nothing was sent or charged. |
| not_found | 404 | No such record on this account. |
| rate_limited | 429 | Too many requests. Back off until the reset time in the headers. |
| idempotency_conflict | 422 | That idempotency key was already used with a different body. |
| request_in_flight | 409 | An identical request with this key is still running. |
