BeSends API

Developer documentation

Send email, SMS and WhatsApp
from the system you already run.

One HTTP call, one API key, three channels. Your ERP, CRM, LMS, core banking system or a twelve-line cron script can all use the same endpoint. No SDK to install.

SMS

To one number or ten thousand, scheduled or immediate.

Email

From your own authenticated domain, with attachments.

WhatsApp

Text, image, audio, video or document.

Base URL https://besends.com/api Auth Api-key header Format JSON Rate limit 60 req/min

Quickstart

Three steps. If you have your API key, this takes about a minute.

1. Get your API key

Sign in to your BeSends panel and open Developer → API key. Generate one if you have not already. It is a 36-character string. Treat it like a password: it can send messages and spend your credits.

2. Send a test message

Replace YOUR_API_KEY and the recipient, then run it.

curl — first SMS
curl -X POST https://besends.com/api/sms/send \
  -H "Api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "contact": [
      { "number": "8801712345678", "message": "Your order #4417 is out for delivery." }
    ]
  }'

3. Read the response

Every successful call returns the same envelope. data holds one entry per message, and each carries an id you can look up later.

200 OK
{
  "success": true,
  "message": "Sms dispatch request created successfully",
  "data": [
    {
      "id": 84213,
      "status": "pending",
      "contact_id": 5591,
      "created_at": "2026-09-07 14:02:11"
    }
  ]
}

Accepted is not delivered. A 200 means BeSends has queued the message. The real outcome arrives on the log record a few seconds later — poll GET /api/get/sms/{id} or read it in the panel. Nothing about this API guarantees delivery, and no honest messaging API can.

Authentication

Every request carries your API key. Three ways to supply it, in the order the server checks them:

MethodHowUse it when
Header preferredApi-key: YOUR_API_KEYAlways, unless you cannot set headers.
Query string?api_key=YOUR_API_KEYLegacy systems that can only fetch a URL.
Request body"api_key": "YOUR_API_KEY"Form posts from tools with no header support.

A key in a URL leaks. It lands in browser history, proxy logs and server access logs. The query and body methods exist so old systems can integrate at all — use the header everywhere you can, and never put a key in front-end JavaScript or a mobile app. Calls must come from your server.

What the server checks

  • The key exists and belongs to an account.
  • That account has a running subscription. An expired plan returns 403 even though the key is valid.
403 — no key supplied
{
  "status": "error",
  "message": "API key is required. Provide via header (Api-key) or URL parameter (api_key)",
  "error": "Invalid Api Key"
}
403 — plan expired
{
  "status": "error",
  "error": "Your Subscription Is Expired! Buy A New Plan"
}

Conventions

  • Base URLhttps://besends.com/api. HTTPS only.
  • Content type — send Content-Type: application/json and Accept: application/json. Without the Accept header a validation failure may come back as HTML instead of JSON.
  • Batchingcontact is always an array. One call can carry many recipients, each with its own message, schedule and gateway. Prefer one call with 500 recipients over 500 calls.
  • Phone numbers — international format without + is safest: 8801712345678. Local formats are accepted and normalised, but be explicit.
  • TimestampsY-m-d H:i:s in your account's timezone, e.g. 2026-09-08 09:30:00. Any other format is rejected.
  • Idempotency — there is none. If a call times out, check the log before retrying, or you may send twice.

SMS

POST /api/sms/send

Queue one or many SMS. The main endpoint — use this one.

FieldTypeNotes
contactrequiredarrayAt least one entry.
contact[].numberrequiredstringRecipient. Max 255 characters.
contact[].messagerequiredstringThe text. Unicode is fine; note that non-Latin script costs more segments.
contact[].schedule_atoptionalstringY-m-d H:i:s. Omit to send now.
contact[].gateway_identifieroptionalstringThe uid of a specific gateway on your account. Omit to use your default.
contact[].sms_typeoptionalstringPassed through to the route as metadata.
methodoptionalstringTop level, not per contact. api or android. Overrides your account default.
Send two SMS, one scheduled
curl -X POST https://besends.com/api/sms/send \
  -H "Api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "contact": [
      {
        "number": "8801712345678",
        "message": "Your fee for September is due on 12 Sep. Pay at the accounts desk."
      },
      {
        "number": "8801812345678",
        "message": "Reminder: your appointment is tomorrow at 10:00.",
        "schedule_at": "2026-09-08 09:00:00"
      }
    ]
  }'
