API docs
Contents

RebelText API

View as Markdown

Send SMS and MMS, track delivery, and get status updates pushed to your server.

Base URL

https://rebeltext.docoitest.com

Every request is authenticated with your API key, and every request and response body is JSON: send Content-Type: application/json whenever a request has a body.

AI agents and LLM tools can read this guide as Markdown, which /docs also returns to any request that sends Accept: text/markdown.


Quick start

  1. Send a message.

    curl -X POST https://rebeltext.docoitest.com/v1/messages \
      -H "Authorization: Bearer $REBELTEXT_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"to_number": "5125550199", "message": "Your order has shipped."}'
    

    The response includes a reference_id, such as msg_01HQ3F5X8K2M9N4P6R7S8T9V0W. That's the message's ID.

  2. Check whether it was delivered.

    curl https://rebeltext.docoitest.com/v1/messages/msg_01HQ3F5X8K2M9N4P6R7S8T9V0W \
      -H "Authorization: Bearer $REBELTEXT_API_KEY"
    
  3. Stop polling. Register an endpoint and we'll tell your server the moment a message's status changes.


Authentication

Send your API key as a bearer token on every request:

Authorization: Bearer rt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

There are two kinds of key:

Prefix What it does
rt_live_ Sends real messages to real phones.
rt_test_ For building and testing your integration. Messages go through validation, pricing, rate limits and webhooks exactly like live ones, but never reach a phone and always end up delivered. Test keys are refused in production.

Treat your key like a password: keep it out of client-side code and out of source control.

You need a subscription to send

A new key works straight away for reading, but sending needs an active plan. Without one, every endpoint that sends a message, registers a brand or campaign, or buys a number answers 402:

{
  "status": false,
  "message": "This account has no active subscription. Choose a plan to start sending.",
  "code": "subscription_required"
}

You can still read: GET /v1/account, GET /v1/plans, GET /v1/messages and your invoices all keep working, so you can see where you stand. Pick a plan in the console and your key can send on its very next request. There's no waiting period and nothing to restart.

Don't confuse this with insufficient_balance. subscription_required means there's no plan; insufficient_balance means there is a plan, but your balance has run out.

Authentication errors

Problem Status code
Key missing or not recognised 401 unauthenticated
Key is valid, but the account is suspended or closed 403 account_inactive
Account has no active subscription 402 subscription_required
Test key used against production 403 test_key_in_production
Provisioning a number past the account's number allowance 403 number_limit_reached

Responses and errors

Every response has the same envelope, so one piece of code can handle them all.

Success

{
  "status": true,
  "message": "Success",
  "data": { }
}

Error

{
  "status": false,
  "message": "The to_number is not a valid North American number.",
  "code": "invalid_request",
  "errors": {
    "to_number": ["The to_number is not a valid North American number."]
  }
}
status
true on success, false on error.
message
A human-readable summary. On a validation error it repeats the first field error, so it's safe to show to a user.
code
A stable, machine-readable error code. Branch on code, never on message: we may reword messages, but codes don't change. See Error codes.
errors
Only on validation errors (422). Maps each field name to a list of what's wrong with it.

Unknown fields in a request body are rejected rather than silently ignored. A typo such as toNumber gets a 422 that names the field.

Request IDs

Every response carries an X-Request-Id header. You can send your own X-Request-Id and we'll use it instead. Quote it when you contact support and we can find the exact request. Recent requests are also listed in the console under API keys › Request log.


Rate limits

Your plan sets how many messages a second you can send. Go over it and you get 429 rate_limited with a Retry-After header: the number of seconds to wait. A rate-limited request isn't charged. Wait at least that long, then retry.


Pagination

The message lists return one page at a time, newest first. Every page says where it sits in the list:

limit
The page size used: what you asked for, or the endpoint's default.
count
How many items are on this page. Never more than limit, and fewer only on the last page.
has_more
Whether there's another page.
next_cursor
Pass it as cursor to get the next page. "" on the last page.

All four are always present. When has_more is true the next page is never empty, so keep requesting until it's false.

A message list covers the last 30 days unless you set since and until (RFC 3339 times).


Media (MMS)

