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 -u key_live_9f2a1c4b:sk_live_... \
https://api.wixly.dev/api/v1/messagesAuthentication
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_...) never send a real message; live credentials (key_live_... / sk_live_...) do.
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 synthetic test_... provider id — without anything actually going out.
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.
curl -X POST https://api.wixly.dev/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://api.wixly.dev/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://api.wixly.dev/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