Developers

SMS API integration

Send transactional, OTP and promotional SMS from your own application with one HTTP request. Every example below is a real request against this platform.

Base URL

https://api.gotisms.com

Every path below is relative to this address, for example https://api.gotisms.com/api/messages/send. Keep it in your configuration rather than repeated through your code. It is the permanent address and is served over HTTPS only.

Authentication

Every request carries your API key as a bearer token. There is no separate API username or password — the key is the whole credential, so treat it like one.

Authorization: Bearer gs_live_your_key_here

To get a key, create an account — or sign in and open API management. One key is live per account at a time: regenerating immediately stops the old one working.

If you have switched on the IP whitelist, a request from an address that is not on the list is refused with the same 401 as an invalid key. That is deliberate — probing should not reveal which of the two it was.

Send a message

POST/api/messages/send

Sends immediately. Recipients are normalised and de-duplicated server-side, so the same number twice is one message and one charge.

Masking goes through the recipient’s own operator. A message from a masked (branded) sender to a Robi number is delivered through Robi, to a Grameenphone number through Grameenphone, and so on — and only on the networks your masking is assigned for. For a transactional message, a recipient your masking cannot reach is sent as non-masking instead, from your account’s non-masking sender and at the non-masking rate; the response’s notice says how many. If there is no non-masking route either, the whole request is refused with a 409 naming the network, rather than half-sent. Promotional messages are never switched. Non-masking (a numeric sender) can be delivered to any network.

Headers

HeaderValue
AuthorizationRequiredBearer gs_live_…
Content-TypeRequiredapplication/json
Idempotency-KeyStrongly advisedAny string unique to this send
Use an idempotency key. If your request times out you cannot tell whether the send happened. Retrying with the same key returns the original result instead of sending — and charging — a second time. Use something already unique to the event, such as your own order id.

Body

FieldNotes
recipientsRequiredArray of up to 1,000 numbers. 01712345678, 8801712345678 and +8801712345678 all work.
senderIdRequiredAn approved sender ID for your account.
messageRequiredUp to 1000 characters.
categoryRequiredtransactional or otp. Promotional is submitted for approval — see below.
titleOptionalWhat to call this send in your reports.
curl -X POST https://api.gotisms.com/api/messages/send \
  -H "Authorization: Bearer gs_live_your_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-48213-otp" \
  -d '{
    "title": "Order 48213 OTP",
    "recipients": ["01712345678"],
    "senderId": "NON-MASKING",
    "message": "Your OTP is 4821",
    "category": "transactional"
  }'

Response

{
  "batchId": "1df687f7-b686-4624-8435-30922c93dd4b",
  "title": "Order 48213 OTP",
  "accepted": 1,
  "rejected": 0,
  "chargedMinor": "40",
  "reservedMinor": "40",
  "supplier": "SSLWIRELESS",
  "results": [
    {
      "msisdn": "8801712345678",
      "operator": "grameenphone",
      "status": "accepted",
      "supplierStatus": "SUCCESS",
      "chargedMinor": "40",
      "detail": null
    }
  ],
  "notice": null
}
Accepted is not delivered. It means the network took the message. Whether a handset received it arrives later as a delivery report, usually within minutes — poll the reports endpoints below, or watch Campaigns in your dashboard.
All money is in poisha and sent as a string, never a float. "40" is ৳0.40. Parse it as an integer; binary floating point cannot represent money exactly.

Promotional messages

POST/api/messages/campaign-requests

Promotional SMS cannot be sent directly. Each promotional campaign needs an ID that the operator approves for the exact message text, so it is submitted for approval: we create the campaign with the operator and send it once it is approved. Posting category: "promotional" to the send endpoint returns 409.

{
  "title": "Eid offer",
  "recipients": ["01712345678", "01812345678"],
  "senderId": "ACMETRADE",
  "message": "Eid Mubarak! 20% off all orders this week."
}

It is checked like a send — sender, wording, numbers and networks — so a request that could never go is refused straight away. The response has a status of pending.

Nothing is charged when you submit. The campaign is priced and charged when it is sent. The message cannot be changed after submitting, because the operator approves that exact text; cancel it and submit again.