Add media to a send and the message becomes an MMS. message is then optional.

  • A message carries one attachment.
  • It must be an image or audio file: JPEG, PNG, GIF, WebP, BMP, TIFF, HEIC/HEIF, AVIF, MP3, M4A, AAC, WAV, Ogg, Opus, AMR, FLAC, AIFF or MIDI. SVG isn't accepted.
  • An MMS costs a flat price per message, however long its text. See Pricing and segments.
  • In the response, sms_type is "mms" and media_url is the attachment's URL.

Attach a file by URL

Send a public http or https URL that the carrier can fetch. A URL ending in one of the file extensions above, such as .png or .mp3, is accepted as is. Any other URL is fetched briefly when you send, and must turn out to be an image or audio file. URLs on private or local network addresses are refused.

curl -X POST https://rebeltext.docoitest.com/v1/messages \
  -H "Authorization: Bearer $REBELTEXT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to_number": "5125550199",
    "message": "Here is your ticket.",
    "media": "https://cdn.example.com/tickets/4471.png"
  }'

Upload a file

Send the request as multipart/form-data, with the file in a part named media, up to 5 MiB. We work out the file type from its contents, not its name, and host the file for the carrier to fetch.

curl -X POST https://rebeltext.docoitest.com/v1/messages \
  -H "Authorization: Bearer $REBELTEXT_API_KEY" \
  -F to_number=5125550199 \
  -F message="Here is your ticket." \
  -F media=@ticket.png

When media is refused

A media value that isn't a supported file, can't be fetched, is over 5 MiB, or is sent as both a URL and a file gets 422 invalid_request, with a media entry in errors.


Phone numbers

  • A number without a leading + gets country_code added (default +1), so 5125550199 and 15125550199 are the same US recipient.
  • A number starting with + is used as is, and country_code is ignored.
  • US and Canadian (+1) numbers must be valid 10-digit North American numbers.

Pricing and segments

Your account holds a balance in US dollars. You add to it by topping up, and your plan may add to it each billing period. Every message is paid for from that balance:

Message How it's priced
SMS Your account's price per segment. RebelText sets the platform price (for example $0.0095 a segment), and your account may have a negotiated price of its own.
MMS A flat price per message, however long its text.
Received SMS May be charged per segment. Free by default, in which case cost on an sms.inbound event is "0.00".

cost in a response is what that message costs, and currency is always "usd". A 200-character GSM message, for example, is 2 segments: at $0.0095 a segment it has "segments": 2 and "cost": "0.019", and your balance goes down by that much.

Amounts are strings

cost, balance and every other amount are decimal strings of US dollars, with up to 4 decimal places and never fewer than 2: "0.01", "0.019", "0.0095", "48.21". They're strings so they never pass through floating point. If you do arithmetic on them, parse them with a decimal type, not a float.

How many segments a message takes

Characters used One segment holds Each segment of a longer message holds
Only standard GSM characters 160 characters 153 characters
Any other character, including most emoji and accented letters outside the GSM set 70 characters 67 characters

{ } [ ] ~ ^ \ | and € each count as two GSM characters.


Safe retries

A network error can leave you unsure whether a send went through, and simply retrying can send the message twice. Send an Idempotency-Key header to make retries safe.

If you retry with the same key, from the same API key, within 24 hours:

  • no second message is sent, and you're not charged again
  • the response is 202 with the original reference_id and "duplicate": true
  • created_at and updated_at are null; read the message for its current state
{
  "status": true,
  "message": "Message accepted",
  "data": {
    "reference_id": "msg_01HQ3F5X8K2M9N4P6R7S8T9V0W",
    "status": "accepted",
    "created_at": null,
    "updated_at": null,
    "balance": "48.21",
    "duplicate": true
  }
}

(Shortened: the full message object is returned.)

Pick a key that identifies the send itself, such as your order ID plus the event (order-4471-shipped), not a random value generated for each attempt.


How webhooks work

A webhook sends an HTTP POST to your server when something happens to your messages, so you don't have to poll.

  1. Register an endpoint with your server's URL, in the console under Webhooks or with the API, and save the secret it returns.
  2. Receive events on that URL and answer each with a 2xx.
  3. Verify each event's signature before you trust it.

There are two event types. An endpoint subscribes to one or both, and to sms.status alone if you don't say.

sms.status
A message you sent changes status: scheduled, sending, sent, delivered, undelivered or failed.
sms.inbound
Someone sends an SMS to one of your numbers.

