Create, bind and rotate API keys
A key proves which tenant is calling and which channels it may use. It is shown in full once, and a key with no binding cannot send at all.
Read this article →One endpoint accepts asynchronously. A success code means the platform took the message — not that a handset received it.
Endpoint and headers below. All request and response bodies are UTF-8 JSON.
| Item | Value |
|---|---|
| Method and path | POST /open/v1/sms/send |
| Base URL | https://api.whaleal.com/open/v1 |
| Auth | Authorization: Bearer sk_… (or X-API-Key) |
| Content-Type | application/json |
| Field | Required | Why it matters |
|---|---|---|
| to | Yes | E.164 format. Opt-out matching compares digits after stripping + and separators, so inconsistent formatting is what makes a STOP fail to match |
| content | Yes | Not stored on the platform, for compliance. Keep your own copy if you need one |
| from | No | Omit it and the platform picks a sender from the connection |
| messageType | No | MARKETING / INFORMATIONAL / TRANSACTIONAL. Omitted means MARKETING, the strictest setting |
| templateId | No | Only for providers that bill or approve per template — pass a template ID you created in your own provider account |
messageType is the only input to the suppression gate. The platform does not read your message body — so an alert that happens to contain promotional wording will pass as TRANSACTIONAL even though, under rules like the US TCPA, it may still be legally marketing. Declaring the type honestly is the caller's responsibility.
An unrecognised value is rejected with HTTP 400 and code 40000 rather than silently downgraded. That is deliberate: a typo should surface as a broken integration, not as verification codes that quietly stop arriving.
The response repeats the recipient, names the provider that actually carried the message, and returns the fields you will want at reconciliation time.
| Field | Meaning |
|---|---|
| messageId | Platform message ID. Use it for status lookups and for de-duplicating callbacks |
| status | Where the message stands right now — see "What each message status means" |
| providerMessageId | Provider-side ID. Null means the provider did not accept it, which also means it is not billed |
| rawStatus | The provider's own wording before normalisation — diagnostic value only |
| encoding | GSM7 / UCS2 / UTF8 / GBK — the encoding actually used |
| segments | Billed segments for this message |
There is no idempotency key. Sending the same request twice sends two messages and bills for two. Because acceptance is asynchronous, a network timeout means the send may already have happened — treat the timeout as unknown, not as failure.
A key proves which tenant is calling and which channels it may use. It is shown in full once, and a key with no binding cannot send at all.
Read this article →Four states, and one of them does not belong in the same bucket as the others. Reading suppressed as a failure sends you looking in the wrong place.
Read this article →The platform POSTs to a URL you configure, with a token you generate. Two implementation details decide whether you lose events.
Read this article →