GET /api/sms/send

The same send as a plain URL, for systems that cannot POST JSON.

Query parameters: contacts (comma separated), message, and optionally schedule_at, sms_type, gateway_identifier, method.

GET send
curl -G "https://besends.com/api/sms/send" \
  -H "Api-key: YOUR_API_KEY" \
  --data-urlencode "contacts=8801712345678,8801812345678" \
  --data-urlencode "message=Your order has shipped."

Everything is visible in the URL, including the message. Use POST unless the calling system genuinely cannot.

Email

POST /api/email/send

Queue one or many emails, with optional attachments.

FieldTypeNotes
contactrequiredarrayAt least one entry.
contact[].emailrequiredstringValid address, max 255.
contact[].subjectrequiredstringMax 255.
contact[].messagerequiredstringHTML is accepted.
contact[].sender_nameoptionalstringDisplay name on the From line.
contact[].reply_to_emailoptionalstringWhere replies go.
contact[].schedule_atoptionalstringY-m-d H:i:s.
contact[].gateway_identifieroptionalstringMust be an active email gateway uid on your account.
attachments[]optionalfileMultipart only. pdf, doc(x), xls(x), csv, txt, png, jpg, jpeg, gif, zip, rar, svg, webp. File count and size caps are set on your plan.
Send an invoice email
curl -X POST https://besends.com/api/email/send \
  -H "Api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "contact": [
      {
        "email": "finance@acme.com.bd",
        "subject": "Invoice INV-2026-0912",
        "message": "<p>Dear Acme,</p><p>Invoice <b>INV-2026-0912</b> for BDT 48,500 is attached and due on 21 September.</p>",
        "sender_name": "Acme Accounts",
        "reply_to_email": "accounts@yourcompany.com.bd"
      }
    ]
  }'

Set up your sending domain first. Mail from an unauthenticated domain is filtered before anyone reads it. Add the SPF, DKIM and DMARC records shown in your panel under Email → Sending domains, and verify them, before your first real send.

GET /api/email/send

URL form. No attachments on this route.

Query parameters: contacts, subject, message, and optionally sender_name, reply_to_email, schedule_at, gateway_identifier.

WhatsApp

POST /api/whatsapp/send

Text or media. Media is passed by public URL, not uploaded.

FieldTypeNotes
contactrequiredarrayAt least one entry.
contact[].numberrequiredstringWhatsApp number in international format.
contact[].messagerequiredstringBody text, or the caption when sending media.
contact[].mediaoptionalstringOne of image, audio, video, document.
contact[].urloptionalstringPublic URL of the file. Required when media is set.
contact[].filenameoptionalstringName the recipient sees. Useful for documents.
contact[].schedule_atoptionalstringY-m-d H:i:s.
contact[].gateway_identifieroptionalstringWhich of your connected numbers to send from.
Text, and a PDF document
curl -X POST https://besends.com/api/whatsapp/send \
  -H "Api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "contact": [
      {
        "number": "8801712345678",
        "message": "Your parcel is out for delivery. Reply 1 to reschedule."
      },
      {
        "number": "8801812345678",
        "message": "Your statement for August is attached.",
        "media": "document",
        "url": "https://files.yourcompany.com.bd/statements/aug-2026.pdf",
        "filename": "Statement-August-2026.pdf"
      }
    ]
  }'

The URL must be publicly reachable. BeSends fetches the file itself; a link behind a login or a firewall will fail. If the file is private, publish it at a signed, time-limited URL.

GET /api/whatsapp/send

URL form. Text only.

Query parameters: contacts, message, and optionally schedule_at, gateway_identifier.

Delivery logs

Take the id from a send response and ask what happened to it.

GET /api/get/sms/{id}