Receiving events

Each event arrives as a POST with a JSON body and these headers:

X-RebelText-Event
The event type: sms.status or sms.inbound.
X-RebelText-Delivery-Id
A unique ID for this event (whd_...). A retried event keeps its ID, so use it to ignore duplicates.
X-RebelText-Attempt
Which attempt this is, starting at 1.
X-RebelText-Timestamp
When it was sent, in Unix seconds.
X-RebelText-Signature
Proves the event came from us. See Verifying signatures.

The body is a message object, the same one GET /v1/messages/{reference_id} returns, so one parser handles both.

How to respond

  • Return any 2xx as soon as the event arrives. Anything else, or no answer within 10 seconds, counts as a failure and is retried.
  • Redirects aren't followed, so a 3xx is a failure too. Register the final URL.
  • Return 410 Gone to refuse an event for good. We stop retrying it straight away.
  • Do slow work, such as database updates or sending emails, after you respond, for example in a background job.
  • Events can arrive more than once and out of order. Skip any X-RebelText-Delivery-Id you've already processed, and order a message's events by updated_at, not by when they arrived.

sms.status body

{
  "reference_id": "msg_01HQ3F5X8K2M9N4P6R7S8T9V0W",
  "sender": "+15125550100",
  "receiver": "+15125550199",
  "sms_type": "sms",
  "message": "Your order #4471 has shipped.",
  "media_url": null,
  "status": "delivered",
  "remarks": "The message was delivered to the recipient.",
  "error_code": null,
  "carrier_error_code": null,
  "carrier_error_message": null,
  "segments": 1,
  "cost": "0.0095",
  "currency": "usd",
  "attempts": 1,
  "metadata": { "order_id": "4471" },
  "api_key_id": "key_01HQ2Z8Y7X6W5V4T3S2R1Q0P9N",
  "scheduled_at": null,
  "sent_at": "2026-09-17T10:04:11.881000Z",
  "received_at": null,
  "delivered_at": "2026-09-17T10:04:14.019000Z",
  "created_at": "2026-09-17T10:04:11.204000Z",
  "updated_at": "2026-09-17T10:04:14.019000Z"
}

sms.inbound body

For a received message, sender is the person who texted you, receiver is your number, and reference_id starts with inb_.

{
  "reference_id": "inb_01HQ5B7C8D9E0F1G2H3J4K5M6N",
  "sender": "+15125550199",
  "receiver": "+15125550100",
  "sms_type": "sms",
  "message": "Thanks! When will it arrive?",
  "media_url": null,
  "status": "received",
  "remarks": "The message was received from the sender.",
  "error_code": null,
  "carrier_error_code": null,
  "carrier_error_message": null,
  "segments": 1,
  "cost": "0.00",
  "currency": "usd",
  "attempts": 0,
  "metadata": {},
  "api_key_id": null,
  "scheduled_at": null,
  "sent_at": null,
  "received_at": "2026-09-17T10:06:40.512000Z",
  "delivered_at": null,
  "created_at": "2026-09-17T10:06:40.730000Z",
  "updated_at": "2026-09-17T10:06:40.730000Z"
}

Verifying signatures

Always check that an event came from RebelText before you act on it. X-RebelText-Signature is built like this:

base64( hex( HMAC-SHA256(raw_request_body, webhook_secret) ) )

To check it:

  1. Compute the HMAC-SHA256 of the raw request body, using your endpoint's full secret (including whsec_) as the key.
  2. Write the result as a lowercase hex string.
  3. Base64-encode that hex string.
  4. Compare the result with X-RebelText-Signature using a constant-time comparison.
  5. Reject the event if X-RebelText-Timestamp is more than 5 minutes from your current time, in either direction.

Use the raw body exactly as it arrived. If you parse the JSON and encode it again, the bytes change and the signature won't match.

Node.js (Express)

const crypto = require('crypto');
const express = require('express');

const app = express();
const SECRET = process.env.REBELTEXT_WEBHOOK_SECRET;

