Documentation
Everything you need to send your first message and integrate Wixly into your product.
Getting started
Create an account, then generate a key ID and secret pair from the dashboard. The secret is shown once, at creation — store it securely.
curl -X POST https://wixly.sites.bt/api/v1/messages \
-u key_live_xxx:sk_live_xxx \
-H "Content-Type: application/json" \
-d '{"to":"+97517234567","channel":"sms","body":"Your OTP is 482913"}'Authentication
Every request is authenticated with a key ID and secret pair, sent as HTTP Basic Auth credentials — the key ID identifies the credential, the secret authenticates it. Test credentials (key_test_... / sk_test_...) run against our sandbox — they are never charged to your wallet; live credentials (key_live_... / sk_live_...) send real messages and are billed from your wallet.
Authorization: Basic base64(key_id:secret)To confirm a sandbox send actually worked, open Developers > Messages in your dashboard and filter to Test. A key_test_... send shows up there the same way a real one would — recipient, channel, status, and a provider id — without being billed.
Sending messages
Send an SMS, email, or OTP with a single call to POST /api/v1/messages. The channel is inferred from the destination, or set it explicitly with channel.
SMS is billed per segment. Up to 160 characters (letters, digits, spaces and punctuation all count) is one segment, charged at your SMS rate; a longer message is still sent and billed as 2, 3, … segments (161–320 characters is two). The response's segments field tells you how many. If you'd rather cap message length, turn on an SMS length limit in Settings > Profile — a longer message is then rejected with a 400 instead of being billed.
curl -X POST https://wixly.sites.bt/api/v1/messages \
-u key_live_9f2a1c4b:sk_live_... \
-H "Content-Type: application/json" \
-d '{
"to": "+97517234567",
"channel": "sms",
"body": "Your OTP is 482913"
}'{
"id": "msg_9f2a1c4b",
"status": "queued",
"to": "+97517234567",
"channel": "sms",
"segments": 1,
"created_at": "2026-08-18T09:41:02Z"
}Sending in bulk
To send up to 100 distinct messages in one call — each with its own destination, channel, and content — use POST /api/v1/messages/bulk instead. It accepts the batch and queues it for delivery, returning immediately with a batch id:
curl -X POST https://wixly.sites.bt/api/v1/messages/bulk \
-u key_live_xxx:sk_live_xxx \
-H "Content-Type: application/json" \
-d '{
"messages": [
{ "to": "+97517123456", "body": "Your OTP is 482913" },
{ "to": "jane@example.com", "subject": "Welcome!", "body": "Thanks for signing up." }
]
}'{
"id": "batch_9f8c2a1e",
"status": "queued",
"accepted": 2
}Poll GET /api/v1/messages/bulk/{id} for the outcome, or subscribe to the message.batch.completed webhook instead of polling:
{
"id": "batch_9f8c2a1e",
"status": "completed",
"total": 2,
"sent": 2,
"failed": 0,
"blocked": 0,
"created_at": "2026-08-25T12:00:00.000Z",
"completed_at": "2026-08-25T12:00:04.000Z"
}Rate limits
Two independent limits apply to sending — a per-endpoint rate limit on request velocity, and your plan's message limit on how many messages you can send in a billing cycle. They fail differently, and a large send needs to handle both.
POST /api/v1/messagesPOST /api/v1/messages/bulkGET /api/v1/messages/bulk/{id}Handling a 429
Exceeding a limit returns 429 with a standard Retry-After header (seconds to wait) — back off using that header, not a fixed guess:
HTTP/1.1 429 Too Many Requests
Retry-After: 42
Content-Type: application/json
{
"error": {
"code": "rate_limited",
"message": "You've exceeded your rate limit. Retry in 42s."
}
}Sending at scale — e.g. 1,000 messages
POST /api/v1/messages/bulk accepts up to 100 messages per call, so 1,000 messages means 10 calls. At 10 requests/minute that fits in one minute if paced — split the array into chunks of 100, send one chunk per call, and retry a chunk (the same chunk, not the next one) after the Retry-After delay if you get a 429:
const CHUNK_SIZE = 100; // max per bulk request
async function sendMany(messages) {
for (let i = 0; i < messages.length; i += CHUNK_SIZE) {
const chunk = messages.slice(i, i + CHUNK_SIZE);
await sendChunkWithRetry(chunk);
}
}
async function sendChunkWithRetry(chunk) {
const res = await fetch('https://wixly.sites.bt/api/v1/messages/bulk', {
method: 'POST',
headers: {
Authorization: 'Basic ' + btoa(`${keyId}:${secret}`),
'Content-Type': 'application/json',
},
body: JSON.stringify({ messages: chunk }),
});
if (res.status === 429) {
const retryAfter = Number(res.headers.get('Retry-After')) || 1;
await new Promise((r) => setTimeout(r, retryAfter * 1000));
return sendChunkWithRetry(chunk); // same chunk, after backing off
}
return res.json(); // { id, status: "queued", accepted }
}Separately, check blocked on the batch status (GET /api/v1/messages/bulk/{id}) — messages that fit in the request but not in what was left of your plan's cycle limit are never a 429; they're sent as far as your plan allows and the rest is reported there instead of silently dropped.
Webhooks
Configure a webhook URL in the dashboard to get notified as messages and campaigns change state.
message.sentA message has been accepted and queued for delivery.message.deliveredA message was successfully delivered to the recipient.message.failedA message could not be delivered.campaign.completedAll messages in a campaign have finished sending.message.batch.completedAll messages in a POST /api/v1/messages/bulk request have finished sending.{
"event": "message.delivered",
"message_id": "msg_9f2a1c4b",
"to": "+97517234567",
"channel": "sms",
"timestamp": "2026-08-18T09:41:07Z"
}Errors
Wixly uses standard HTTP status codes. Every error response includes a machine-readable message you can display or log.
400401403404429500