# Batler SMS — full documentation Batler SMS is an Israeli SMS/OTP platform with a REST/JSON API (v1). Auth: `Authorization: Bearer bm_live_…` (or `bm_test_…` for test mode: validated, never sent, never charged). Send POSTs require an `Idempotency-Key` header. Phones are accepted as 05XXXXXXXX / +972… and returned in E.164. 1 SMS segment = 1 credit (Hebrew: 70 chars single, 67 per segment). Errors are `{ error: { code, message, details } }`. Delivery and other events arrive by webhooks signed with `X-Batler-Signature: t=,v1=`. Official Node SDK: `@batler/sms`. Base URL: https://smsprovider-web.vercel.app/api/v1 --- # Send your first SMS in 5 minutes This guide takes you from an API key to a delivered message, with curl or the Node SDK. ## 1. Create an API key In the portal: **API → Keys → New key**. Two kinds exist: | Kind | Prefix | Behaviour | | --- | --- | --- | | Live | `bm_live_` | Really sends and charges credits | | Test | `bm_test_` | Full validation, never sends, never charges — see [Test mode](https://smsprovider-web.vercel.app/docs/test-mode) | The full key is shown once. Keep it in an environment variable (`BATLER_API_KEY`), never in client-side code. ## 2. Make sure you have a verified sender Every message is sent from an approved sender name: a shared name from the system list, or a name approved for your account. New names are requested in the portal (Sender names → request a new name): capital Latin letters, up to 10 characters, with a business certificate and an ID document. You get an answer within one business day. [GET /senders](https://smsprovider-web.vercel.app/docs/api/list-senders) returns the full list. ## 3. Send a message ```bash curl -X POST https://smsprovider-web.vercel.app/api/v1/messages \ -H "Authorization: Bearer $BATLER_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"to": "0501234567", "from": "MyBrand", "body": "Hi! Your order has shipped"}' ``` Or with the SDK: ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const res = await sms.messages.send({ to: "0501234567", from: "MyBrand", body: "Hi! Your order has shipped" }); console.log(res.messages[0].id); ``` The response comes back immediately, before the actual send: ```json { "batch_id": "7d1c3c5e-2b1f-4c55-9a51-1f0b8f1b2a10", "accepted": 1, "rejected": [], "credits_reserved": 1, "messages": [{ "id": "0e9b2f0a-6c1d-4f43-8c33-6f3b3f0d9a11", "status": "queued", "to": "+972501234567", "segments": 1, "credits": 1 }] } ``` ## 4. Track delivery - **Webhook (recommended):** register a URL and receive `message.delivered` / `message.failed` in real time — see [Webhooks](https://smsprovider-web.vercel.app/docs/webhooks). - **Polling:** `GET https://smsprovider-web.vercel.app/api/v1/messages/{id}`. Message lifecycle: `queued` → `sent` → `delivered` / `failed` / `expired`. A cancelled scheduled message becomes `cancelled` and its credits are released. ## Important rules - **Idempotency-Key** is required on every send POST. The same key within 24 hours returns the same response without charging twice — safe to retry after a timeout. - Phones are accepted as `05XXXXXXXX`, `5XXXXXXXX`, `+972…` or `972…` and always returned in E.164. - A delivery failure (handset off, number does not exist) does not refund the credit; a failure before sending releases it. --- # Test mode (bm_test_) `bm_test_…` keys let you build and test a full integration without sending real messages and without paying. | | `bm_live_` | `bm_test_` | | --- | --- | --- | | Validation, phone normalisation, segment counting | ✓ | ✓ | | Blacklist, verified sender, idempotency | ✓ | ✓ | | Actual delivery to the recipient | ✓ | ✗ | | Credits charged | ✓ | ✗ (`credits_reserved: 0`) | | Status lifecycle and webhooks | ✓ | ✓ (simulated) | - A test request goes through every step — including full validation against the sending platform — except delivery itself. - Test messages are flagged in the portal and do not count toward the daily limit. - In the portal, the API page shows examples with your test key already filled in. Switch to `bm_live_` only after the integration works end to end. --- # One-time passcodes (OTP) Send and verify one-time codes by SMS without writing OTP logic. We generate the code, send it as a regular SMS from the sender name OTP and check it. The code itself is never stored — only a keyed hash — and never returned to you. ## Send ```bash curl -X POST https://smsprovider-web.vercel.app/api/v1/otp/send \ -H "Authorization: Bearer $BATLER_API_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"to": "0501234567", "app_id": "login", "valid_minutes": 5, "text": "Your MyBrand login code: [code]"}' ``` | Field | Default | Notes | | --- | --- | --- | | `to` | — | Israeli mobile only | | `app_id` | `default` | Separates flows (login, payment approval…). Each `app_id` has its own active code | | `valid_minutes` | 5 | 1–15 | | `max_tries` | 3 | 3–5 wrong attempts, then the code is invalidated | | `code_length` | 6 | 4–8 digits | | `text` | Hebrew default with `[code]` | Must contain `[code]` exactly once | Response: `{ "session_id", "expires_at" }`. Cost: by message segments (usually one credit); verification is free. The message appears in your message history with its delivery report, with the code masked. ## Verify ```ts const { verified, tries_left } = await sms.otp.verify({ to: "0501234567", code: userInput, app_id: "login" }); if (!verified) showError(`Wrong code, ${tries_left} attempts left`); ``` ## Limits and protections - Up to **3 codes per phone per 10 minutes** and **10 per hour** per account. Exceeding returns 429 `otp_rate_limited`. - Do not auto-retry `verify` — every call counts as an attempt. - Webhook events: `otp.verified`, `otp.failed`. - Channel: SMS only. --- # Campaigns and contact lists ## Contact lists A list holds a phone number and up to six dynamic fields `df1`–`df6` (first name, city, appointment date…). ```bash curl -X POST https://smsprovider-web.vercel.app/api/v1/lists \ -H "Authorization: Bearer $BATLER_API_KEY" -H "Content-Type: application/json" \ -d '{"name": "VIP customers", "contacts": [{"phone": "0501234567", "df1": "Dana"}, {"phone": "0527654321", "df1": "Yossi"}]}' ``` - Invalid numbers are returned in `rejected` with a reason; duplicates are merged. - Add/remove: `POST` / `DELETE https://smsprovider-web.vercel.app/api/v1/lists/{id}/contacts` (up to 10,000 per call). - In the portal you can import CSV or Excel with column mapping. ## Sending a campaign ```json POST https://smsprovider-web.vercel.app/api/v1/campaigns { "list_id": "5b0d2a8e-1d7c-4a35-9a7e-3c1a3b5e9f00", "from": "MyBrand", "body": "Hi {{df1}}, end of season sale: 20% off! [shop]", "links": { "shop": "https://example.com/sale" }, "unsubscribe": "link", "schedule_at": "2026-11-05T09:00:00+02:00" } ``` - `{{df1}}`…`{{df6}}` are replaced with each contact's value. - Unsubscribed contacts and blacklisted numbers are filtered automatically; `skip_recent_days` (1–14) skips people messaged recently. - The cost is reserved upfront (segments × recipients) and returned as `credits_reserved`. - Cancel before the scheduled time: `POST https://smsprovider-web.vercel.app/api/v1/campaigns/{id}/cancel` — credits are released. ## Links and click tracking Every link in a message goes through our shortener: write `[link-id]` in the text and pass `links: { "link-id": "https://…" }`. Each recipient gets a unique short link, so you can see who clicked and when (`GET https://smsprovider-web.vercel.app/api/v1/campaigns/{id}/clicks`, webhook `link.clicked`). WhatsApp/iMessage previews and bots are not counted as clicks. The short link (~30 chars) counts toward message length. ## Unsubscribe (required for marketing messages) | `unsubscribe` | What is added | | --- | --- | | `link` | Our short unsubscribe link at the end. Click → one-tap confirmation → the number joins your blacklist | | `reply` | A "reply to unsubscribe" text — a reply removes the recipient automatically | | `none` | Nothing (transactional messages only) | Place the unsubscribe link inside the text with `[unsubscribe]`. A `blacklist.added` event is sent for every opt-out. --- # Webhooks Get real-time updates instead of polling. Each event is POSTed as JSON to your HTTPS URL. ## Setup ```bash curl -X POST https://smsprovider-web.vercel.app/api/v1/webhooks \ -H "Authorization: Bearer $BATLER_API_KEY" -H "Content-Type: application/json" \ -d '{"url": "https://example.com/hooks/batler", "events": ["message.delivered", "message.failed", "inbound.received"]}' ``` The response includes a `secret` (`whsec_…`) — **shown only once**. Store it to verify signatures. ## Events | Event | When | | --- | --- | | `message.sent` | Handed to the mobile network | | `message.delivered` | Delivery receipt from the handset | | `message.failed` | Failure, with `reason` (e.g. `blocked`, `no_coverage`, `kosher_phone`) | | `message.expired` | No delivery receipt in time | | `inbound.received` | A reply to your sender number | | `otp.verified` / `otp.failed` | OTP verification outcome | | `wallet.low_balance` / `wallet.credited` | Low balance / credits purchased | | `blacklist.added` | A recipient opted out | | `link.clicked` | A tracked link was clicked | Shape: ```json { "id": "4f0c7c1e-1c2b-4a0e-9d7b-2e3f4a5b6c7d", "type": "message.delivered", "created_at": "2026-11-02T18:00:05Z", "data": { "message_id": "0e9b2f0a-6c1d-4f43-8c33-6f3b3f0d9a11", "to": "+972501234567", "ref": "order-123", "status": "delivered", "reason": null, "operator": "Partner", "occurred_at": "2026-11-02T18:00:04Z" } } ``` ## Verifying the signature Every request carries `X-Batler-Signature: t=,v1=` where `v1 = HMAC-SHA256(secret, ".")`, plus `X-Batler-Event-Id` for deduplication. Compute the signature over the **raw body** and reject timestamps older than 5 minutes. ### Node.js ```ts import { verifyWebhookSignature } from "@batler/sms"; app.post("/hooks/batler", express.raw({ type: "application/json" }), async (req, res) => { const raw = req.body.toString("utf8"); if (!(await verifyWebhookSignature(process.env.BATLER_WEBHOOK_SECRET!, req.get("X-Batler-Signature"), raw))) return res.sendStatus(401); const event = JSON.parse(raw); // dedupe by req.get("X-Batler-Event-Id"), then handle event.type res.sendStatus(200); }); ``` ### PHP ```php $raw = file_get_contents('php://input'); $header = $_SERVER['HTTP_X_BATLER_SIGNATURE'] ?? ''; parse_str(str_replace(',', '&', $header), $p); $expected = hash_hmac('sha256', $p['t'] . '.' . $raw, getenv('BATLER_WEBHOOK_SECRET')); if (!isset($p['v1']) || !hash_equals($expected, $p['v1']) || abs(time() - (int)$p['t']) > 300) { http_response_code(401); exit; } $event = json_decode($raw, true); http_response_code(200); ``` ### Python ```python import hmac, hashlib, time def verify(secret: str, header: str, raw: bytes) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) t, v1 = parts.get("t", ""), parts.get("v1", "") if not t.isdigit() or abs(time.time() - int(t)) > 300: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1) ``` ## Retries Return 2xx within 10 seconds. Otherwise we retry after 1m, 5m, 30m, 2h, 6h and 24h. After 10 consecutive failures the endpoint is disabled and you get an email. Delivery log and replay: `GET https://smsprovider-web.vercel.app/api/v1/webhooks/{id}/deliveries` and `…/replay`. --- # Connecting Make.com No dedicated module needed — **HTTP → Make a request** is enough. ## Send an SMS from a scenario 1. Add an **HTTP → Make a request** module. 2. URL: `https://smsprovider-web.vercel.app/api/v1/messages` · Method: `POST`. 3. Headers: - `Authorization`: `Bearer bm_live_…` (store it as a keychain/connection) - `Content-Type`: `application/json` - `Idempotency-Key`: `{{uuid}}` (Make's uuid function, or a unique id from the trigger — so a re-run never sends twice) 4. Body type: `Raw` · Content type: `JSON`: ```json { "to": "{{1.phone}}", "from": "MyBrand", "body": "Hi {{1.first_name}}, your appointment is confirmed for {{1.date}}", "ref": "{{1.id}}" } ``` 5. Enable **Parse response** to get `messages[].id`. ## Receiving events in Make 1. Create a scenario with **Webhooks → Custom webhook** and copy its URL. 2. Register it: `POST https://smsprovider-web.vercel.app/api/v1/webhooks` with the events you want (e.g. `inbound.received`). 3. For full security verify `X-Batler-Signature` (see [Webhooks](https://smsprovider-web.vercel.app/docs/webhooks)) — at minimum deduplicate on `X-Batler-Event-Id`. ## OTP in a form Two HTTP modules: `POST /otp/send` when the user enters a phone, and `POST /otp/verify` with the code. Continue only if `verified` is `true`. --- # SDK, MCP and AI agents ## Node/TypeScript SDK ```bash npm install @batler/sms ``` - Full types, automatic `Idempotency-Key` on sends, retries only for requests that are safe to repeat, `BatlerError` with `code`/`status`/`retryAfter`. - `verifyWebhookSignature(secret, header, rawBody)` for webhooks. ## MCP server AI agents (Claude Code, Claude Desktop, Cursor) can send SMS, OTP and check the balance through our MCP server: ```bash claude mcp add batler-sms --env BATLER_API_KEY=bm_test_xxx -- npx -y @batler/sms-mcp ``` Tools: `send_sms`, `send_otp`, `verify_otp`, `get_message`, `list_campaigns`, `get_balance`. Start with a test key. ## Machine-readable docs | File | What | | --- | --- | | [/openapi.json](https://smsprovider-web.vercel.app/openapi.json) | OpenAPI 3.1 — for code generation and collections (Postman/Bruno: Import → URL) | | [/llms.txt](https://smsprovider-web.vercel.app/llms.txt) | Short index for agents | | [/llms-full.txt](https://smsprovider-web.vercel.app/llms-full.txt) | All docs in one markdown file | | `/docs/.md` | Every page as raw markdown | Every page has a **"Copy for AI"** button that copies a ready prompt: the system description, the relevant endpoints and an instruction to connect your app — paste it into Claude Code or Cursor inside your project. --- # Errors and rate limits Every error has the same shape: ```json { "error": { "code": "insufficient_credits", "message": "Not enough credits. Top up your balance and try again.", "details": { "available": 12, "required": 40 } } } ``` Rely on `code` (stable), not on `message` (may change). | HTTP | code | Meaning | What to do | | --- | --- | --- | --- | | 400 | `validation_error` | Invalid body/parameter; `details` lists fields | Fix the request. Unknown fields are rejected | | 400 | `idempotency_key_required` | Missing `Idempotency-Key` on a send | Add a unique UUID per logical send | | 401 | `unauthorized` | Key missing, wrong, expired or revoked | Check the key | | 402 | `insufficient_credits` | Not enough credits | Buy a package in the portal | | 403 | `insufficient_scope` | The key lacks the scope | Create a key with the right scope | | 403 | `sender_not_verified` | The sender name is not approved for your account | Use a name from `GET /senders` or request one in the portal | | 403 | `forbidden` / `org_suspended` | No access / account suspended | Contact support | | 404 | `not_found` | Resource does not exist (or belongs to another account) | — | | 409 | `idempotency_conflict` | Same key with a different request, or the original is still processing | Use a new key for a new request | | 422 | `invalid_destination` | No valid destination; see `details.rejected` | Check the numbers | | 429 | `rate_limited` / `otp_rate_limited` / `daily_limit_exceeded` | Limit exceeded | Wait `retry_after` seconds | | 500/503 | `internal_error` / `service_unavailable` | Temporary problem | Retry with the same `Idempotency-Key` | ## Delivery failure reasons (`reason`) `undeliverable`, `network_failure`, `expired`, `kosher_phone` (a "kosher" number that blocks SMS), `send_window` (sending not allowed at this hour), `blocked` (recipient blocked messages — auto-added to your blacklist), `invalid_content`, `no_coverage`, `rejected`, `cancelled`, `temporary_unavailable`, `internal_error`. Rejection reasons in a send response (`rejected[].reason`): `invalid_phone`, `international_not_allowed`, `blacklisted`, `recently_messaged`, `duplicate`, `body_too_long`, `empty_body`. ## Rate limits | Kind | Limit per key | | --- | --- | | Send (POST) | 60 requests / minute | | Read (GET) | 600 requests / minute | | OTP to the same phone | 3 per 10 minutes, 10 per hour | Every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`; a 429 also has `Retry-After`. For high volume use `/messages/batch` (up to 2,500 messages per request) instead of one request per message. New accounts are limited to 500 messages per day until business details are verified. --- # Phone formats and segments ## Phones Send in any of these formats; responses are always E.164: | Input | Stored as | | --- | --- | | `0501234567` | `+972501234567` | | `501234567` | `+972501234567` | | `972501234567` | `+972501234567` | | `+972 50-123-4567` | `+972501234567` | Spaces, dashes and parentheses are fine. International destinations (`+44…`) are sent only with `allow_international: true` and only if your account is enabled for it; they are priced per destination (default 5 credits per segment) and charged from a separate international balance. ## Segments and billing Long messages are sent as several segments, and each segment = one credit: | Encoding | When | Single message | Per segment (multi-part) | | --- | --- | --- | --- | | GSM-7 | English, digits and basic symbols only | 160 chars | 153 | | Unicode (UCS-2) | Any Hebrew character, emoji or special character | 70 chars | 67 | - **A single Hebrew character makes the whole message Unicode.** - `^ { } \ [ ~ ] | €` count as two characters in GSM. - An emoji counts as two characters. - Short links (~30 chars) and the unsubscribe link count toward length. - Maximum length: 1,005 characters. Example: a 120-character Hebrew message = 2 segments = 2 credits. ## Sender IDs - **Phone number** (mobile or landline): verified with a code we send to it. Lets recipients reply (`inbound.received`). - **Alphanumeric name**: 2–11 characters, Latin letters/digits/space, must include a letter. Recipients cannot reply. --- # Changelog Breaking changes ship only in a new version (`/v2`). New fields and events may be added to v1 — make sure your code ignores unknown fields. ## 1.0.0 — November 2026 - API v1 launch: messages, batches, list campaigns, OTP, contact lists, blacklist, inbound messages, sender IDs, balance and usage, webhooks, price quotes. - Short links with click tracking and unsubscribe links. - `@batler/sms` SDK and `@batler/sms-mcp` MCP server. - `bm_test_` test keys. --- # Send an SMS ```http POST https://smsprovider-web.vercel.app/api/v1/messages ``` Send the same text to one or up to 1,000 recipients. Credits are reserved immediately (1 segment = 1 credit) and messages are queued. Embedded links (`[link-id]` in the text + `links`) are shortened with click tracking. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `sms:send` > Send an `Idempotency-Key` header (a unique string, e.g. a UUID). Repeating the same key within 24 hours returns the same response without charging again. ## Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `to` | string \| string[] | yes | len 5–20, items 1–1000 | | `from` | string | no | len 2–20 | | `body` | string | yes | len 1–1005 | | `schedule_at` | string (date-time) | no | | | `campaign_name` | string | no | len 1–50 | | `unsubscribe_link` | boolean | no | | | `unsubscribe_reply` | boolean | no | | | `links` | map | no | | | `skip_recent_days` | integer | no | 1–14 | | `allow_international` | boolean | no | | | `android_hash` | string | no | pattern `^[A-Za-z0-9+/]{11}$` | | `ref` | string | no | len 1–100 | ## Response `200` — `SendResult` ```json { "batch_id": "7d1c3c5e-2b1f-4c55-9a51-1f0b8f1b2a10", "accepted": 1, "rejected": [], "credits_reserved": 1, "messages": [ { "id": "0e9b2f0a-6c1d-4f43-8c33-6f3b3f0d9a11", "status": "queued", "to": "+972501234567", "segments": 1, "credits": 1, "ref": "order-123", "scheduled_at": null, "created_at": "2026-11-02T18:00:00Z" } ] } ``` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 400 | `idempotency_key_required` | | 409 | `idempotency_conflict` | | 402 | `insufficient_credits` | | 403 | `sender_not_verified` | | 422 | `invalid_destination` | | 429 | `daily_limit_exceeded` | | 403 | `org_suspended` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X POST "https://smsprovider-web.vercel.app/api/v1/messages" \ -H "Authorization: Bearer $BATLER_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"to":["0501234567"],"from":"MyBrand","body":"שלום! ההזמנה שלך יצאה למשלוח: [track]","links":{"track":"https://example.com/orders/123"},"ref":"order-123"}' ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const body = { "to": [ "0501234567" ], "from": "MyBrand", "body": "שלום! ההזמנה שלך יצאה למשלוח: [track]", "links": { "track": "https://example.com/orders/123" }, "ref": "order-123" }; const result = await sms.messages.send(body); ``` ### PHP ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", "Idempotency-Key: " . bin2hex(random_bytes(16)), ], CURLOPT_POSTFIELDS => json_encode(["to" => ["0501234567"], "from" => "MyBrand", "body" => "שלום! ההזמנה שלך יצאה למשלוח: [track]", "links" => ["track" => "https://example.com/orders/123"], "ref" => "order-123"], JSON_UNESCAPED_UNICODE), ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "POST", "https://smsprovider-web.vercel.app/api/v1/messages", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}", "Idempotency-Key": str(uuid.uuid4())}, json={"to": ["0501234567"], "from": "MyBrand", "body": "שלום! ההזמנה שלך יצאה למשלוח: [track]", "links": {"track": "https://example.com/orders/123"}, "ref": "order-123"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Post, "https://smsprovider-web.vercel.app/api/v1/messages"); req.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString()); req.Content = new StringContent("{\"to\":[\"0501234567\"],\"from\":\"MyBrand\",\"body\":\"שלום! ההזמנה שלך יצאה למשלוח: [track]\",\"links\":{\"track\":\"https://example.com/orders/123\"},\"ref\":\"order-123\"}", System.Text.Encoding.UTF8, "application/json"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Send a batch of different messages ```http POST https://smsprovider-web.vercel.app/api/v1/messages/batch ``` Up to 2,500 different messages in one request (a different text per recipient). **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `sms:send` > Send an `Idempotency-Key` header (a unique string, e.g. a UUID). Repeating the same key within 24 hours returns the same response without charging again. ## Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `messages` | object[] | yes | items 1–2500 | | `from` | string | no | len 2–20 | | `schedule_at` | string (date-time) | no | | | `campaign_name` | string | no | len 1–50 | | `allow_international` | boolean | no | | ## Response `200` — `SendResult` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 400 | `idempotency_key_required` | | 409 | `idempotency_conflict` | | 402 | `insufficient_credits` | | 403 | `sender_not_verified` | | 422 | `invalid_destination` | | 429 | `daily_limit_exceeded` | | 403 | `org_suspended` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X POST "https://smsprovider-web.vercel.app/api/v1/messages/batch" \ -H "Authorization: Bearer $BATLER_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"from":"MyBrand","messages":[{"to":"0501234567","body":"דנה, התור שלך מחר ב-10:00","ref":"appt-1"},{"to":"0527654321","body":"יוסי, התור שלך מחר ב-11:30","ref":"appt-2"}]}' ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const body = { "from": "MyBrand", "messages": [ { "to": "0501234567", "body": "דנה, התור שלך מחר ב-10:00", "ref": "appt-1" }, { "to": "0527654321", "body": "יוסי, התור שלך מחר ב-11:30", "ref": "appt-2" } ] }; const result = await sms.messages.batch(body); ``` ### PHP ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", "Idempotency-Key: " . bin2hex(random_bytes(16)), ], CURLOPT_POSTFIELDS => json_encode(["from" => "MyBrand", "messages" => [["to" => "0501234567", "body" => "דנה, התור שלך מחר ב-10:00", "ref" => "appt-1"], ["to" => "0527654321", "body" => "יוסי, התור שלך מחר ב-11:30", "ref" => "appt-2"]]], JSON_UNESCAPED_UNICODE), ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "POST", "https://smsprovider-web.vercel.app/api/v1/messages/batch", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}", "Idempotency-Key": str(uuid.uuid4())}, json={"from": "MyBrand", "messages": [{"to": "0501234567", "body": "דנה, התור שלך מחר ב-10:00", "ref": "appt-1"}, {"to": "0527654321", "body": "יוסי, התור שלך מחר ב-11:30", "ref": "appt-2"}]}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Post, "https://smsprovider-web.vercel.app/api/v1/messages/batch"); req.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString()); req.Content = new StringContent("{\"from\":\"MyBrand\",\"messages\":[{\"to\":\"0501234567\",\"body\":\"דנה, התור שלך מחר ב-10:00\",\"ref\":\"appt-1\"},{\"to\":\"0527654321\",\"body\":\"יוסי, התור שלך מחר ב-11:30\",\"ref\":\"appt-2\"}]}", System.Text.Encoding.UTF8, "application/json"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # List messages ```http GET https://smsprovider-web.vercel.app/api/v1/messages ``` Filter by status, date range, campaign or phone. Cursor pagination. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `reports` ## Query parameters | Field | Type | Required | Notes | | --- | --- | --- | --- | | `limit` | integer | no | 1–1000, default `100` | | `cursor` | string | no | len 0–200 | | `status` | `queued` \| `sent` \| `delivered` \| `failed` \| `expired` \| `cancelled` | no | | | `from` | string (date-time) | no | | | `to` | string (date-time) | no | | | `campaign_id` | string (uuid) | no | | | `phone` | string | no | len 5–20 | ## Response `200` — `MessagePage` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/messages?status=delivered&limit=100" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.messages.list({ status: "delivered" }); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/messages?status=delivered&limit=100", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/messages?status=delivered&limit=100"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Get a message ```http GET https://smsprovider-web.vercel.app/api/v1/messages/{id} ``` Delivery status, segments, credits, mobile operator and link clicks. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `reports` ## Path parameters | Field | Type | | --- | --- | | `id` | string (uuid) | ## Response `200` — `Message` ## Errors | Status | Code | | --- | --- | | 404 | `not_found` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/messages/{id}" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.messages.get(id); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/messages/{id}", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/messages/{id}"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Send a campaign to a list ```http POST https://smsprovider-web.vercel.app/api/v1/campaigns ``` Send to a contact list with dynamic fields `{{df1}}`…`{{df6}}`. Unsubscribed and blacklisted contacts are filtered out automatically. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `sms:send` > Send an `Idempotency-Key` header (a unique string, e.g. a UUID). Repeating the same key within 24 hours returns the same response without charging again. ## Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `list_id` | string (uuid) | yes | | | `name` | string | no | len 1–100 | | `from` | string | no | len 2–20 | | `body` | string | yes | len 1–1005 | | `schedule_at` | string (date-time) | no | | | `unsubscribe` | `none` \| `link` \| `reply` | no | default `"none"` | | `skip_recent_days` | integer | no | 1–14 | | `links` | map | no | | ## Response `200` — `SendResult` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 400 | `idempotency_key_required` | | 409 | `idempotency_conflict` | | 402 | `insufficient_credits` | | 403 | `sender_not_verified` | | 422 | `invalid_destination` | | 429 | `daily_limit_exceeded` | | 403 | `org_suspended` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | | 404 | `not_found` | ## Examples ### curl ```bash curl -X POST "https://smsprovider-web.vercel.app/api/v1/campaigns" \ -H "Authorization: Bearer $BATLER_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"list_id":"5b0d2a8e-1d7c-4a35-9a7e-3c1a3b5e9f00","from":"MyBrand","body":"היי {{df1}}, מבצע סוף עונה: 20% הנחה! [shop]","links":{"shop":"https://example.com/sale"},"unsubscribe":"link","schedule_at":"2026-11-05T09:00:00+02:00"}' ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const body = { "list_id": "5b0d2a8e-1d7c-4a35-9a7e-3c1a3b5e9f00", "from": "MyBrand", "body": "היי {{df1}}, מבצע סוף עונה: 20% הנחה! [shop]", "links": { "shop": "https://example.com/sale" }, "unsubscribe": "link", "schedule_at": "2026-11-05T09:00:00+02:00" }; const result = await sms.campaigns.create(body); ``` ### PHP ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", "Idempotency-Key: " . bin2hex(random_bytes(16)), ], CURLOPT_POSTFIELDS => json_encode(["list_id" => "5b0d2a8e-1d7c-4a35-9a7e-3c1a3b5e9f00", "from" => "MyBrand", "body" => "היי {{df1}}, מבצע סוף עונה: 20% הנחה! [shop]", "links" => ["shop" => "https://example.com/sale"], "unsubscribe" => "link", "schedule_at" => "2026-11-05T09:00:00+02:00"], JSON_UNESCAPED_UNICODE), ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "POST", "https://smsprovider-web.vercel.app/api/v1/campaigns", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}", "Idempotency-Key": str(uuid.uuid4())}, json={"list_id": "5b0d2a8e-1d7c-4a35-9a7e-3c1a3b5e9f00", "from": "MyBrand", "body": "היי {{df1}}, מבצע סוף עונה: 20% הנחה! [shop]", "links": {"shop": "https://example.com/sale"}, "unsubscribe": "link", "schedule_at": "2026-11-05T09:00:00+02:00"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Post, "https://smsprovider-web.vercel.app/api/v1/campaigns"); req.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString()); req.Content = new StringContent("{\"list_id\":\"5b0d2a8e-1d7c-4a35-9a7e-3c1a3b5e9f00\",\"from\":\"MyBrand\",\"body\":\"היי {{df1}}, מבצע סוף עונה: 20% הנחה! [shop]\",\"links\":{\"shop\":\"https://example.com/sale\"},\"unsubscribe\":\"link\",\"schedule_at\":\"2026-11-05T09:00:00+02:00\"}", System.Text.Encoding.UTF8, "application/json"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # List campaigns ```http GET https://smsprovider-web.vercel.app/api/v1/campaigns ``` All sends and campaigns, newest first. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `reports` ## Query parameters | Field | Type | Required | Notes | | --- | --- | --- | --- | | `limit` | integer | no | 1–1000, default `100` | | `cursor` | string | no | len 0–200 | | `status` | `draft` \| `scheduled` \| `sending` \| `sent` \| `cancelled` \| `failed` | no | | ## Response `200` — `CampaignPage` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/campaigns" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.campaigns.list(); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/campaigns", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/campaigns"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Get a campaign ```http GET https://smsprovider-web.vercel.app/api/v1/campaigns/{id} ``` Sent, delivered, failed and cost. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `reports` ## Path parameters | Field | Type | | --- | --- | | `id` | string (uuid) | ## Response `200` — `Campaign` ## Errors | Status | Code | | --- | --- | | 404 | `not_found` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/campaigns/{id}" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.campaigns.get(id); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/campaigns/{id}", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/campaigns/{id}"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Cancel a scheduled campaign ```http POST https://smsprovider-web.vercel.app/api/v1/campaigns/{id}/cancel ``` Cancels messages not yet sent and releases their credits. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `sms:send` ## Path parameters | Field | Type | | --- | --- | | `id` | string (uuid) | ## Response `200` — `CancelResult` ## Errors | Status | Code | | --- | --- | | 404 | `not_found` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X POST "https://smsprovider-web.vercel.app/api/v1/campaigns/{id}/cancel" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.campaigns.cancel(id); ``` ### PHP ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "POST", "https://smsprovider-web.vercel.app/api/v1/campaigns/{id}/cancel", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Post, "https://smsprovider-web.vercel.app/api/v1/campaigns/{id}/cancel"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Campaign clicks ```http GET https://smsprovider-web.vercel.app/api/v1/campaigns/{id}/clicks ``` Unique and total clicks, CTR, hourly breakdown and clickers (bots and link previews excluded). **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `reports` ## Path parameters | Field | Type | | --- | --- | | `id` | string (uuid) | ## Response `200` — `CampaignClicks` ## Errors | Status | Code | | --- | --- | | 404 | `not_found` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/campaigns/{id}/clicks" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.campaigns.clicks(id); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/campaigns/{id}/clicks", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/campaigns/{id}/clicks"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Send a verification code ```http POST https://smsprovider-web.vercel.app/api/v1/otp/send ``` Sends a one-time code (4–8 digits, default 6) as a regular SMS from the sender name OTP to an Israeli mobile. The code is generated and checked by us, never stored (only a keyed hash) and never returned to you. Limit: 3 codes per phone per 10 minutes, 10 per hour. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `otp` > Send an `Idempotency-Key` header (a unique string, e.g. a UUID). Repeating the same key within 24 hours returns the same response without charging again. ## Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `to` | string | yes | len 5–20 | | `app_id` | string | no | pattern `^[A-Za-z0-9_-]{1,32}$` | | `max_tries` | integer | no | 3–5 | | `valid_minutes` | integer | no | 1–15 | | `code_length` | integer | no | 4–8 | | `text` | string | no | len 6–300 | ## Response `200` — `OtpSendResult` ```json { "session_id": "c3a1b2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "expires_at": "2026-11-02T18:05:00Z" } ``` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 429 | `otp_rate_limited` | | 402 | `insufficient_credits` | | 422 | `invalid_destination` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X POST "https://smsprovider-web.vercel.app/api/v1/otp/send" \ -H "Authorization: Bearer $BATLER_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"to":"0501234567","app_id":"login","valid_minutes":5,"text":"קוד הכניסה שלך ל-MyBrand: [code]"}' ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const body = { "to": "0501234567", "app_id": "login", "valid_minutes": 5, "text": "קוד הכניסה שלך ל-MyBrand: [code]" }; const result = await sms.otp.send(body); ``` ### PHP ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", "Idempotency-Key: " . bin2hex(random_bytes(16)), ], CURLOPT_POSTFIELDS => json_encode(["to" => "0501234567", "app_id" => "login", "valid_minutes" => 5, "text" => "קוד הכניסה שלך ל-MyBrand: [code]"], JSON_UNESCAPED_UNICODE), ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "POST", "https://smsprovider-web.vercel.app/api/v1/otp/send", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}", "Idempotency-Key": str(uuid.uuid4())}, json={"to": "0501234567", "app_id": "login", "valid_minutes": 5, "text": "קוד הכניסה שלך ל-MyBrand: [code]"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Post, "https://smsprovider-web.vercel.app/api/v1/otp/send"); req.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString()); req.Content = new StringContent("{\"to\":\"0501234567\",\"app_id\":\"login\",\"valid_minutes\":5,\"text\":\"קוד הכניסה שלך ל-MyBrand: [code]\"}", System.Text.Encoding.UTF8, "application/json"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Verify a code ```http POST https://smsprovider-web.vercel.app/api/v1/otp/verify ``` Checks the code the user typed. Each call counts as an attempt; after `max_tries` wrong attempts the code is invalidated. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `otp` ## Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `to` | string | yes | len 5–20 | | `code` | string | yes | pattern `^\d{4,10}$` | | `app_id` | string | no | pattern `^[A-Za-z0-9_-]{1,32}$` | ## Response `200` — `OtpVerifyResult` ```json { "verified": true, "tries_left": 2, "session_id": "c3a1b2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } ``` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 404 | `not_found` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X POST "https://smsprovider-web.vercel.app/api/v1/otp/verify" \ -H "Authorization: Bearer $BATLER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"to":"0501234567","code":"482913","app_id":"login"}' ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const body = { "to": "0501234567", "code": "482913", "app_id": "login" }; const result = await sms.otp.verify(body); ``` ### PHP ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode(["to" => "0501234567", "code" => "482913", "app_id" => "login"], JSON_UNESCAPED_UNICODE), ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "POST", "https://smsprovider-web.vercel.app/api/v1/otp/verify", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, json={"to": "0501234567", "code": "482913", "app_id": "login"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Post, "https://smsprovider-web.vercel.app/api/v1/otp/verify"); req.Content = new StringContent("{\"to\":\"0501234567\",\"code\":\"482913\",\"app_id\":\"login\"}", System.Text.Encoding.UTF8, "application/json"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # List contact lists ```http GET https://smsprovider-web.vercel.app/api/v1/lists ``` All contact lists with active subscriber counts. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `lists` ## Query parameters | Field | Type | Required | Notes | | --- | --- | --- | --- | | `limit` | integer | no | 1–1000, default `100` | | `cursor` | string | no | len 0–200 | ## Response `200` — `ContactListPage` ## Errors | Status | Code | | --- | --- | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/lists" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.lists.list(); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/lists", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/lists"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Create a list ```http POST https://smsprovider-web.vercel.app/api/v1/lists ``` Create a list with up to 10,000 contacts (phone + df1–df6). Invalid numbers are returned in `rejected`. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `lists` ## Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `name` | string | yes | len 1–100 | | `contacts` | object[] | no | items 0–10000, default `[]` | ## Response `201` — `ContactList` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X POST "https://smsprovider-web.vercel.app/api/v1/lists" \ -H "Authorization: Bearer $BATLER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"לקוחות VIP","contacts":[{"phone":"0501234567","df1":"דנה"},{"phone":"0527654321","df1":"יוסי"}]}' ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const body = { "name": "לקוחות VIP", "contacts": [ { "phone": "0501234567", "df1": "דנה" }, { "phone": "0527654321", "df1": "יוסי" } ] }; const result = await sms.lists.create(body); ``` ### PHP ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode(["name" => "לקוחות VIP", "contacts" => [["phone" => "0501234567", "df1" => "דנה"], ["phone" => "0527654321", "df1" => "יוסי"]]], JSON_UNESCAPED_UNICODE), ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "POST", "https://smsprovider-web.vercel.app/api/v1/lists", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, json={"name": "לקוחות VIP", "contacts": [{"phone": "0501234567", "df1": "דנה"}, {"phone": "0527654321", "df1": "יוסי"}]}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Post, "https://smsprovider-web.vercel.app/api/v1/lists"); req.Content = new StringContent("{\"name\":\"לקוחות VIP\",\"contacts\":[{\"phone\":\"0501234567\",\"df1\":\"דנה\"},{\"phone\":\"0527654321\",\"df1\":\"יוסי\"}]}", System.Text.Encoding.UTF8, "application/json"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Delete a list ```http DELETE https://smsprovider-web.vercel.app/api/v1/lists/{id} ``` Deletes the list and its contacts. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `lists` ## Path parameters | Field | Type | | --- | --- | | `id` | string (uuid) | ## Response `200` — `Ok` ## Errors | Status | Code | | --- | --- | | 404 | `not_found` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X DELETE "https://smsprovider-web.vercel.app/api/v1/lists/{id}" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.lists.delete(id); ``` ### PHP ```php "DELETE", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "DELETE", "https://smsprovider-web.vercel.app/api/v1/lists/{id}", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Delete, "https://smsprovider-web.vercel.app/api/v1/lists/{id}"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # List contacts ```http GET https://smsprovider-web.vercel.app/api/v1/lists/{id}/contacts ``` Contacts in a list, including unsubscribe time. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `lists` ## Path parameters | Field | Type | | --- | --- | | `id` | string (uuid) | ## Query parameters | Field | Type | Required | Notes | | --- | --- | --- | --- | | `limit` | integer | no | 1–1000, default `100` | | `cursor` | string | no | len 0–200 | ## Response `200` — `ContactPage` ## Errors | Status | Code | | --- | --- | | 404 | `not_found` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/lists/{id}/contacts" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.lists.contacts(id); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/lists/{id}/contacts", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/lists/{id}/contacts"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Add contacts ```http POST https://smsprovider-web.vercel.app/api/v1/lists/{id}/contacts ``` Add up to 10,000 contacts; duplicates are updated. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `lists` ## Path parameters | Field | Type | | --- | --- | | `id` | string (uuid) | ## Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `contacts` | object[] | yes | items 1–10000 | ## Response `200` — `MutationResult` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 404 | `not_found` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X POST "https://smsprovider-web.vercel.app/api/v1/lists/{id}/contacts" \ -H "Authorization: Bearer $BATLER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contacts":[]}' ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const body = { "contacts": [] }; const result = await sms.lists.addContacts(id, body.contacts); ``` ### PHP ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode(["contacts" => []], JSON_UNESCAPED_UNICODE), ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "POST", "https://smsprovider-web.vercel.app/api/v1/lists/{id}/contacts", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, json={"contacts": []}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Post, "https://smsprovider-web.vercel.app/api/v1/lists/{id}/contacts"); req.Content = new StringContent("{\"contacts\":[]}", System.Text.Encoding.UTF8, "application/json"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Remove contacts ```http DELETE https://smsprovider-web.vercel.app/api/v1/lists/{id}/contacts ``` Remove numbers from the list (JSON body with `phones`). **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `lists` ## Path parameters | Field | Type | | --- | --- | | `id` | string (uuid) | ## Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `phones` | string[] | yes | items 1–10000 | ## Response `200` — `MutationResult` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 404 | `not_found` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X DELETE "https://smsprovider-web.vercel.app/api/v1/lists/{id}/contacts" \ -H "Authorization: Bearer $BATLER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"phones":["0501234567"]}' ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const body = { "phones": [ "0501234567" ] }; const result = await sms.lists.removeContacts(id, body.phones); ``` ### PHP ```php "DELETE", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode(["phones" => ["0501234567"]], JSON_UNESCAPED_UNICODE), ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "DELETE", "https://smsprovider-web.vercel.app/api/v1/lists/{id}/contacts", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, json={"phones": ["0501234567"]}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Delete, "https://smsprovider-web.vercel.app/api/v1/lists/{id}/contacts"); req.Content = new StringContent("{\"phones\":[\"0501234567\"]}", System.Text.Encoding.UTF8, "application/json"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # List blacklist ```http GET https://smsprovider-web.vercel.app/api/v1/blacklist ``` Numbers that will never receive messages from your account, and why. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `lists` ## Query parameters | Field | Type | Required | Notes | | --- | --- | --- | --- | | `limit` | integer | no | 1–1000, default `100` | | `cursor` | string | no | len 0–200 | ## Response `200` — `BlacklistPage` ## Errors | Status | Code | | --- | --- | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/blacklist" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.blacklist.list(); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/blacklist", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/blacklist"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Add to blacklist ```http POST https://smsprovider-web.vercel.app/api/v1/blacklist ``` Blocks numbers from receiving messages from your account. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `lists` ## Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `phones` | string[] | yes | items 1–10000 | | `reason` | string | no | len 0–200 | ## Response `200` — `MutationResult` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X POST "https://smsprovider-web.vercel.app/api/v1/blacklist" \ -H "Authorization: Bearer $BATLER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"phones":["0501234567"]}' ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const body = { "phones": [ "0501234567" ] }; const result = await sms.blacklist.add(body.phones); ``` ### PHP ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode(["phones" => ["0501234567"]], JSON_UNESCAPED_UNICODE), ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "POST", "https://smsprovider-web.vercel.app/api/v1/blacklist", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, json={"phones": ["0501234567"]}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Post, "https://smsprovider-web.vercel.app/api/v1/blacklist"); req.Content = new StringContent("{\"phones\":[\"0501234567\"]}", System.Text.Encoding.UTF8, "application/json"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Remove from blacklist ```http DELETE https://smsprovider-web.vercel.app/api/v1/blacklist ``` Removes numbers from the blacklist. Re-adding someone who opted out requires their renewed consent by law. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `lists` ## Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `phones` | string[] | yes | items 1–10000 | ## Response `200` — `MutationResult` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X DELETE "https://smsprovider-web.vercel.app/api/v1/blacklist" \ -H "Authorization: Bearer $BATLER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"phones":["0501234567"]}' ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const body = { "phones": [ "0501234567" ] }; const result = await sms.blacklist.remove(body.phones); ``` ### PHP ```php "DELETE", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode(["phones" => ["0501234567"]], JSON_UNESCAPED_UNICODE), ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "DELETE", "https://smsprovider-web.vercel.app/api/v1/blacklist", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, json={"phones": ["0501234567"]}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Delete, "https://smsprovider-web.vercel.app/api/v1/blacklist"); req.Content = new StringContent("{\"phones\":[\"0501234567\"]}", System.Text.Encoding.UTF8, "application/json"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # List inbound messages ```http GET https://smsprovider-web.vercel.app/api/v1/inbound ``` Replies received on your sender numbers. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `reports` ## Query parameters | Field | Type | Required | Notes | | --- | --- | --- | --- | | `limit` | integer | no | 1–1000, default `100` | | `cursor` | string | no | len 0–200 | | `from` | string (date-time) | no | | | `to` | string (date-time) | no | | ## Response `200` — `InboundPage` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/inbound" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.inbound.list(); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/inbound", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/inbound"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # List sender names ```http GET https://smsprovider-web.vercel.app/api/v1/senders ``` Sender names you can send from: shared names (shared: true) and names approved for your account. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `sms:send` ## Response `200` — `SenderPage` ## Errors | Status | Code | | --- | --- | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/senders" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.senders.list(); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/senders", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/senders"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Look up a sender name ```http POST https://smsprovider-web.vercel.app/api/v1/senders ``` Returns a sender name available to the account (shared or approved for it). New names are requested in the portal (Sender names → request a new name) with a business certificate and an ID document; answered within one business day. Sending from phone numbers is not available. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `sms:send` ## Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `value` | string | yes | len 2–20 | | `is_default` | boolean | no | | | `note` | string | no | len 0–500 | ## Response `201` — `Sender` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X POST "https://smsprovider-web.vercel.app/api/v1/senders" \ -H "Authorization: Bearer $BATLER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"value":"…"}' ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const body = { "value": "…" }; const result = await sms.senders.create(body); ``` ### PHP ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode(["value" => "…"], JSON_UNESCAPED_UNICODE), ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "POST", "https://smsprovider-web.vercel.app/api/v1/senders", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, json={"value": "…"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Post, "https://smsprovider-web.vercel.app/api/v1/senders"); req.Content = new StringContent("{\"value\":\"…\"}", System.Text.Encoding.UTF8, "application/json"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Get balance ```http GET https://smsprovider-web.vercel.app/api/v1/account/balance ``` Balance, reserved credits, international balance and expiry. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `reports` ## Response `200` — `Balance` ```json { "balance_credits": 4820, "reserved_credits": 120, "available_credits": 4700, "intl_balance_credits": 0, "intl_available_credits": 0, "expires_at": "2027-11-02T00:00:00Z" } ``` ## Errors | Status | Code | | --- | --- | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/account/balance" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.account.balance(); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/account/balance", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/account/balance"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Get usage ```http GET https://smsprovider-web.vercel.app/api/v1/account/usage ``` Usage per day or per campaign. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `reports` ## Query parameters | Field | Type | Required | Notes | | --- | --- | --- | --- | | `from` | string (date-time) | no | | | `to` | string (date-time) | no | | | `group_by` | `day` \| `campaign` | no | default `"day"` | ## Response `200` — `Usage` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/account/usage" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.account.usage({ group_by: "day" }); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/account/usage", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/account/usage"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # List webhook endpoints ```http GET https://smsprovider-web.vercel.app/api/v1/webhooks ``` Your endpoints and the events they receive. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `webhooks` ## Response `200` — `WebhookEndpointPage` ## Errors | Status | Code | | --- | --- | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/webhooks" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.webhooks.list(); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/webhooks", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/webhooks"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Create a webhook endpoint ```http POST https://smsprovider-web.vercel.app/api/v1/webhooks ``` A public HTTPS URL. The `secret` is returned only once — store it to verify signatures. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `webhooks` ## Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `url` | string (uri) | yes | len 0–2000 | | `events` | `message.sent` \| `message.delivered` \| `message.failed` \| `message.expired` \| `inbound.received` \| `otp.verified` \| `otp.failed` \| `wallet.low_balance` \| `wallet.credited` \| `blacklist.added` \| `link.clicked`[] | yes | items 1–11 | ## Response `201` — `WebhookEndpoint` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X POST "https://smsprovider-web.vercel.app/api/v1/webhooks" \ -H "Authorization: Bearer $BATLER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/hooks/batler","events":["message.delivered","message.failed","inbound.received"]}' ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const body = { "url": "https://example.com/hooks/batler", "events": [ "message.delivered", "message.failed", "inbound.received" ] }; const result = await sms.webhooks.create(body); ``` ### PHP ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode(["url" => "https://example.com/hooks/batler", "events" => ["message.delivered", "message.failed", "inbound.received"]], JSON_UNESCAPED_UNICODE), ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "POST", "https://smsprovider-web.vercel.app/api/v1/webhooks", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, json={"url": "https://example.com/hooks/batler", "events": ["message.delivered", "message.failed", "inbound.received"]}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Post, "https://smsprovider-web.vercel.app/api/v1/webhooks"); req.Content = new StringContent("{\"url\":\"https://example.com/hooks/batler\",\"events\":[\"message.delivered\",\"message.failed\",\"inbound.received\"]}", System.Text.Encoding.UTF8, "application/json"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Delete a webhook endpoint ```http DELETE https://smsprovider-web.vercel.app/api/v1/webhooks/{id} ``` Stops delivering events to the URL. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `webhooks` ## Path parameters | Field | Type | | --- | --- | | `id` | string (uuid) | ## Response `200` — `Ok` ## Errors | Status | Code | | --- | --- | | 404 | `not_found` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X DELETE "https://smsprovider-web.vercel.app/api/v1/webhooks/{id}" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.webhooks.delete(id); ``` ### PHP ```php "DELETE", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "DELETE", "https://smsprovider-web.vercel.app/api/v1/webhooks/{id}", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Delete, "https://smsprovider-web.vercel.app/api/v1/webhooks/{id}"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # List deliveries ```http GET https://smsprovider-web.vercel.app/api/v1/webhooks/{id}/deliveries ``` Delivery attempts, response codes and status. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `webhooks` ## Path parameters | Field | Type | | --- | --- | | `id` | string (uuid) | ## Query parameters | Field | Type | Required | Notes | | --- | --- | --- | --- | | `limit` | integer | no | 1–1000, default `100` | | `cursor` | string | no | len 0–200 | ## Response `200` — `WebhookDeliveryPage` ## Errors | Status | Code | | --- | --- | | 404 | `not_found` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/webhooks/{id}/deliveries" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.webhooks.deliveries(id); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/webhooks/{id}/deliveries", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/webhooks/{id}/deliveries"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Replay a delivery ```http POST https://smsprovider-web.vercel.app/api/v1/webhooks/{id}/deliveries/{delivery_id}/replay ``` Re-queues an event (same `X-Batler-Event-Id`). **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `webhooks` ## Path parameters | Field | Type | | --- | --- | | `id` | string (uuid) | | `delivery_id` | string (uuid) | ## Response `200` — `Ok` ## Errors | Status | Code | | --- | --- | | 404 | `not_found` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X POST "https://smsprovider-web.vercel.app/api/v1/webhooks/{id}/deliveries/{delivery_id}/replay" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.webhooks.replay(id, deliveryId); ``` ### PHP ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "POST", "https://smsprovider-web.vercel.app/api/v1/webhooks/{id}/deliveries/{delivery_id}/replay", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Post, "https://smsprovider-web.vercel.app/api/v1/webhooks/{id}/deliveries/{delivery_id}/replay"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ``` --- # Price quote by volume ```http GET https://smsprovider-web.vercel.app/api/v1/pricing/quote ``` Price before VAT for a credit amount (plus the VAT and the total), the current tier and how many credits to the next tier. **Authentication:** `Authorization: Bearer bm_live_…` · **Required scope:** `reports` ## Query parameters | Field | Type | Required | Notes | | --- | --- | --- | --- | | `credits` | integer | yes | 1–10000000 | ## Response `200` — `Quote` ## Errors | Status | Code | | --- | --- | | 400 | `validation_error` | | 401 | `unauthorized` | | 403 | `insufficient_scope` | | 429 | `rate_limited` | | 500 | `internal_error` | ## Examples ### curl ```bash curl -X GET "https://smsprovider-web.vercel.app/api/v1/pricing/quote?credits=12000" \ -H "Authorization: Bearer $BATLER_API_KEY" ``` ### Node.js ```ts import { BatlerSMS } from "@batler/sms"; const sms = new BatlerSMS({ apiKey: process.env.BATLER_API_KEY! }); const result = await sms.pricing.quote(12000); ``` ### PHP ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("BATLER_API_KEY"), "Content-Type: application/json", ], ]); $result = json_decode(curl_exec($ch), true); ``` ### Python ```python import os, uuid, requests resp = requests.request( "GET", "https://smsprovider-web.vercel.app/api/v1/pricing/quote?credits=12000", headers={"Authorization": f"Bearer {os.environ['BATLER_API_KEY']}"}, timeout=30, ) resp.raise_for_status() result = resp.json() ``` ### C# ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("BATLER_API_KEY")); var req = new HttpRequestMessage(HttpMethod.Get, "https://smsprovider-web.vercel.app/api/v1/pricing/quote?credits=12000"); var res = await http.SendAsync(req); var result = await res.Content.ReadAsStringAsync(); ```