app.post('/hooks/rebeltext', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.get('X-RebelText-Signature') || '';
  const timestamp = Number(req.get('X-RebelText-Timestamp'));

  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) {
    return res.sendStatus(400);
  }

  const hex = crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
  const expected = Buffer.from(Buffer.from(hex).toString('base64'));
  const received = Buffer.from(signature);

  if (expected.length !== received.length || !crypto.timingSafeEqual(expected, received)) {
    return res.sendStatus(403);
  }

  const event = JSON.parse(req.body);
  res.sendStatus(200);

  // Process `event` after responding.
});

PHP

<?php
$rawBody   = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_REBELTEXT_SIGNATURE'] ?? '';
$timestamp = (int) ($_SERVER['HTTP_X_REBELTEXT_TIMESTAMP'] ?? 0);
$secret    = getenv('REBELTEXT_WEBHOOK_SECRET');

if (abs(time() - $timestamp) > 300) {
    http_response_code(400);
    exit;
}

$expected = base64_encode(hash_hmac('sha256', $rawBody, $secret));

if (!hash_equals($expected, $signature)) {
    http_response_code(403);
    exit;
}

$event = json_decode($rawBody, true);
http_response_code(200);

Python

import base64, hashlib, hmac, time

def verify(secret: str, signature: str, timestamp: str, body: bytes) -> bool:
    try:
        if abs(time.time() - int(timestamp)) > 300:
            return False
    except (TypeError, ValueError):
        return False

    digest = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    expected = base64.b64encode(digest.encode()).decode()
    return hmac.compare_digest(expected, signature)

Retries and pausing

Retries. When a delivery fails, we try again on this schedule. Each delay varies by up to ±20%.

Retry Delay after the previous attempt
1 30 seconds
2 2 minutes
3 10 minutes
4 1 hour
5 6 hours

If every retry fails, the delivery is marked dead. Once your server is working again you can replay it.

Pausing. After 10 failed deliveries in a row, we pause sending to that endpoint. The first pause lasts 30 seconds; if failures continue, later pauses last 1, 2, 5 and then 15 minutes. When a pause ends we send one delivery first, and the rest once it succeeds. While paused, disabled_until in List endpoints shows when sending resumes. A successful delivery resets both the failure count and the pause length.

Pacing. We send at most 8 deliveries to one endpoint at a time, so a backlog arrives as a steady stream rather than all at once.


Use with AI agents (MCP)

RebelText speaks the Model Context Protocol, so an AI agent such as Claude, ChatGPT, Claude Code or Cursor can send messages and look up their status for you. It runs on an ordinary API key and behaves exactly like the REST API: the same validation, the same subscription requirement, the same charges, and every call appears in your API log.

Endpoint: https://rebeltext.docoitest.com/mcp (Streamable HTTP).

Sign in from the app

Most AI apps, such as Claude and ChatGPT, need only the endpoint URL. Add it as a connector or custom MCP server and the app sends you to the RebelText console to sign in and approve it. The page names the app and the address it will return you to. Only allow it if that address belongs to the app you are connecting.

Approving issues a live API key named after the app, such as "Claude (connected app)". It counts toward your plan's keys and appears on the API keys page with the others. To disconnect the app, revoke that key. Connecting needs a plan and a free key in your allowance.

Use an API key

Apps that take a header instead can use a key you issue yourself. Claude Code:

claude mcp add --transport http rebeltext https://rebeltext.docoitest.com/mcp \
  --header "Authorization: Bearer $REBELTEXT_API_KEY"

Other clients take the same URL and Authorization header in their MCP server settings.

Tools

Tool Does REST equivalent
send_message Sends one SMS or MMS. Charges your balance. POST /v1/messages
get_message One sent message and its status GET /v1/messages/{id}
list_messages Sent messages, with filters GET /v1/messages
get_inbound_message One received message GET /v1/inbound-messages/{id}
list_inbound_messages Received messages GET /v1/inbound-messages
get_conversation Texts to and from one number, oldest first, each marked outbound or inbound GET /v1/messages and GET /v1/inbound-messages
get_account Account, plan and subscription GET /v1/account
get_usage Balance and queue depth GET /v1/account/usage
list_numbers Numbers you can send from GET /v1/numbers
list_suppressions Numbers you won't message GET /v1/suppressions
add_suppressions Stop messaging numbers POST /v1/suppressions
remove_suppression Allow a number again DELETE /v1/suppressions/{number}

