Skip to content

Errors & statuses

TextMe reports failures in the body of an otherwise ordinary HTTP 200 response. There is one shape for all of them, and one field to branch on.

The error envelope

json
{
  "status": 4,
  "message": "Not enough credit"
}
FieldMeaning
status0 on success; any other value is a failure.
messageHuman-readable explanation. For status 2 it names the missing field.

Never branch on the HTTP status alone

Authentication failures, validation failures and quota failures all arrive as HTTP 200. A client that only checks response.ok will treat every one of them as a successful send.

Every code example on this site follows the same three steps: post, decode, then compare status to 0 before trusting anything else in the body.

Two statuses, two vocabularies

The word status appears in two unrelated places, and confusing them is the most common integration mistake.

WhereVocabulary
Request statusTop-level status on any responseDid the API accept this call?status codes
Delivery statusstatus inside each transactions[] entry of a delivery reportWhat did the handset do with the message?DLR statuses

A delivery report can carry status: 0 at the top (the report was produced) while individual transactions carry 102 (delivered) or 1 (failed). They are different scales that happen to share a field name.

Partial success

Several operations accept a batch and process it row by row. Those return status: 0 — the operation ran — together with an errors list describing the rows that did not make it.

json
{
  "status": 0,
  "message": "The phone numbers have been added successfully",
  "errors": [
    "The phone 05XXXXXXXX is already on the contact list and therefore not added"
  ]
}

The operations that behave this way are contact-list writes (newCL, removeCL, addNumCL, rmNumCL). Treating their status: 0 as "everything worked" will silently drop recipients — read errors whenever it is present.

Failure classes

Credentials and permissions3, 10, 11, 504, 511, 998 (as אין הרשאה in delivery statuses). The token is wrong, expired, mismatched, or the account is not entitled to the operation. Retrying does not help; see Authentication.

Malformed request1, 2, 997. The document did not parse, a required field is absent, or the root element is not an operation. Deterministic: the same payload will always fail. Fix the payload; the test endpoint is the cheap way to iterate.

Rejected values9, 714, 980, 986, 989, 990, 991, 992, 993, 995, 996. A specific field failed validation — a phone that is too short, a message that is too long, an add_unsubscribe value that is not 2 or 3. The message says which.

Account state4 (no credit), 12 (not enough money), 5 (not permitted to send at this hour), 515 (unverified sender). Nothing is wrong with the request; something is wrong with the account. These can become successful later, once the account is topped up, the hour changes, or the sender is verified.

Nothing left to send to8 (every destination is blocklisted), 715 (every destination was filtered by temp_bl), 988 (the contact list does not exist). The call was well-formed and simply had no recipients left. Worth logging distinctly: it usually means the audience, not the code, needs attention.

Server-side6, 970, 999. Process failure. Safe to retry once with backoff; if it persists, contact support.

Retrying safely

There is no idempotency key. A retried send is a second send, and both will be delivered and billed.

  • Never blind-retry a send. If sms or bulk times out at the transport level, you do not know whether the message went out. Resolve it by reading rather than writing: give every destination an id (see delivery reports) and query the report before resending.
  • Reads are free to retry. balance, dlr, dlrByDate, incoming, getCL, getVerifiedPhones and the other read operations change nothing.
  • Writes are not idempotent. Calling newCL twice creates two lists; addNumBL twice is harmless, but updateAmountSub twice moves credit twice.
  • Back off on 6 and 999. Retry once after a short delay rather than immediately.

Worked example

The pattern every example on this site uses, in full:

js
const response = await fetch('https://my.textme.co.il/api', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TEXTME_API_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(payload),
})

// The transport rarely fails, but when it does you learn nothing about
// whether the message was sent — do not retry a send here.
if (!response.ok) {
  throw new Error(`TextMe transport error: HTTP ${response.status}`)
}

const result = await response.json()

// This is the check that matters.
if (Number(result.status) !== 0) {
  throw new TextMeError(result.status, result.message)
}

// A batch operation can succeed overall and still drop rows.
for (const problem of result.errors ?? []) {
  console.warn('TextMe partial failure:', problem)
}

return result
python
import os

import httpx


class TextMeError(RuntimeError):
    def __init__(self, status, message):
        super().__init__(f"TextMe {status}: {message}")
        self.status = int(status)
        self.message = message


def call(payload):
    response = httpx.post(
        "https://my.textme.co.il/api",
        headers={"Authorization": f"Bearer {os.environ['TEXTME_API_TOKEN']}"},
        json=payload,
    )
    # A transport failure leaves a send in an unknown state — do not retry it.
    response.raise_for_status()

    result = response.json()

    # This is the check that matters.
    if int(result["status"]) != 0:
        raise TextMeError(result["status"], result["message"])

    # A batch operation can succeed overall and still drop rows.
    for problem in result.get("errors", []):
        print("TextMe partial failure:", problem)

    return result
php
<?php

function textme(array $payload): array
{
    $client = new \GuzzleHttp\Client([
        'headers' => [
            'Authorization' => 'Bearer '.getenv('TEXTME_API_TOKEN'),
            'Accept' => 'application/json',
        ],
    ]);

    $response = $client->post('https://my.textme.co.il/api', ['json' => $payload]);
    $result = json_decode($response->getBody()->getContents(), true);

    // This is the check that matters.
    if ((int) $result['status'] !== 0) {
        throw new RuntimeException("TextMe {$result['status']}: {$result['message']}");
    }

    // A batch operation can succeed overall and still drop rows.
    foreach ($result['errors'] ?? [] as $problem) {
        error_log("TextMe partial failure: {$problem}");
    }

    return $result;
}

Full tables

  • Status codes — every value the top-level status can take.
  • DLR statuses — every value a delivery report transaction can take, with English glosses for the Hebrew messages.