Also /api/get/email/{id} and /api/get/whatsapp/{id}.

Look up one message
curl "https://besends.com/api/get/sms/84213" \
  -H "Api-key: YOUR_API_KEY" \
  -H "Accept: application/json"
200 OK
{
  "success": true,
  "message": "Successfully fetched Sms from Logs",
  "data": {
    "id": 84213,
    "created_at": "2026-09-07 14:02:11",
    "status": "delivered",
    "message": {
      "message": "Your order #4417 is out for delivery."
    },
    "contact": {
      "first_name": "Rina",
      "last_name": "Haque",
      "email_contact": "8801712345678",
      "meta_data": null
    }
  }
}

Do not poll in a tight loop. You have 60 requests a minute across the whole API. Check a message a few seconds after sending, then back off. For bulk sends, read the report in the panel or export it rather than polling every id.

Status values

The status field on a log record takes one of these:

ValueMeansFinal?
pendingAccepted and waiting for a worker.No
scheduleHeld until its schedule_at time.No
processingHanded to the route, awaiting its answer.No
deliveredThe network confirmed delivery.Yes
failRejected. The reason is on the record in the panel.Yes
cancelCancelled before it went out.Yes

Errors

CodeMeaningWhat to do
403Missing, unknown, or expired-plan API key.Check the key and that the subscription is running. Do not retry automatically.
404Log id does not exist, or is not yours.Check the id came from your own send.
422Validation failed.Read errors — it names the exact field. Fix and resend; retrying unchanged will fail again.
429More than 60 requests in a minute.Back off and retry. Batch recipients into fewer calls.
500Something broke our side.Retry once after a short pause. If it persists, send us the timestamp.
422 — the shape of a validation failure
{
  "success": false,
  "message": "Validation failed",
  "errors": {
    "contact.0.subject": ["The contact.0.subject field is required."],
    "contact.0.message": ["The contact.0.message field is required."]
  }
}

Two envelopes, not one. Authentication failures come from middleware and use {"status":"error","error":"…"}. Everything past authentication uses {"success":false,"message":"…"}. Handle both: check the HTTP status code first, and only then read the body.

Rate limits and credits

  • 60 requests per minute, counted per IP address. Batch recipients into one call rather than making one call per recipient.
  • Credits are per channel. Each message spends one credit from that channel's monthly allowance. A send that fails validation costs nothing; a send that is accepted and then rejected by the network still spends the credit.
  • Running out pauses sending until you top up or the next period begins. Watch your balance in the panel if you send in bursts.
  • Daily caps apply per channel to protect delivery quality. Your plan's caps are shown in the panel.

Recipes

Fire a message when something happens in your system

The most valuable messages are consequences, not campaigns: an invoice went overdue, a parcel shipped, a fee is unpaid. Call the API at the moment the fact becomes true.

PHP — overdue invoice hook
<?php
// Called by your ERP the moment an invoice passes its due date.
function notifyOverdue(array $invoice): void
{
    $body = [
        'contact' => [[
            'number'  => $invoice['phone'],
            'message' => sprintf(
                'Invoice %s for BDT %s was due on %s. Pay at %s',
                $invoice['number'],
                number_format($invoice['amount']),
                $invoice['due_date'],
                'https://yourcompany.com.bd/pay'
            ),
        ]],
    ];

    $ch = curl_init(getenv('CONNECTS_BASE') . '/sms/send');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => [
            'Api-key: ' . getenv('CONNECTS_API_KEY'),
            'Content-Type: application/json',
            'Accept: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode($body),
        CURLOPT_TIMEOUT    => 15,
    ]);

    $response = curl_exec($ch);
    $status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    // Messaging must never break invoicing. Log and carry on.
    if ($status !== 200) {
        error_log("BeSends overdue notice failed for {$invoice['number']}: {$response}");
    }
}

Send to a large list without hitting the rate limit

One call carries many recipients. Chunk your list and pause between calls.

Python — 50,000 recipients, safely
import os, time, requests