send_message is marked as destructive, so well-behaved clients ask you before each send. An agent with your key can still spend your balance, so give it a key you can revoke, and pass idempotency_key so a retried send never goes out twice (Safe retries).


API reference

Every endpoint, grouped by what it acts on. Each one lists its parameters, with a request and a response example beside it. Paths are relative to the base URL, https://rebeltext.docoitest.com, and every request needs your API key.


Messages

Send messages and find out what happened to them. Both endpoints return the message object.

Send a message (SMS or MMS)

POST /v1/messages

Queues one SMS for delivery, or one MMS when it carries media.

A successful call returns 202 Accepted: the message is queued, not yet delivered. To find out what happened next, check its status or let a webhook tell you.

Headers

Authorizationrequired
Bearer <your API key>
Content-Typerequired
application/json, or multipart/form-data to upload a media file. Both take the same field names.
Idempotency-Keyoptional
A unique string for this send, such as order-4471-shipped. Retry with the same key within 24 hours and no second message is sent. See Safe retries.

Body

to_numberstringrequired
Who to send to. Any common format works: 5125550199, (512) 555-0199, +15125550199. See Phone numbers.
messagestringrequired, unless media is sent
The text. Up to 1600 characters and 10 segments.
mediastring or fileoptional
One image or audio attachment, as a public URL or an uploaded file. Makes the message an MMS. See Media.
from_numberstringoptional
One of your numbers to send from. Leave it out and we pick the best available number on your account.
country_codestringoptional
Country code for to_number, such as +1. Defaults to +1. Ignored when to_number starts with +.
metadataobjectoptional
Your own string key/value pairs, such as {"order_id": "4471"}. Returned when you read the message and in its webhooks. In a multipart/form-data request, send it as a JSON-encoded string.
scheduled_atstringoptional
Send later instead of now: an RFC 3339 time such as 2026-09-20T15:00:00Z, up to 90 days ahead and not in the past.

What you get back

data is a message object plus balance: what your account has left, in US dollars, after paying for this message.

  • Save reference_id. It's the ID you use to check the message's status and to match its webhook events.
  • status is accepted, or scheduled if you set a future scheduled_at.
  • receiver is the recipient in E.164 format, such as +15125550199.
  • sender is "" if you left out from_number. The number we chose shows up when you read the message after it's sent.

Errors

Problem Status code
Invalid phone number or country_code; message missing or too long; media unsupported, unreachable or too big; invalid scheduled_at 422 invalid_request
Body isn't valid JSON, or has an unknown field 400 / 422 invalid_request
Recipient has opted out of messages from you 422 recipient_suppressed
Balance too low for this message 402 insufficient_balance
No active subscription 402 subscription_required
Too many requests a second (Rate limits) 429 rate_limited
Request body too large 413 payload_too_large

Check message status

GET /v1/messages/{reference_id}

Returns the current state of a message you sent.

Reading the result

data is a message object. Its status says where the message is; Message statuses lists every value.

When a message fails, status is failed or undelivered, error_code is set and remarks gives the reason. If the carrier supplied its own code and text, you get them word for word in carrier_error_code and carrier_error_message:

{
  "status": "undelivered",
  "remarks": "The carrier accepted the message but could not deliver it. Reason: Blocked by carrier spam filter.",
  "error_code": "40002",
  "carrier_error_code": "40002",
  "carrier_error_message": "Message blocked by carrier spam filter"
}

Tip: Don't poll this endpoint in a tight loop. Webhooks tell you the moment a status changes.

Errors

Problem Status code
No message with this ID on your account 404 not_found

List messages

GET /v1/messages

Returns the messages you've sent, newest first, a page at a time. See Pagination.

limitdefault 50
How many messages to return, up to 200.
cursor
next_cursor from the previous page.
sincedefault 30 days ago
Start of the time range, as an RFC 3339 time.
untildefault now
End of the time range, as an RFC 3339 time.
status
Only messages with this status.
from
Only messages sent from this number. Any common format works.
to
Only messages sent to this number. Any common format works.
api_key_id
Only messages sent with this API key (key_...).

A from or to that isn't a phone number gets 422 invalid_request rather than an empty list. A message still waiting for us to choose its sender has no from yet, so it only appears in lists that don't filter on from.

Send a batch

POST /v1/messages/batch

