Merdot Connect
Developers

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.

Authentication
Authorization: Bearer mdc_live_...

# A test key looks the same but starts mdc_test_
# and will never send anything for real.
POST/v1/messages

Send a message

Send on any live channel. The same shape works everywhere, so switching channel means changing one field.

FieldTypeDescription
channelstringrequiredemail, sms, whatsapp, voice, rcs or push.
tostringrequiredAn email address, a phone number in international format, or for push your own identifier for the person.
subjectstringoptionalEmail only. Required for email.
textstringoptionalThe plain-text body.
htmlstringoptionalEmail only. An HTML body. Give text as well where you can.
fromstringoptionalEmail 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_namestringoptionalDisplay name shown to the recipient.
Request
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."
  }'
Response
{
  "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.
GET/v1/messages/:id

Fetch one message

The message and its whole event timeline.

Request
curl https://api.merdotconnect.com/v1/messages/msg_7RkQ2vT1nZ \
  -H "Authorization: Bearer mdc_live_..."
Response
{
  "id": "msg_7RkQ2vT1nZ",
  "status": "submitted",
  "events": [ ... ]
}
  • An id belonging to another account returns 404, never another account's data.
GET/v1/messages

List messages

Most recent first.

FieldTypeDescription
limitnumberoptional1 to 100. Defaults to 25.
channelstringoptionalFilter to one channel.
Request
curl "https://api.merdotconnect.com/v1/messages?limit=25&channel=email" \
  -H "Authorization: Bearer mdc_live_..."
Response
{ "data": [ { "id": "msg_...", "status": "delivered" } ], "has_more": false }
POST/v1/otp/send

Send a verification code

We generate the code, deliver it, and fall through to the next channel when one cannot deliver.

FieldTypeDescription
tostringrequiredWhere to send it.
channelsstring[]optionalChannels to try, in order. Defaults to whatsapp, sms, email.
brandstringoptionalYour name, used in the message.
templatestringoptionalYour own wording. Use {{code}} where the code should go.
Request
curl https://api.merdotconnect.com/v1/otp/send \
  -H "Authorization: Bearer mdc_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "to": "+919876543210", "channels": ["whatsapp", "sms"] }'
Response
{
  "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.
POST/v1/otp/verify

Check a verification code

Five wrong attempts burn the code. A correct one is consumed and cannot be replayed.

FieldTypeDescription
tostringrequiredThe same destination the code went to.
codestringrequiredWhat the person typed.
Request
curl https://api.merdotconnect.com/v1/otp/verify \
  -H "Authorization: Bearer mdc_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "to": "+919876543210", "code": "418290" }'
Response
{ "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.

CodeHTTPMeaning
missing_key401No API key was presented.
invalid_key401The key is unknown or has been revoked.
invalid_json400The body did not parse as JSON.
invalid_channel400The channel is not one we support.
missing_to400No recipient was given.
channel-not-certified422That channel has not cleared certification yet.
channel-not-ready422No route is connected for that channel.
invalid-destination422The address or number is not usable.
suppressed422The recipient is on your suppression list. Nothing was sent or charged.
not_found404No such record on this account.
rate_limited429Too many requests. Back off until the reset time in the headers.
idempotency_conflict422That idempotency key was already used with a different body.
request_in_flight409An identical request with this key is still running.