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.

First request
curl -u key_live_9f2a1c4b:sk_live_... \
  https://api.wixly.dev/api/v1/messages

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_...) never send a real message; live credentials (key_live_... / sk_live_...) do.

Header
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"
  }'
Response
{
  "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:

Request
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." }
    ]
  }'
Response
{
  "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:

GET /api/v1/messages/bulk/{id}
{
  "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/messages
60 requests / minuteper API key — one message per request
POST /api/v1/messages/bulk
10 requests / minuteper API key — up to 100 messages per request, so up to 1,000 messages/minute
GET /api/v1/messages/bulk/{id}
120 requests / minuteper API key — polling a batch’s status

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:

Response
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:

Chunked send with backoff
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.
Example payload
{
  "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.

400
Bad RequestThe request body is missing a required field or is malformed.
401
UnauthorizedThe API key is missing or invalid.
403
ForbiddenThe API key doesn't have permission for this action.
404
Not FoundThe requested resource doesn't exist, or belongs to a different account.
429
Too Many RequestsYou've exceeded your rate limit. Retry after the reset window.
500
Internal Server ErrorSomething went wrong on our end. Retry the request.