Sends a message to up to 1000 recipients in one request. Settings at the top apply to every message, and each recipient can override body, media and metadata. A recipient's metadata is merged into the batch's, not swapped for it.

The field names differ from Send a message: from, body and to here, and media is a list.

messagesobject[]required
1 to 1000 recipients. Each has to (required, any common phone format) and, optionally, its own body, media and metadata.
bodystringrequired, unless every recipient has its own body or media
The text for every message.
fromstringoptional
One of your numbers to send from. Leave it out and we pick.
mediastring[]optional
Attachment URLs for every message, which makes them MMS. The response's media_url reports the first.
metadataobjectoptional
Your own string key/value pairs, added to every message.
scheduled_atstringoptional
Send later instead of now: an RFC 3339 time.

Every recipient is accepted or rejected on its own, so one bad number doesn't hold up the rest. The response is 202 whenever at least one recipient was accepted, and 422 only when none were.

A recipient that didn't become a message has reference_id null, a cost of "0.00", and status either rejected (the reason follows Reason: in remarks, and error_code names it) or error (a problem on our side; error_code is internal_error).

Batches don't take an Idempotency-Key, so retrying a batch after a network error can send it twice. Before you retry, list messages to see what went out.

Get a batch

GET /v1/batches/{id}

Returns a batch's summary: how many recipients were accepted and rejected, and what they cost. Each recipient's outcome is in the response to Send a batch; to see them later, list messages.

Errors

Problem Status code
No batch with this ID on your account 404 not_found

Received messages

Messages people send to your numbers, the console's Received messages. Each one is stored, charged and sent to any endpoint subscribed to sms.inbound (see Receiving events). These endpoints read them back.

  • Whose reply it is. A reply to one of your own numbers is yours. A reply to a shared-pool number goes to the account that most recently messaged that person from it in the last 30 days.
  • What it costs. A received SMS is charged per segment at the price for received messages, which is free unless your account says otherwise; a received MMS is charged per message. The charge is taken when the message arrives, even if it takes your balance below zero.
  • Attachments. A received MMS has its first attachment in media_url and all of them in media_urls. We copy them to our own storage within seconds, because the carrier's copies expire after 30 days.
  • Opt-outs. A reply that's just STOP, STOPALL, UNSUBSCRIBE, CANCEL, END or QUIT (in any case) adds the sender to your suppressions, and START removes them again. The reply itself is still stored and delivered. HELP isn't answered for you.

List received messages

GET /v1/inbound-messages

Returns replies to your numbers, newest first, a page at a time. See Pagination.

limitdefault 50
How many messages to return, up to 200.
cursor
next_cursor from the previous page.
sincedefault 30 days ago
Start of the time range, as an RFC 3339 time.
untildefault now
End of the time range, as an RFC 3339 time.
from
Only messages from this number. Any common format works.
to
Only messages to this one of your numbers.

Get a received message

GET /v1/inbound-messages/{id}

Returns one reply, exactly as the sms.inbound webhook delivered it.

Errors

Problem Status code
No received message with this ID on your account 404 not_found

Suppressions

Numbers you won't message. A send to a suppressed number is refused with 422 recipient_suppressed and isn't charged. A recipient who replies STOP is added automatically (see Received messages), and you can add your own, such as a do-not-call list.

List suppressions

GET /v1/suppressions

Returns your suppressed numbers, each with why and when it was added.

limitdefault 100
How many to return, up to 500.

Add suppressions

POST /v1/suppressions

Stops messages to one number or thousands, straight away.

numbersstring[]required
1 to 10,000 phone numbers, in any common format.
reasonstringoptional
manual (the default), dnc_import, carrier_block or complaint. stop_keyword is refused: only a STOP reply records it.
notestringoptional
Your own note, applied to every number.

Numbers that can't be read as phone numbers are skipped and listed in invalid; the rest are still added.

Remove a suppression

DELETE /v1/suppressions/{number}

Lets you message a number again. The number can be in any common format.

A number that replied STOP can't be removed this way: only their own START reply lifts it.

Errors

Problem Status code
The number isn't suppressed 404 not_found
The recipient replied STOP 409 recipient_opted_out

Numbers

List your numbers

GET /v1/numbers

Returns the numbers you can send from: your own, plus any shared numbers available to your account.