BASE  = os.environ["CONNECTS_BASE"]        # https://…/api
KEY   = os.environ["CONNECTS_API_KEY"]
CHUNK = 500                                 # recipients per request
PAUSE = 1.2                                 # seconds between requests

def send_all(recipients, body_for):
    """recipients: iterable of dicts. body_for(r) -> message string."""
    queued, failed = [], []

    for i in range(0, len(recipients), CHUNK):
        batch = recipients[i:i + CHUNK]
        payload = {"contact": [
            {"number": r["phone"], "message": body_for(r)} for r in batch
        ]}

        for attempt in range(3):
            res = requests.post(
                f"{BASE}/sms/send",
                headers={"Api-key": KEY,
                         "Content-Type": "application/json",
                         "Accept": "application/json"},
                json=payload, timeout=60,
            )

            if res.status_code == 429:          # rate limited — wait it out
                time.sleep(20)
                continue
            if res.status_code == 422:          # our payload is wrong; do not retry
                failed.append((i, res.json()))
                break
            if res.ok and res.json().get("success"):
                queued += [row["id"] for row in res.json()["data"]]
                break
            time.sleep(3 * (attempt + 1))       # 5xx — back off and try again
        else:
            failed.append((i, "gave up after 3 attempts"))

        time.sleep(PAUSE)

    return queued, failed

Reach the people an earlier channel missed

Email the detail, then SMS only the people who did not receive it. Because all three channels run off one contact list, this is a filter on your side and two calls.

Node — email first, SMS the gaps
// 1. Email everyone, and keep the log ids alongside the recipient.
const emailed = await post('/email/send', {
  contact: audience.map((p) => ({
    email: p.email,
    subject: 'Your September statement',
    message: renderStatement(p),
  })),
});

const pairs = audience.map((p, i) => ({ person: p, logId: emailed.data[i].id }));

// 2. Give the queue time to resolve, then check each one.
await new Promise((r) => setTimeout(r, 30_000));

const missed = [];
for (const { person, logId } of pairs) {
  const log = await get(`/get/email/${logId}`);
  if (log.data.status === 'fail') missed.push(person);
}

// 3. SMS only those, in one call.
if (missed.length) {
  await post('/sms/send', {
    contact: missed.map((p) => ({
      number: p.phone,
      message: `We could not email your September statement. Collect it at ${p.branch}.`,
    })),
  });
}

Build it with AI

Paste the prompt below into Claude, ChatGPT, Cursor, Copilot or whatever you use. It carries everything the model needs: the real endpoints, the exact payload shapes, both error envelopes, and the failure modes that matter. Fill in the four bracketed lines at the top and it will write an integration that fits your stack.

Never paste your API key into a prompt. The prompt below tells the model to read the key from an environment variable, which is where it belongs anyway.

Copy this whole block into your AI assistant
You are integrating the BeSends messaging API into an existing system.