GET /api/messages/campaign-requests lists yours with their status; DELETE /api/messages/campaign-requests/{id} cancels one that is still pending.

Delivery status

A message moves from accepted to delivered, undelivered or rejected when the network reports back. Read it with the reports endpoints below.

Not every route reports delivery. If no report arrives within 24 hours the message is marked unknown — which means exactly that: it was accepted, and the network never said what happened next. Do not treat unknown as failed and resend automatically, or a recipient may get the same message twice.

Schedule a send

POST/api/messages/scheduled

The same body as an immediate send, plus scheduledAt as an ISO instant. At least a minute ahead, at most 90 days.

{
  "title": "Eid campaign",
  "recipients": ["01712345678"],
  "senderId": "NON-MASKING",
  "message": "Eid Mubarak from Acme",
  "category": "promotional",
  "scheduledAt": "2026-09-20T04:00:00Z"
}
Nothing is charged when you schedule. The send is priced and paid for when it runs, so if the balance is short at that moment it fails rather than going out.

GET /api/messages/scheduled lists them; DELETE /api/messages/scheduled/{id} cancels one that has not run.

Balance

GET/api/wallet

{
  "customerId": "46d42cb8-c2f8-419e-83a4-be5e13b3e0ca",
  "availableMinor": "49920",
  "reservedMinor": "0",
  "totalMinor": "49920",
  "creditLimitMinor": null,
  "lowBalanceThresholdMinor": "10000",
  "isLow": false
}

availableMinor is what you can spend right now. reservedMinor is held against sends in flight.

Rates

GET/api/pricing

What you pay per segment, by category and sender kind. A masked (branded) sender is priced separately from a numeric one.

{
  "rates": [
    { "category": "otp", "masked": false, "label": "OTP non-masking", "rateMinor": "50" },
    { "category": "transactional", "masked": false, "label": "Transactional non-masking", "rateMinor": "40" },
    { "category": "promotional", "masked": true, "label": "Promotional masking", "rateMinor": "25" }
  ]
}

To price a message before sending it, post to /api/pricing/preview with message, category, senderId and optionally msisdns. It runs the same calculation the send does, so the two can never disagree.

Sender IDs

GET/api/sender-ids/usable

The senders you may currently send with. Use value in a send.

{
  "values": [
    { "value": "NON-MASKING", "label": "Default Non-Masking", "masked": false },
    { "value": "GOTITEST", "label": "GOTITEST", "masked": true }
  ]
}

Reports

Both take from and to as YYYY-MM-DD.

GET/api/reports/campaigns?from=2026-09-01&to=2026-09-11

One row per send, with delivered, failed and pending counts. Add &category=promotional to narrow it.

GET/api/reports/campaigns/{batchId}/failures

The numbers a send did not reach, each with the operator's own reason.

GET/api/reports/transactional?from=2026-09-01&to=2026-09-11

Every non-promotional message, newest first. Pass limit (up to 200, default 50); when the response has a nextCursor, request again with &cursor= set to it for the next page. A date range covers at most 92 days.

Errors

Errors are JSON and the status code carries the meaning.

StatusMeansWhat to do
400The body failed validationRead fields — it names each problem
401Bad key, revoked key, or a blocked IPCheck the key and your IP whitelist
403Not allowed for this accounte.g. masked sending is not enabled
404No such recordCheck the id
409Refused, not brokene.g. no rate configured, or a duplicate
429Too many requestsWait for X-RateLimit-Reset seconds, then retry
{
  "statusCode": 400,
  "error": "Validation failed",
  "message": "Please correct the highlighted fields",
  "fields": {
    "recipients": "Add at least one recipient",
    "senderId": "Choose a sender ID"
  }
}

Rate limit

300 requests per minute per IP address. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets), so a client can slow down before it is refused.

Sending to one number per request, 300 a minute is 5 messages a second. For a larger send, put up to 1,000 numbers in one request instead of looping — it is also far fewer round trips.
A 409 is the API refusing on purpose — an unpriced route, for instance, is rejected rather than guessed at, because a wrong charge cannot be undone once a message has gone.