number
The number, in E.164 format.
dedicated
true for a number that's yours alone, false for a shared one.
sms, mms
Whether it can send SMS and MMS.
toll_free
Whether it's a toll-free number.
tps_limit
The most messages a second it may send. Can be below 1 (some carriers allow only a few a minute). Left out when there's no limit.
daily_limit
The most messages a day it may send. Left out when there's no limit.

Account

Get your account

GET /v1/account

Returns your account, its plan, balance and subscription.

id, name, status
Your account.
plan_id
Your plan. Left out when you're on no plan.
balance, currency
What you have left, as a decimal string of US dollars.
rate_per_second
How many messages a second you can send. See Rate limits.
subscription
Your active subscription: its plan, status and current billing period, and whether it cancels at the end of it. Left out when there isn't one.

Get usage

GET /v1/account/usage

Your balance, plan, send rate and how busy delivery is, in one call, so a bulk sender can pace itself.

balance, currency
What you have left, as a decimal string of US dollars.
plan
Your plan. Left out when you're on no plan.
rate_per_second
How many messages a second you can send.
queue_depth
Messages waiting to be sent across the whole platform, not just yours: a sign of how backed up delivery is.

There's no API for adding money: top up from the console (Billing), in whole dollars.


Webhooks

Manage your endpoints: the URLs we send events to. You can do the same in the console under Webhooks. How events are delivered, verified and retried is covered in How webhooks work.

Register an endpoint

POST /v1/webhooks

Registers a URL to receive events.

urlstringrequired
Where we send events. http or https; use https for anything real.
event_typesstring[]optional
Which events to receive: sms.status, sms.inbound, or both. Defaults to ["sms.status"].
descriptionstringoptional
A note for yourself, up to 500 characters.

⚠️ Save secret from the response now: it is shown only once. You need it to verify signatures. If you lose it, delete the endpoint and register a new one.

Errors

Problem Status code
Missing or invalid url, unknown event type, description too long 422 invalid_request

List endpoints

GET /v1/webhooks

Returns all your endpoints and how healthy each one is. secret is never included.

Each endpoint has these health fields as well as the ones it was created with:

consecutive_failures
Failed deliveries in a row. Back to 0 after a successful delivery.
disabled_until
Set while we've paused sending to this endpoint after too many failures. See Retries and pausing. Left out when not paused.
last_success_at
When a delivery last succeeded. Left out if none has.
last_failure_at
When a delivery last failed. Left out if none has.

Delete an endpoint

DELETE /v1/webhooks/{id}

Stops all events to this endpoint. Returns 404 not_found if there's no endpoint with this ID on your account.

Send a test event

POST /v1/webhooks/{id}/test