## Fill these in before you start
- Language / framework: [e.g. Laravel 10, Django 5, Spring Boot 3, .NET 8, Node + Express]
- What triggers a message: [e.g. an invoice passes its due date; a student's fee is unpaid]
- Which channels: [SMS / email / WhatsApp — pick the ones you need]
- Roughly how many messages per send: [e.g. 1, or 40,000 in three days]

## The API

Base URL: https://besends.com/api
Transport: HTTPS, JSON. Send `Content-Type: application/json` and `Accept: application/json`.
Auth: header `Api-key: <key>` on every request. Read the key from the environment
variable CONNECTS_API_KEY. Never hard-code it, never send it to a browser or a mobile app,
never put it in a URL.

Rate limit: 60 requests per minute per IP. `contact` is an array — batch many recipients
into one request rather than looping one request per recipient.

### POST /sms/send
{
  "contact": [
    {
      "number": "8801712345678",              // required, string, max 255
      "message": "text",                       // required, string
      "schedule_at": "2026-09-08 09:00:00",    // optional, exactly Y-m-d H:i:s
      "gateway_identifier": "gateway-uid",     // optional
      "sms_type": "transactional"              // optional
    }
  ],
  "method": "api"                              // optional, top level: "api" or "android"
}

### POST /email/send
{
  "contact": [
    {
      "email": "person@example.com",           // required, valid email, max 255
      "subject": "text",                       // required, max 255
      "message": "<p>HTML allowed</p>",         // required
      "sender_name": "Acme Accounts",          // optional
      "reply_to_email": "reply@example.com",   // optional
      "schedule_at": "2026-09-08 09:00:00",    // optional
      "gateway_identifier": "gateway-uid"      // optional, must be an active email gateway uid
    }
  ]
}
Attachments require a multipart request instead of JSON, with fields named
contact[0][email], contact[0][subject], contact[0][message] and files at attachments[0].
Allowed: pdf, doc, docx, xls, xlsx, csv, txt, png, jpg, jpeg, gif, zip, rar, svg, webp.

### POST /whatsapp/send
{
  "contact": [
    {
      "number": "8801712345678",               // required
      "message": "text or media caption",      // required
      "media": "document",                     // optional: image | audio | video | document
      "url": "https://public.example/file.pdf",// required when media is set; must be publicly fetchable
      "filename": "Statement.pdf",             // optional
      "schedule_at": "2026-09-08 09:00:00",    // optional
      "gateway_identifier": "gateway-uid"      // optional
    }
  ]
}

### GET /get/sms/{id} — also /get/email/{id} and /get/whatsapp/{id}
Returns one log record. `status` is one of:
pending | schedule | processing | delivered | fail | cancel

## Responses

Success (HTTP 200):
{ "success": true, "message": "…", "data": [ { "id": 84213, "status": "pending", … } ] }

Validation failure (HTTP 422):
{ "success": false, "message": "Validation failed",
  "errors": { "contact.0.message": ["The contact.0.message field is required."] } }

Auth failure (HTTP 403) — NOTE THE DIFFERENT SHAPE, it comes from middleware:
{ "status": "error", "error": "Invalid Api Key" }
or
{ "status": "error", "error": "Your Subscription Is Expired! Buy A New Plan" }

## Write the integration with these rules

1. One reusable client class/module. Base URL and API key come from configuration, never
   from literals in the calling code.
2. Handle BOTH response envelopes. Branch on the HTTP status code first, then read the body:
   403 -> auth or expired plan, do NOT retry, surface loudly to an operator
   422 -> our payload is wrong, do NOT retry, log the `errors` object verbatim
   429 -> rate limited, wait and retry with exponential backoff
   5xx -> retry up to 3 times with backoff, then give up and log
   200 -> read `data[].id` and store each id against the record that caused the message
3. Store the returned log id alongside your own record. That id is the only way to ask later
   what happened to the message.
4. Treat a 200 as "accepted", never as "delivered". If the caller needs the real outcome,
   poll GET /get/{channel}/{id} after a delay, with backoff — never in a tight loop.
5. Messaging must never break the calling process. Wrap every call so that a failure is
   logged and the invoice/order/enrolment still completes.
6. Batch: chunk recipients (about 500 per request is sensible) and pause briefly between
   chunks to stay inside 60 requests per minute.
7. Set an explicit HTTP timeout (30s is reasonable, 60s when attaching files).
8. Phone numbers: normalise to international format without a leading +, e.g. 8801712345678.
9. Timestamps for schedule_at must be exactly Y-m-d H:i:s or the request is rejected.
10. Do not invent endpoints, fields or query parameters. Everything available is above; if
    something you need is not listed, say so rather than guessing.

Now write the integration, including the error handling and a short usage example.

Before you go live

  • The API key is in an environment variable or secret store — not in the repository.
  • Calls are made from your server. No key ever reaches a browser or a mobile app.
  • Your sending domain has SPF, DKIM and DMARC set up and verified, if you send email.
  • Every call has a timeout, and a failure is logged rather than thrown at the user.
  • 403 and 422 are not retried; 429 and 5xx are, with backoff.
  • Log ids are stored against your own records.
  • You have sent one real message to your own handset and seen it arrive.
  • Someone gets alerted when sends start failing — silence is the failure mode that costs the most.

Get help

If something here does not match what the server does, tell us — the documentation is wrong until proven otherwise.