Sends a sample sms.status event, with status delivered, to this endpoint (the console's Send test). Use it to check that your server receives events and verifies signatures, without sending a real message.

The test event has reference_id msg_test_00000000000000000000000000 and metadata {"test": "true"}, so you can recognise and ignore it.


Delivery log

Every event we send to an endpoint is a delivery. The console shows them under Webhooks › Delivery log. Use these requests to see what was sent, what your server answered, and to send a delivery again.

List deliveries

GET /v1/webhook-deliveries

Shows recent webhook deliveries, newest first, with the payload we sent and the response your server gave. Use it to debug an endpoint that isn't working.

message_id
Only deliveries for this message (msg_...).
limitdefault 50
How many deliveries to return, up to 200.

Each delivery has a status:

Delivery status Meaning
pending Waiting to be sent. After a failed attempt, last_error and response_code explain the failure and next_retry_at says when it will be retried.
sending Being sent now.
delivered Your server returned a 2xx (delivered_at).
dead All retries failed, or your server returned 410. Nothing more happens unless you replay it.

Replay a delivery

POST /v1/webhook-deliveries/{id}/replay

Sends a dead or delivered delivery again, with a fresh set of retries. It keeps the same payload and delivery ID, so your duplicate check still works.

Returns 404 not_found if the delivery doesn't exist, is still pending or sending, or belongs to an endpoint that has been deleted.


Objects and errors

The message object

Sending a message, reading one, and every webhook event all return this object. Every field is always present, except media_urls: one that doesn't apply is null, or {} for metadata. All timestamps are UTC, such as 2026-09-17T10:04:14.019000Z.

reference_idstring
The message ID: msg_... for messages you send, inb_... for messages you receive.
senderstring
The sending number, in E.164 format. "" until a number is chosen, if you didn't set from_number.
receiverstring
The receiving number, in E.164 format.
sms_typestring
sms, or mms for a message with media.
messagestring
The message text.
media_urlstring | null
The attachment's URL for an MMS; null for SMS.
media_urlsstring[]
Only on a received MMS: every attachment's URL. Left out otherwise.
statusstring
Where the message is. See Message statuses.
remarksstring
status in plain English. On failure it ends with Reason: and the reason.
error_codestring | null
Set when status is failed, undelivered or rejected; otherwise null. See Failure codes.
carrier_error_codestring | null
The carrier's own error code, exactly as it sent it, when a failure came from the carrier. null when the carrier gave none, including for failures RebelText sets itself.
carrier_error_messagestring | null
The carrier's own error text, exactly as it sent it, up to 512 characters. null when the carrier gave none.
segmentsinteger
How many segments the message was split into.
coststring
What the message costs, in US dollars, as a decimal string such as "0.0095". See Pricing and segments.
currencystring
The currency of cost. Always usd.
attemptsinteger
Send attempts so far.
metadataobject
The metadata you sent, or {}.
api_key_idstring | null
The API key the message was sent with, such as key_01HQ.... null for messages you receive.
scheduled_atstring | null
When the message is scheduled to send, if it is.
sent_atstring | null
When the carrier accepted the message.
received_atstring | null
When a received message arrived. null for messages you send.
delivered_atstring | null
When delivery to the phone was confirmed.
created_atstring | null
When the message was created.
updated_atstring | null
When the message last changed status.

Message statuses

A sent message usually moves through these statuses:

accepted → queued → sending → sent → delivered
                                   ↘ undelivered
Status Meaning Final? Charged?
accepted We received it and queued it for sending.
scheduled Waiting for its scheduled_at time.
queued Waiting to be sent.
sending Being handed to the carrier.
sent The carrier accepted it but hasn't confirmed delivery yet. Yes
delivered The carrier confirmed it reached the phone. ✅ Yes
undelivered The carrier accepted it but couldn't deliver it. ✅ Yes
failed It couldn't be sent. ✅ Only if the carrier accepted it first
rejected Refused before sending. ✅ No
cancelled Cancelled before it was sent. ✅ No
received A message someone sent you (sms.inbound). ✅ Yes
  • A message never moves back to an earlier status.
  • sent usually becomes delivered or undelivered within seconds, but some carriers never confirm delivery, so sent may be the last status you see.
  • Webhooks are sent for scheduled, sending, sent, delivered, undelivered and failed. Not for accepted: you already have that from the send response.

Failure codes

When status is failed, error_code says why. The codes below come from RebelText. Any other value comes from the carrier, and then carrier_error_code and carrier_error_message carry the carrier's own code and wording, so you can tell a carrier's spam block from a problem on the route.

error_code Meaning How to fix it
no_sender_number Your account has no number available to send from. Add a number to your account.
sender_not_sms_capable The sending number can't send SMS. Send from a different number, or leave out from_number.
sender_not_found from_number isn't one of your numbers. Use one of your account's numbers, or leave out from_number.

Error codes

code Status Meaning
invalid_request 400, 422 The request is malformed or failed validation. For a 422, errors lists the problems by field.
unauthenticated 401 The API key is missing or invalid.
account_inactive 403 Your account is suspended or closed.
test_key_in_production 403 You used an rt_test_ key against production.
insufficient_balance 402 Your balance is too low to send this message. Top up, or wait for your plan's next renewal.
subscription_required 402 Your account has no active subscription. Choose a plan in the console; sending works again on your next request.
not_found 404 The resource doesn't exist, or doesn't belong to your account.
payload_too_large 413 The request body is too large.
recipient_suppressed 422 The recipient has opted out of messages from you.
recipient_opted_out 409 You tried to remove a suppression the recipient asked for by replying STOP. Only their START reply lifts it.
rate_limited 429 Too many requests. Wait the number of seconds in Retry-After, then retry.
internal_error 500, 503 Something went wrong on our side. Retry with the same Idempotency-Key; if it keeps happening, contact support with the X-Request-Id.