You are viewing public documentation. The code samples below show placeholder credentials. Log in or register to see your real API key pre-filled in every example.

API Documentation

Complete guide to integrating SMESS WhatsApp API into your application

New to this? You don't have to write any code — the API Tester below does it by clicking a button, and this page is written so your own AI assistant can read it and build the integration for you. Prefer to code it yourself? The Quick Start walks through every step.

Queue-Based Delivery Auto-Retry Logic Real-time Processing
API Key SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Create and reveal keys from your client portal.

Quick Start Guide

Three ways to get started — pick whichever fits you:
  • No coding at all. Use the point-and-click API Tester — fill in a short form and press Send to make a real WhatsApp message go out. Nothing to install, nothing to type.
  • Let an AI write the code for you. This page is written so an AI assistant (ChatGPT, Claude, Copilot, or similar) can read it and build your integration correctly on its own. Copy this page's address and tell your AI something like: “Read https://smess.io/api-docs and write me a script that sends a WhatsApp message using my SMESS API key.” Then paste in your own key from API Keys wherever it asks for one.
  • Learning to code and want to do it by hand. The 4 steps below get you to a working message. Every example on this page starts with a plain curl command you can paste straight into a terminal (on a Mac, open the Terminal app; on Windows, open Command Prompt or PowerShell) — no extra software needed.
1
Connect Your WhatsApp

Messages are sent from your own WhatsApp number. Link it once by scanning a QR code on the Connections page (WhatsApp → Linked Devices). Until a number is connected, sends can't deliver.

2
Get Your API Key

Register or log in, then create a key at API Keys.

SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3
Make Your First Request

Send messages via POST to /api/send using apikey, recipient and text — or try it without writing code in the API Tester, the fastest way to a first successful send.

4
Track & Handle Responses

The API returns HTTP 202 with a queue_id; errors carry an error_code. Watch delivery in Messages / Queue, or get push notifications via Delivery Webhooks.

Words used on this page, in plain terms
APIA way for one computer program to ask another to do something — here, to send a WhatsApp message. You (or your AI, or a script) send a request; SMESS sends the message.
API keyA private password-like code that proves a request is really from your account. Yours is on the API Keys page — never share it publicly.
EndpointA specific address the API listens on, e.g. /api/send. Different endpoints do different things (send one message, send many, check status).
RequestOne call to an endpoint — e.g. one instruction to send one message.
JSONA simple, structured way to write data as text, e.g. {"recipient":"+233...","text":"Hi"}. One of two ways to format a request on this page; a plain form is the other.
HTTP status codeA 3-digit number every response carries, e.g. 202 (accepted) or 401 (bad key). See Error Handling for the full list.
QueueMessages are not sent the instant you ask — they are placed in line and delivered moments later, paced to keep your number safe. queue_id is the ticket number for one message in that line.
WebhookThe reverse of an API call: instead of you checking on a message, SMESS notifies your own server the moment it is delivered or fails. Optional — see Delivery Webhooks.
Rate limitA ceiling on how many requests or messages you can send in a given time, there to protect your WhatsApp number from being flagged as spam. See Rate Limits.

Authentication

All API requests require your API key. You can authenticate in three ways (in priority order):

Method How Notes
POST field apikey=SM-... Recommended for server-to-server integrations.
Header X-API-Key: SM-... Preferred for cleaner logs.
Bearer Authorization: Bearer SM-... Standard OAuth-style header.
curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "text=Hello World!"
import requests

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    data={
        "recipient": "+233000000000",
        "text": "Hello World!",
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    recipient: "+233000000000",
    text: "Hello World!",
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'recipient' => '+233000000000',
        'text' => 'Hello World!',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "recipient=%2B233000000000&text=Hello+World%21";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("recipient", "+233000000000")
	form.Set("text", "Hello World!")

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(form.Encode()))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["recipient"] = "+233000000000",
    ["text"] = "Hello World!",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request.set_form_data(
  'recipient' => '+233000000000',
  'text' => 'Hello World!',
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)

JSON request bodies are supported. Send Content-Type: application/json with the same parameter names — form-encoded and JSON behave identically:

curl -X POST https://smess.io/api/send \
  -H "Content-Type: application/json" \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d '{
        "recipient": "+233000000000",
        "text": "Hello World!"
      }'
import requests

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    json={
        "recipient": "+233000000000",
        "text": "Hello World!",
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "recipient": "+233000000000",
    "text": "Hello World!"
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS     => json_encode([
        'recipient' => '+233000000000',
        'text' => 'Hello World!',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = """
    {
      "recipient": "+233000000000",
      "text": "Hello World!"
    }
    """;

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"strings"
)

func main() {
	body := `{
  "recipient": "+233000000000",
  "text": "Hello World!"
}`

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(body))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var json = @"{
  ""recipient"": ""+233000000000"",
  ""text"": ""Hello World!""
}";

var content = new StringContent(json, Encoding.UTF8, "application/json");

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request['Content-Type'] = 'application/json'
request.body = {
  'recipient' => '+233000000000',
  'text' => 'Hello World!',
}.to_json

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)
Security Tip: Keep your API key secure and never expose it in client-side code. Always make API calls from a server. If a key is leaked, revoke it immediately from API Keys.

Sender Identity & Fallback

Messages are delivered from your own WhatsApp number — recipients see your number, your business name, and your profile photo. You link the number once by scanning a QR code on the Connections page; it stays linked as a companion device.

TierChannelSender the recipient sees
Primary Your linked WhatsApp session Your number, name & photo
Automatic Fallback Cloud Backup Still your number, name & photo. If your primary session isn't connected, SMESS automatically routes through Cloud Backup instead — for any client who has it configured, on every send, no action needed from you. This isn't limited to a particular plan. If Cloud Backup isn't configured or is also unavailable, nothing is sent and you're notified by email so you can reconnect.
Interactive messages adapt automatically. Buttons, copy-code, and list messages are delivered as clear numbered text ("Reply with a number to choose", codes highlighted for easy copying) — so they work for every recipient on every device through your single direct connection. Keep your phone online — a linked device whose phone disappears for extended periods is eventually unlinked by WhatsApp.
Linked Channels — delivering through a partner's number

If your own number isn't connected yet, a partner business on SMESS can sponsor your delivery. You request the link from your Connections page and they approve it with caps and an end date; either side can end it instantly. Your API keys, quota, reports and webhooks stay entirely your own — only the transport is shared.

Because WhatsApp ties sender identity to the number, recipients see the sponsor's number and business name. SMESS therefore labels every linked message automatically — your brand on line one, optional event details on line two:

curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "text=Welcome! Your table is Table 4." \
  -d "event_name=Ama & Kofi Wedding" \
  -d "event_date=01 Aug 2026"

# Delivered to the guest as:
#   *Your Brand*
#   Ama & Kofi Wedding · 01 Aug 2026
#   Welcome! Your table is Table 4.
import requests

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    data={
        "recipient": "+233000000000",
        "text": "Welcome! Your table is Table 4.",
        "event_name": "Ama & Kofi Wedding",
        "event_date": "01 Aug 2026",
    },
)

print(response.json())

# Delivered to the guest as:
#   *Your Brand*
#   Ama & Kofi Wedding · 01 Aug 2026
#   Welcome! Your table is Table 4.
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    recipient: "+233000000000",
    text: "Welcome! Your table is Table 4.",
    event_name: "Ama & Kofi Wedding",
    event_date: "01 Aug 2026",
  }),
});

console.log(await response.json());

// Delivered to the guest as:
//   *Your Brand*
//   Ama & Kofi Wedding · 01 Aug 2026
//   Welcome! Your table is Table 4.
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'recipient' => '+233000000000',
        'text' => 'Welcome! Your table is Table 4.',
        'event_name' => 'Ama & Kofi Wedding',
        'event_date' => '01 Aug 2026',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));

// Delivered to the guest as:
//   *Your Brand*
//   Ama & Kofi Wedding · 01 Aug 2026
//   Welcome! Your table is Table 4.
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "recipient=%2B233000000000&text=Welcome%21+Your+table+is+Table+4.&event_name=Ama+%26+Kofi+Wedding&event_date=01+Aug+2026";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());

// Delivered to the guest as:
//   *Your Brand*
//   Ama & Kofi Wedding · 01 Aug 2026
//   Welcome! Your table is Table 4.
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("recipient", "+233000000000")
	form.Set("text", "Welcome! Your table is Table 4.")
	form.Set("event_name", "Ama & Kofi Wedding")
	form.Set("event_date", "01 Aug 2026")

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(form.Encode()))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}

// Delivered to the guest as:
//   *Your Brand*
//   Ama & Kofi Wedding · 01 Aug 2026
//   Welcome! Your table is Table 4.
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["recipient"] = "+233000000000",
    ["text"] = "Welcome! Your table is Table 4.",
    ["event_name"] = "Ama & Kofi Wedding",
    ["event_date"] = "01 Aug 2026",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());

// Delivered to the guest as:
//   *Your Brand*
//   Ama & Kofi Wedding · 01 Aug 2026
//   Welcome! Your table is Table 4.
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request.set_form_data(
  'recipient' => '+233000000000',
  'text' => 'Welcome! Your table is Table 4.',
  'event_name' => 'Ama & Kofi Wedding',
  'event_date' => '01 Aug 2026',
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)

# Delivered to the guest as:
#   *Your Brand*
#   Ama & Kofi Wedding · 01 Aug 2026
#   Welcome! Your table is Table 4.
ParameterTypeDescription
event_name optional Event, campaign or project name shown under your brand (max 80 chars). Ignored when you send on your own channel.
event_date optional Human-readable date shown beside the event name, e.g. 01 Aug 2026 (max 40 chars).
Send only your message body in text — do not add the brand or event line yourself, or it will appear twice. If no parameters are supplied, the default label agreed with your sponsor is used.
Using a brand-new number? Warm it up first

WhatsApp aggressively flags newly registered numbers that immediately start sending automated traffic — that can get a number restricted or banned regardless of which API or tool you use. Before going live:

1–2 weeks of normal use Use the number in the WhatsApp app like a human first: real two-way conversations, a group or two, complete business profile (name, photo, category, description).
Ramp volumes gradually Start low, prioritise recipients who have saved your number or messaged you first, and grow over weeks — never launch a fresh number straight into a campaign.
Automatic protection SMESS paces every newly connected number with a warm-up ramp during its first week (shorter sending intervals unlock as the number matures). Slower early delivery is deliberate protection, not a malfunction.
Meta Verified Recommended: subscribe to Meta Verified in the WhatsApp Business app (Settings → Meta Verified, ∼US$11/month). You get the verified badge — stronger recipient trust — plus access to Meta support channels if your number is ever flagged.

Send Message

POST https://smess.io/api/send

Messages are submitted to a queue and delivered by the background worker. The API returns HTTP 202 Accepted on success, not 200.

Core Parameters
Name Type Required Description
apikey string Yes Your API authentication key. Also accepted as X-API-Key or Authorization: Bearer header. Not api_key.
recipient string Yes Phone number with country code (e.g., +233000000000). Any country is supported — see below. Not phone.
text string Yes* Message text or media caption. Not message. *At least one content field is required.
priority string/int No otp / urgent / high / normal (default) / bulk / low
idempotency_key string No Unique key per send attempt. Replaying the same key within 5 minutes returns the original response without sending a duplicate. Recommended for OTPs.
Recipient numbers — international sending

Every country is supported. A recipient in full international form (leading +) is validated against the E.164 standard and accepted worldwide — there is no country allowlist to request or maintain. A number in local form is resolved using your account's country setting.

AcceptedWhy it works
+233000000000Ghana — full international form
+96100000000Lebanon — the country does not need to be “enabled” anywhere
+44000000000United Kingdom
+10000000000United States / Canada
+97100000000
+4900000000000
+33000000000
UAE, Germany, France — and every other country code
+8600000000000
+5500000000000
China, Brazil — no configuration needed
0240000000Local form → becomes +233000000000 via your account country
233000000000Country code without + → normalised to +233000000000
+233 00 000 0000
+233-000-000000
Spaces, dashes and brackets are stripped automatically

These are rejected with HTTP 400 and error_code: INVALID_RECIPIENT:

RejectedWhy it fails
+233Country code only — no subscriber number
+0123456789E.164 numbers cannot start with 0 after the +
+12345Too short — E.164 requires 7–15 digits in total
+1234567890123456Too long — exceeds the 15-digit E.164 maximum
not-a-number
+44 (0) ABC
Contains letters
(empty)No recipient supplied
Accepted is not the same as delivered. SMESS validates the format; the number must also have an active WhatsApp account. Delivery problems (including “not on WhatsApp”) arrive through the delivery webhook with a reason — they are not send-time errors. Messaging brand-new international contacts is also subject to WhatsApp's own new-contact restrictions.
Message Type Auto-Detection

The endpoint detects the message type from the parameters you provide. Include only the parameters for the type you want to send:

TypeRequired parameter(s)
Texttext
Imagefile (image URL)
Documentdocument (file URL) + optional filename
Videovideo
Audioaudio
Locationlatitude + longitude + optional label
Contact cardcontact_name + contact_phone
Buttonsbutton1 + button1id (up to 3)
Copy code (OTP)copycode + optional copytext
List menulist_title + list_button + list_sections (JSON)
Example Request
curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "text=Hello from SMESS!"
import requests

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    data={
        "recipient": "+233000000000",
        "text": "Hello from SMESS!",
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    recipient: "+233000000000",
    text: "Hello from SMESS!",
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'recipient' => '+233000000000',
        'text' => 'Hello from SMESS!',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "recipient=%2B233000000000&text=Hello+from+SMESS%21";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("recipient", "+233000000000")
	form.Set("text", "Hello from SMESS!")

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(form.Encode()))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["recipient"] = "+233000000000",
    ["text"] = "Hello from SMESS!",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request.set_form_data(
  'recipient' => '+233000000000',
  'text' => 'Hello from SMESS!',
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)
Success Response (HTTP 202)
Success Response
{
    "success": true,
    "message": "Message queued successfully. It will be sent by the queue worker.",
    "data": {
        "queue_id": 42,
        "recipient": "+233000000000",
        "message_type": "text",
        "priority": 5,
        "priority_label": "normal",
        "status": "queued",
        "timestamp": "2026-06-16 10:30:00",
        "remaining_quota": 999,
        "note": "Message will be sent by queue worker with rate limiting and spam prevention"
    }
}

Message Types

All message types use the same POST /api/send endpoint. The type is auto-detected from the parameters you send.

Document
curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "document=https://example.com/invoice.pdf" \
  -d "filename=Invoice-2026.pdf" \
  -d "text=Here is your invoice."
import requests

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    data={
        "recipient": "+233000000000",
        "document": "https://example.com/invoice.pdf",
        "filename": "Invoice-2026.pdf",
        "text": "Here is your invoice.",
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    recipient: "+233000000000",
    document: "https://example.com/invoice.pdf",
    filename: "Invoice-2026.pdf",
    text: "Here is your invoice.",
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'recipient' => '+233000000000',
        'document' => 'https://example.com/invoice.pdf',
        'filename' => 'Invoice-2026.pdf',
        'text' => 'Here is your invoice.',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "recipient=%2B233000000000&document=https%3A%2F%2Fexample.com%2Finvoice.pdf&filename=Invoice-2026.pdf&text=Here+is+your+invoice.";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("recipient", "+233000000000")
	form.Set("document", "https://example.com/invoice.pdf")
	form.Set("filename", "Invoice-2026.pdf")
	form.Set("text", "Here is your invoice.")

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(form.Encode()))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["recipient"] = "+233000000000",
    ["document"] = "https://example.com/invoice.pdf",
    ["filename"] = "Invoice-2026.pdf",
    ["text"] = "Here is your invoice.",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request.set_form_data(
  'recipient' => '+233000000000',
  'document' => 'https://example.com/invoice.pdf',
  'filename' => 'Invoice-2026.pdf',
  'text' => 'Here is your invoice.',
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)
Upload the file instead of linking it

No public URL for the file? Upload it to SMESS first with POST /api/media (multipart/form-data, field file; the key goes in the apikey field or the X-API-Key header, exactly as for /api/send), then send using the media_id you get back. SMESS delivers the file as a document attachment (the recipient taps to open it, whatever the type) and destroys it shortly after delivery — only its name, size and checksum stay in your message history. One upload can be sent to many recipients (bulk included); it is removed once every message that uses it has gone out.

Precedence: when a request carries a media_id (or an uploaded file) together with document, file or a template's own file link, the upload wins and the others are ignored.

# 1. Upload (multipart) → media_id
curl -X POST https://smess.io/api/media \
  -F "apikey=SM-your-api-key" \
  -F "file=@/path/to/invoice.pdf"

# {"success":true,"media_id":"med_3f9a1c27b4d0e8f1","filename":"invoice.pdf","size_bytes":184320,
#  "mime":"application/pdf","kind":"ephemeral","expires_at":"2026-09-12 10:00:00","url":"https://smess.io/m/..."}

# 2. Send it (text becomes the caption; filename is optional)
curl -X POST https://smess.io/api/send \
  -d "apikey=SM-your-api-key" \
  -d "recipient=+233000000000" \
  -d "media_id=med_3f9a1c27b4d0e8f1" \
  -d "text=Here is your invoice."

# Or do both in one call: post the file straight to /api/send
curl -X POST https://smess.io/api/send \
  -F "apikey=SM-your-api-key" \
  -F "recipient=+233000000000" \
  -F "file=@/path/to/invoice.pdf" \
  -F "text=Here is your invoice."
ParameterTypeDescription
filemultipart fileAccepted: pdf, doc, docx, xls, xlsx, ppt, pptx, txt, csv, rtf, odt, ods, odp, zip, rar, 7z, gz, tar, jpg, jpeg, png, gif, webp, svg, mp4, mov, avi, mkv, mp3, ogg, wav, m4a. Size limit is your plan's attachment limit (see Rate Limits), never more than 50 MB per request. Executables and web scripts are refused whatever they are named.
media_idstringReturned by /api/media; pass it to /api/send or /api/bulk instead of document. Belongs to your account only.
persistentoptional1 keeps the file until you delete it or no template links to it any more (for a template you reuse). Default 0: destroyed about 15 minutes after the last message using it went out, 24 hours after upload if never used, and in any case 72 hours after upload unless a scheduled message still needs it.
actionoptionalstatus with a media_id returns whether the upload is still active or purged (and why). delete destroys it now. Default upload.
Lifecycle and limits. Sending a media_id after its file was destroyed returns 410 with MEDIA_PURGED and a message saying why — upload the file again. Each account may hold up to 500 MB or 200 not-yet-destroyed uploads at a time (STORAGE_QUOTA_EXCEEDED), and /api/media shares your plan's per-minute request limit (RATE_LIMITED). Upload errors: NO_FILE, EMPTY_FILE, UNSUPPORTED_TYPE, FILE_TOO_LARGE (413), UPLOAD_INCOMPLETE, MEDIA_NOT_FOUND (404 on send), MEDIA_UPLOAD_DISABLED (503).
Image
curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "file=https://example.com/photo.jpg" \
  -d "text=Check this out!"
import requests

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    data={
        "recipient": "+233000000000",
        "file": "https://example.com/photo.jpg",
        "text": "Check this out!",
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    recipient: "+233000000000",
    file: "https://example.com/photo.jpg",
    text: "Check this out!",
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'recipient' => '+233000000000',
        'file' => 'https://example.com/photo.jpg',
        'text' => 'Check this out!',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "recipient=%2B233000000000&file=https%3A%2F%2Fexample.com%2Fphoto.jpg&text=Check+this+out%21";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("recipient", "+233000000000")
	form.Set("file", "https://example.com/photo.jpg")
	form.Set("text", "Check this out!")

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(form.Encode()))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["recipient"] = "+233000000000",
    ["file"] = "https://example.com/photo.jpg",
    ["text"] = "Check this out!",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request.set_form_data(
  'recipient' => '+233000000000',
  'file' => 'https://example.com/photo.jpg',
  'text' => 'Check this out!',
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)
Buttons (up to 3)
curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "text=Confirm your delivery?" \
  -d "button1=Yes" \
  -d "button1id=confirm" \
  -d "button2=No" \
  -d "button2id=cancel"
import requests

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    data={
        "recipient": "+233000000000",
        "text": "Confirm your delivery?",
        "button1": "Yes",
        "button1id": "confirm",
        "button2": "No",
        "button2id": "cancel",
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    recipient: "+233000000000",
    text: "Confirm your delivery?",
    button1: "Yes",
    button1id: "confirm",
    button2: "No",
    button2id: "cancel",
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'recipient' => '+233000000000',
        'text' => 'Confirm your delivery?',
        'button1' => 'Yes',
        'button1id' => 'confirm',
        'button2' => 'No',
        'button2id' => 'cancel',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "recipient=%2B233000000000&text=Confirm+your+delivery%3F&button1=Yes&button1id=confirm&button2=No&button2id=cancel";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("recipient", "+233000000000")
	form.Set("text", "Confirm your delivery?")
	form.Set("button1", "Yes")
	form.Set("button1id", "confirm")
	form.Set("button2", "No")
	form.Set("button2id", "cancel")

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(form.Encode()))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["recipient"] = "+233000000000",
    ["text"] = "Confirm your delivery?",
    ["button1"] = "Yes",
    ["button1id"] = "confirm",
    ["button2"] = "No",
    ["button2id"] = "cancel",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request.set_form_data(
  'recipient' => '+233000000000',
  'text' => 'Confirm your delivery?',
  'button1' => 'Yes',
  'button1id' => 'confirm',
  'button2' => 'No',
  'button2id' => 'cancel',
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)
Copy Code (OTP)
curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "text=Your verification code is:" \
  -d "copycode=482916" \
  -d "copytext=Tap to Copy" \
  -d "priority=otp" \
  -d "idempotency_key=otp-user-123-1789191987"
import requests

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    data={
        "recipient": "+233000000000",
        "text": "Your verification code is:",
        "copycode": "482916",
        "copytext": "Tap to Copy",
        "priority": "otp",
        "idempotency_key": "otp-user-123-1789191987",
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    recipient: "+233000000000",
    text: "Your verification code is:",
    copycode: "482916",
    copytext: "Tap to Copy",
    priority: "otp",
    idempotency_key: "otp-user-123-1789191987",
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'recipient' => '+233000000000',
        'text' => 'Your verification code is:',
        'copycode' => '482916',
        'copytext' => 'Tap to Copy',
        'priority' => 'otp',
        'idempotency_key' => 'otp-user-123-1789191987',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "recipient=%2B233000000000&text=Your+verification+code+is%3A&copycode=482916&copytext=Tap+to+Copy&priority=otp&idempotency_key=otp-user-123-1789191987";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("recipient", "+233000000000")
	form.Set("text", "Your verification code is:")
	form.Set("copycode", "482916")
	form.Set("copytext", "Tap to Copy")
	form.Set("priority", "otp")
	form.Set("idempotency_key", "otp-user-123-1789191987")

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(form.Encode()))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["recipient"] = "+233000000000",
    ["text"] = "Your verification code is:",
    ["copycode"] = "482916",
    ["copytext"] = "Tap to Copy",
    ["priority"] = "otp",
    ["idempotency_key"] = "otp-user-123-1789191987",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request.set_form_data(
  'recipient' => '+233000000000',
  'text' => 'Your verification code is:',
  'copycode' => '482916',
  'copytext' => 'Tap to Copy',
  'priority' => 'otp',
  'idempotency_key' => 'otp-user-123-1789191987',
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)
Location
curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "latitude=5.6037" \
  -d "longitude=-0.1870" \
  -d "label=Accra Mall"
import requests

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    data={
        "recipient": "+233000000000",
        "latitude": "5.6037",
        "longitude": "-0.1870",
        "label": "Accra Mall",
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    recipient: "+233000000000",
    latitude: "5.6037",
    longitude: "-0.1870",
    label: "Accra Mall",
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'recipient' => '+233000000000',
        'latitude' => '5.6037',
        'longitude' => '-0.1870',
        'label' => 'Accra Mall',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "recipient=%2B233000000000&latitude=5.6037&longitude=-0.1870&label=Accra+Mall";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("recipient", "+233000000000")
	form.Set("latitude", "5.6037")
	form.Set("longitude", "-0.1870")
	form.Set("label", "Accra Mall")

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(form.Encode()))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["recipient"] = "+233000000000",
    ["latitude"] = "5.6037",
    ["longitude"] = "-0.1870",
    ["label"] = "Accra Mall",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request.set_form_data(
  'recipient' => '+233000000000',
  'latitude' => '5.6037',
  'longitude' => '-0.1870',
  'label' => 'Accra Mall',
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)
List Menu
curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "text=Please select a department:" \
  -d "list_title=Contact Us" \
  -d "list_button=View Departments" \
  -d 'list_sections=[{"title":"Sales","rows":[{"title":"New Orders","description":"Place a new order","rowId":"sales_new"}]}]'
import requests
import json

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    data={
        "recipient": "+233000000000",
        "text": "Please select a department:",
        "list_title": "Contact Us",
        "list_button": "View Departments",
        "list_sections": json.dumps([
          {
            "title": "Sales",
            "rows": [
              {
                "title": "New Orders",
                "description": "Place a new order",
                "rowId": "sales_new"
              }
            ]
          }
        ]),
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    recipient: "+233000000000",
    text: "Please select a department:",
    list_title: "Contact Us",
    list_button: "View Departments",
    list_sections: JSON.stringify([
      {
        "title": "Sales",
        "rows": [
          {
            "title": "New Orders",
            "description": "Place a new order",
            "rowId": "sales_new"
          }
        ]
      }
    ]),
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'recipient' => '+233000000000',
        'text' => 'Please select a department:',
        'list_title' => 'Contact Us',
        'list_button' => 'View Departments',
        'list_sections' => json_encode([
            [
                'title' => 'Sales',
                'rows' => [
                    [
                        'title' => 'New Orders',
                        'description' => 'Place a new order',
                        'rowId' => 'sales_new'
                    ]
                ]
            ]
        ]),
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "recipient=%2B233000000000&text=Please+select+a+department%3A&list_title=Contact+Us&list_button=View+Departments&list_sections=%5B%7B%22title%22%3A%22Sales%22%2C%22rows%22%3A%5B%7B%22title%22%3A%22New+Orders%22%2C%22description%22%3A%22Place+a+new+order%22%2C%22rowId%22%3A%22sales_new%22%7D%5D%7D%5D";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("recipient", "+233000000000")
	form.Set("text", "Please select a department:")
	form.Set("list_title", "Contact Us")
	form.Set("list_button", "View Departments")
	form.Set("list_sections", `[{"title":"Sales","rows":[{"title":"New Orders","description":"Place a new order","rowId":"sales_new"}]}]`)

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(form.Encode()))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["recipient"] = "+233000000000",
    ["text"] = "Please select a department:",
    ["list_title"] = "Contact Us",
    ["list_button"] = "View Departments",
    ["list_sections"] = @"[{""title"":""Sales"",""rows"":[{""title"":""New Orders"",""description"":""Place a new order"",""rowId"":""sales_new""}]}]",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request.set_form_data(
  'recipient' => '+233000000000',
  'text' => 'Please select a department:',
  'list_title' => 'Contact Us',
  'list_button' => 'View Departments',
  'list_sections' => [
    {
      'title' => 'Sales',
      'rows' => [
        {
          'title' => 'New Orders',
          'description' => 'Place a new order',
          'rowId' => 'sales_new'
        }
      ]
    }
  ].to_json,
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)

Message Templates

Templates are ready-made messages — text, media, buttons, list menus or copy codes — that you send by ID instead of rebuilding every time. Everything you can send, with its ID, placeholders and usage count, is listed on the Templates page.

Two kinds of template

Your account sees two shelves on that page, and the API treats them identically — both are sent with template_id, and nothing else about the call changes.

ShelfWho can send itWho can change it
SMESS Library Every SMESS account, yours included — nothing to set up, the IDs are ready to use today. SMESS. A library template is read-only for clients; use Save a copy on the Templates page to get an editable version of your own.
My templates Only your account. Another client can never see or send one of yours. You — create, edit, pause and delete them yourself on the Templates page.
You do not have to wait for us to build one. Start with a SMESS Library template as it is, save a copy and edit the wording, or write your own from scratch at Templates. A copy is a brand-new template of your own: it gets its own template_id and its own usage count, and editing it never touches the library original.
Sending a template

Sending one is an ordinary POST /api/send call with one extra parameter, template_id — the same call whether the ID belongs to the SMESS Library or to you. The template supplies the content (and therefore the message type); you supply the recipient and any placeholder values.

Templates apply to /api/send. The bulk endpoint /api/bulk does not accept template_id — it is ignored there. The portal's Bulk Sender does support templates: it submits each row individually to /api/send.

The template_id values in the examples below are illustrations only. Every ID is real for somebody, so copy the ones listed beside your templates on the Templates page rather than the numbers printed here.

Name Type Required Description
template_id int No ID of any template your account may send — one of your own or one from the SMESS Library, as shown on the Templates page. Omit it for a normal ad-hoc send.
variables JSON object No Placeholder values, e.g. {"customer_name":"Ama","order_id":"ORD-1"}. In a JSON request body it may be a real object instead of a string.
var_<name> string No A single placeholder value, e.g. var_customer_name=Ama. Merged over variables, so var_* wins when both supply the same name.

All three are accepted the same three ways as every other parameter: POST form field, query string, or JSON body.

Placeholders use single braces

Template content is written with {name} placeholders — Hi {customer_name}, your order {order_id} is ready. — and every occurrence is replaced with the value you supply. Substitution happens everywhere text appears in a template: the message body, video captions, button labels, and list titles and rows.

A placeholder you don't supply is left exactly as written — {order_id} stays {order_id}. SMESS never blanks it out, so a half-filled message is visible rather than silently wrong. Each template — library or your own — lists the names it expects on the Templates page.
Request values override template values

The template is a starting point, not a straitjacket. Anything you send explicitly wins over the template's own value, so one template can serve many messages:

What you sendWhat is delivered
text=… Your text is used — the template's own text is discarded.
(no text) The template's text is used, with its placeholders filled in.
text= (empty) Empty values never override — the template's text is used.

The same rule applies to every parameter, not just text: recipient, media URL, button labels, copy code and so on. Everything else behaves exactly as on a normal send — the merged values go through the same message-type auto-detection and the same validation. A buttons template therefore arrives as a buttons message without you sending button1, an image template carries its own file URL, and so on.

Text Template with Variables
curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "template_id=12" \
  -d 'variables={"customer_name":"Ama","order_id":"ORD-1"}'
import requests
import json

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    data={
        "recipient": "+233000000000",
        "template_id": "12",
        "variables": json.dumps({
          "customer_name": "Ama",
          "order_id": "ORD-1"
        }),
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    recipient: "+233000000000",
    template_id: "12",
    variables: JSON.stringify({
      "customer_name": "Ama",
      "order_id": "ORD-1"
    }),
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'recipient' => '+233000000000',
        'template_id' => '12',
        'variables' => json_encode([
            'customer_name' => 'Ama',
            'order_id' => 'ORD-1'
        ]),
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "recipient=%2B233000000000&template_id=12&variables=%7B%22customer_name%22%3A%22Ama%22%2C%22order_id%22%3A%22ORD-1%22%7D";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("recipient", "+233000000000")
	form.Set("template_id", "12")
	form.Set("variables", `{"customer_name":"Ama","order_id":"ORD-1"}`)

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(form.Encode()))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["recipient"] = "+233000000000",
    ["template_id"] = "12",
    ["variables"] = @"{""customer_name"":""Ama"",""order_id"":""ORD-1""}",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request.set_form_data(
  'recipient' => '+233000000000',
  'template_id' => '12',
  'variables' => {
    'customer_name' => 'Ama',
    'order_id' => 'ORD-1'
  }.to_json,
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)

The same send with individual fields instead of a JSON string — often easier from a form-encoded client:

curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "template_id=12" \
  -d "var_customer_name=Ama" \
  -d "var_order_id=ORD-1"
import requests

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    data={
        "recipient": "+233000000000",
        "template_id": "12",
        "var_customer_name": "Ama",
        "var_order_id": "ORD-1",
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    recipient: "+233000000000",
    template_id: "12",
    var_customer_name: "Ama",
    var_order_id: "ORD-1",
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'recipient' => '+233000000000',
        'template_id' => '12',
        'var_customer_name' => 'Ama',
        'var_order_id' => 'ORD-1',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "recipient=%2B233000000000&template_id=12&var_customer_name=Ama&var_order_id=ORD-1";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("recipient", "+233000000000")
	form.Set("template_id", "12")
	form.Set("var_customer_name", "Ama")
	form.Set("var_order_id", "ORD-1")

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(form.Encode()))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["recipient"] = "+233000000000",
    ["template_id"] = "12",
    ["var_customer_name"] = "Ama",
    ["var_order_id"] = "ORD-1",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request.set_form_data(
  'recipient' => '+233000000000',
  'template_id' => '12',
  'var_customer_name' => 'Ama',
  'var_order_id' => 'ORD-1',
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)

And as a JSON body, where variables is a real object:

curl -X POST https://smess.io/api/send \
  -H "Content-Type: application/json" \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d '{
        "recipient": "+233000000000",
        "template_id": 12,
        "variables": {
          "customer_name": "Ama",
          "order_id": "ORD-1"
        }
      }'
import requests

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    json={
        "recipient": "+233000000000",
        "template_id": 12,
        "variables": {
          "customer_name": "Ama",
          "order_id": "ORD-1"
        },
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "recipient": "+233000000000",
    "template_id": 12,
    "variables": {
      "customer_name": "Ama",
      "order_id": "ORD-1"
    }
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS     => json_encode([
        'recipient' => '+233000000000',
        'template_id' => 12,
        'variables' => [
            'customer_name' => 'Ama',
            'order_id' => 'ORD-1'
        ],
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = """
    {
      "recipient": "+233000000000",
      "template_id": 12,
      "variables": {
        "customer_name": "Ama",
        "order_id": "ORD-1"
      }
    }
    """;

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"strings"
)

func main() {
	body := `{
  "recipient": "+233000000000",
  "template_id": 12,
  "variables": {
    "customer_name": "Ama",
    "order_id": "ORD-1"
  }
}`

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(body))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var json = @"{
  ""recipient"": ""+233000000000"",
  ""template_id"": 12,
  ""variables"": {
    ""customer_name"": ""Ama"",
    ""order_id"": ""ORD-1""
  }
}";

var content = new StringContent(json, Encoding.UTF8, "application/json");

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request['Content-Type'] = 'application/json'
request.body = {
  'recipient' => '+233000000000',
  'template_id' => 12,
  'variables' => {
    'customer_name' => 'Ama',
    'order_id' => 'ORD-1'
  },
}.to_json

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)
Buttons Template

The buttons live in the template — you only fill in the placeholders:

curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "template_id=18" \
  -d "var_customer_name=Ama"

# Delivered as the template defines it, e.g.
#   Hi Ama, is 2pm tomorrow still good for your delivery?
#   [ Yes ]  [ No ]
import requests

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    data={
        "recipient": "+233000000000",
        "template_id": "18",
        "var_customer_name": "Ama",
    },
)

print(response.json())

# Delivered as the template defines it, e.g.
#   Hi Ama, is 2pm tomorrow still good for your delivery?
#   [ Yes ]  [ No ]
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    recipient: "+233000000000",
    template_id: "18",
    var_customer_name: "Ama",
  }),
});

console.log(await response.json());

// Delivered as the template defines it, e.g.
//   Hi Ama, is 2pm tomorrow still good for your delivery?
//   [ Yes ]  [ No ]
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'recipient' => '+233000000000',
        'template_id' => '18',
        'var_customer_name' => 'Ama',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));

// Delivered as the template defines it, e.g.
//   Hi Ama, is 2pm tomorrow still good for your delivery?
//   [ Yes ]  [ No ]
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "recipient=%2B233000000000&template_id=18&var_customer_name=Ama";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());

// Delivered as the template defines it, e.g.
//   Hi Ama, is 2pm tomorrow still good for your delivery?
//   [ Yes ]  [ No ]
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("recipient", "+233000000000")
	form.Set("template_id", "18")
	form.Set("var_customer_name", "Ama")

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(form.Encode()))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}

// Delivered as the template defines it, e.g.
//   Hi Ama, is 2pm tomorrow still good for your delivery?
//   [ Yes ]  [ No ]
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["recipient"] = "+233000000000",
    ["template_id"] = "18",
    ["var_customer_name"] = "Ama",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());

// Delivered as the template defines it, e.g.
//   Hi Ama, is 2pm tomorrow still good for your delivery?
//   [ Yes ]  [ No ]
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request.set_form_data(
  'recipient' => '+233000000000',
  'template_id' => '18',
  'var_customer_name' => 'Ama',
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)

# Delivered as the template defines it, e.g.
#   Hi Ama, is 2pm tomorrow still good for your delivery?
#   [ Yes ]  [ No ]

A single label can still be overridden per send — add -d "button1=Yes, 2pm works" and the rest of the template is untouched.

Overriding the Template Text
curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "template_id=18" \
  -d "text=Hi Ama, is 2pm tomorrow still good?"
import requests

response = requests.post(
    "https://smess.io/api/send",
    headers={"X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"},
    data={
        "recipient": "+233000000000",
        "template_id": "18",
        "text": "Hi Ama, is 2pm tomorrow still good?",
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/send", {
  method: "POST",
  headers: {
    "X-API-Key": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    recipient: "+233000000000",
    template_id: "18",
    text: "Hi Ama, is 2pm tomorrow still good?",
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/send');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'recipient' => '+233000000000',
        'template_id' => '18',
        'text' => 'Hi Ama, is 2pm tomorrow still good?',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "recipient=%2B233000000000&template_id=18&text=Hi+Ama%2C+is+2pm+tomorrow+still+good%3F";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/send"))
    .header("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("recipient", "+233000000000")
	form.Set("template_id", "18")
	form.Set("text", "Hi Ama, is 2pm tomorrow still good?")

	req, _ := http.NewRequest("POST", "https://smess.io/api/send", strings.NewReader(form.Encode()))
	req.Header.Set("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["recipient"] = "+233000000000",
    ["template_id"] = "18",
    ["text"] = "Hi Ama, is 2pm tomorrow still good?",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/send")
{
    Content = content,
};
request.Headers.Add("X-API-Key", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/send')

request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
request.set_form_data(
  'recipient' => '+233000000000',
  'template_id' => '18',
  'text' => 'Hi Ama, is 2pm tomorrow still good?',
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)

Your text replaces the template's body; the template's buttons still ride along.

Success Response (HTTP 202)
Success Response
{
    "success": true,
    "message": "Message queued successfully. It will be sent by the queue worker.",
    "data": {
        "queue_id": 42,
        "recipient": "+233000000000",
        "message_type": "buttons",
        "status": "queued",
        "template_id": 18
    }
}
Template Errors

A template_id that cannot be used is always an error — it never falls through to an empty message.

HTTPerror_codeMeaning
404 TEMPLATE_NOT_FOUND That ID is neither one of your own templates nor a SMESS Library template. Another client's private template is invisible to you, so their ID simply does not exist for your account.
404 TEMPLATE_INACTIVE The template exists but is switched off. Switch one of your own back on from the Templates page; for a SMESS Library template, ask us.
400 TEMPLATE_INVALID variables was not a JSON object, or the template resolved to no sendable content.
Error Response Example
{
    "success": false,
    "error": "Template not found for this account.",
    "error_code": "TEMPLATE_NOT_FOUND",
    "code": 404
}
Usage is counted on delivery. A template's usage count increases when a message built from it is actually delivered, not when it is queued — a queued message that later fails never inflates the count. The figure shown on the Templates page therefore reflects real sends, for library templates and your own alike.

Bulk Sending

For campaigns, submit many messages in one call with POST /api/bulk Premium plan and above. The queue worker paces delivery automatically (smart-sending gaps and caps still apply per recipient).

curl -X POST https://smess.io/api/bulk \
  -d "apikey=SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d 'messages=[{"recipient":"+233000000000","text":"Hello Ama!"},{"recipient":"+233000000000","text":"Hello Kojo!","document":"https://example.com/invoice.pdf"}]'
import requests
import json

response = requests.post(
    "https://smess.io/api/bulk",
    data={
        "apikey": "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "messages": json.dumps([
          {
            "recipient": "+233000000000",
            "text": "Hello Ama!"
          },
          {
            "recipient": "+233000000000",
            "text": "Hello Kojo!",
            "document": "https://example.com/invoice.pdf"
          }
        ]),
    },
)

print(response.json())
// Node.js 18+ — fetch is built in. Run this server-side only;
// never ship your API key to a browser.

const response = await fetch("https://smess.io/api/bulk", {
  method: "POST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    apikey: "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    messages: JSON.stringify([
      {
        "recipient": "+233000000000",
        "text": "Hello Ama!"
      },
      {
        "recipient": "+233000000000",
        "text": "Hello Kojo!",
        "document": "https://example.com/invoice.pdf"
      }
    ]),
  }),
});

console.log(await response.json());
<?php

$ch = curl_init('https://smess.io/api/bulk');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POSTFIELDS     => http_build_query([
        'apikey' => 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
        'messages' => json_encode([
            [
                'recipient' => '+233000000000',
                'text' => 'Hello Ama!'
            ],
            [
                'recipient' => '+233000000000',
                'text' => 'Hello Kojo!',
                'document' => 'https://example.com/invoice.pdf'
            ]
        ]),
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

print_r(json_decode($response, true));
// Java 17+ (java.net.http, text blocks)

HttpClient client = HttpClient.newHttpClient();

String body = "apikey=SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&messages=%5B%7B%22recipient%22%3A%22%2B233000000000%22%2C%22text%22%3A%22Hello+Ama%21%22%7D%2C%7B%22recipient%22%3A%22%2B233000000000%22%2C%22text%22%3A%22Hello+Kojo%21%22%2C%22document%22%3A%22https%3A%2F%2Fexample.com%2Finvoice.pdf%22%7D%5D";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://smess.io/api/bulk"))
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	form := url.Values{}
	form.Set("apikey", "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
	form.Set("messages", `[{"recipient":"+233000000000","text":"Hello Ama!"},{"recipient":"+233000000000","text":"Hello Kojo!","document":"https://example.com/invoice.pdf"}]`)

	req, _ := http.NewRequest("POST", "https://smess.io/api/bulk", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
using var client = new HttpClient();

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["apikey"] = "SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    ["messages"] = @"[{""recipient"":""+233000000000"",""text"":""Hello Ama!""},{""recipient"":""+233000000000"",""text"":""Hello Kojo!"",""document"":""https://example.com/invoice.pdf""}]",
});

var request = new HttpRequestMessage(HttpMethod.Post, "https://smess.io/api/bulk")
{
    Content = content,
};

var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://smess.io/api/bulk')

request = Net::HTTP::Post.new(uri)
request.set_form_data(
  'apikey' => 'SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
  'messages' => [
    {
      'recipient' => '+233000000000',
      'text' => 'Hello Ama!'
    },
    {
      'recipient' => '+233000000000',
      'text' => 'Hello Kojo!',
      'document' => 'https://example.com/invoice.pdf'
    }
  ].to_json,
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

puts JSON.parse(response.body)
ParameterTypeDescription
messages JSON array Array of {recipient, text, document} objects — each may carry media_id (an uploaded file) instead of document. Alternative: upload a CSV file as csv_file with header recipient, text, document, media_id.
media_id optional One uploaded file attached to every message in the batch that does not name its own media_id. Upload once with POST /api/media; SMESS destroys it after the whole batch has gone out.
campaign_id optional Your identifier for the batch — returned in the response and attached to every queued message. Auto-generated if omitted.
scheduled_at optional YYYY-MM-DD HH:MM:SS — queue now, deliver later. Great for campaigns timed to business hours.
priority optional Defaults to bulk priority (paced slower than transactional sends so they never block OTPs or receipts).

Response:

{
  "success": true,
  "queued": 2,
  "total_submitted": 2,
  "campaign_id": "spring-promo-01",
  "message": "2 messages queued for delivery"
}
Per-message failures don't abort the batch — invalid rows are reported in an errors array while the rest queue normally. Delivery outcomes arrive per message via Delivery Webhooks or the Queue page.

Track Messages

After queuing, track message status in the client portal or via the message detail API:

ResourceURLPurpose
Queue /my-queue Pending / processing / sent / failed messages
Message History /my-messages Delivered messages and delivery receipts
Note: Save the queue_id returned in the 202 response and use it to correlate with status updates in your dashboard or webhooks.

Delivery-Status Webhooks

Prefer push over polling? Set up a delivery-status webhook yourself in API Keys → Delivery Webhook: save your HTTPS endpoint, generate a signing secret, and hit Send test event to confirm your endpoint verifies correctly. SMESS then POSTs a JSON event whenever one of your messages reaches a final state:

{
  "event":     "message.status",
  "queue_id":  12345,
  "status":    "sent",
  "recipient": "233000000000",
  "error":     null,
  "attempts":  1,
  "timestamp": "2026-07-24T15:04:05+00:00"
}
DetailBehaviour
status sent or failed — on failure, error carries the reason (delivery error, spam-rule block, recipient cap, …)
Signature Every request carries X-SMESS-Signature: sha256=<hex> — the HMAC-SHA256 of the raw request body using your signing secret. Strip the sha256= prefix, hash the raw bytes before JSON parsing, and use a constant-time comparison — those three details cause most 401s.
Two credentials Your API key (SM-…) authenticates sends; the signing secret (whsec_…) verifies webhooks and is never transmitted. Keep them separate — signing with the API key means rotating it silently breaks your webhooks.
Rotating safely A new secret replaces the old one immediately and is shown only once. Deploy verification that accepts a list of secrets first, then rotate, confirm with Send test event, and drop the old value — otherwise events fail during the gap and are not replayed.
Delivery 5-second timeout, one automatic retry, best-effort — a webhook outage never delays your messages. Respond with any 2xx quickly; process async.
Held messages Gap/cap delays don't fire events — only final sent / failed outcomes do. Correlate using the queue_id from your send response.

Error Handling

The API returns JSON error responses with an HTTP status code and an error_code when available:

400 Bad Request — missing or invalid parameters (e.g., wrong phone format), or a template that produced nothing sendable (TEMPLATE_INVALID)
401 Unauthorized — API key missing or invalid (INVALID_KEY)
402 Payment Required — quota exceeded (QUOTA_EXCEEDED), trial allowance used up (TRIAL_QUOTA_EXCEEDED), or subscription/trial expired
403 Forbidden — key inactive/revoked (KEY_INACTIVE) or account suspended
404 Not Found — template_id is neither one of your own templates nor a SMESS Library one (TEMPLATE_NOT_FOUND), or it is switched off (TEMPLATE_INACTIVE) — see Message Templates; or media_id is not one of your uploads (MEDIA_NOT_FOUND)
410 Gone — the uploaded file behind media_id has already been destroyed (MEDIA_PURGED; the message says why) — upload it again, see Upload the file
413 Payload Too Large — media file or upload exceeds your plan limit (FILE_TOO_LARGE)
429 Too Many Requests — per-minute rate limit (RATE_LIMITED), per-recipient cooldown (OTP_COOLDOWN), or too many not-yet-destroyed uploads (STORAGE_QUOTA_EXCEEDED)
503 Service Unavailable — WhatsApp connection not ready (WHATSAPP_DISCONNECTED / NOT_ACTIVATED), or file uploads switched off (MEDIA_UPLOAD_DISABLED)
Error Response Example
{
    "success": false,
    "error": "Invalid API key",
    "error_code": "INVALID_KEY",
    "code": 401
}

Rate Limits & Safeguards

Limits are per API key and depend on your plan:

LimitStarter & StandardPremiumUltimate
API submissions / minute 100 300 500
Attachment max size (URL or upload) 25 MB 50 MB 50 MB
Per-recipient minimum gap 60 seconds (configurable 0–600s per key)
Per-recipient daily cap 3 / day by default — a safety limit, not a plan feature. Email support@smess.io to raise it for a real campaign or alert stream.
Why this cap exists: it stops the same person being messaged over and over — by a bug in your code, a retried test, or a runaway loop — before you notice. Hitting it looks like a batch of “Failed — daily limit reached, will retry tomorrow” in your Queue; it is this safeguard working as intended, not an attack or a broken account.
Bulk sending: For campaigns, use priority=bulk and submit via POST /api/bulk (Premium plan and above). The queue worker spaces out delivery automatically.

Troubleshooting

Common mistakes that cause API errors:

HTTP 401 — "API key is required" / "Invalid API key"

The server did not receive a valid key. Common causes:

❌ Wrong (will fail)✅ Correct
api_key (with underscore)apikey
key or tokenapikey / X-API-Key / Authorization: Bearer
Key nested inside another object, e.g. {"auth":{"apikey":"…"}}A top-level apikey field — JSON bodies (Content-Type: application/json) are fully supported, but only top-level fields are read
Query string ?apikey=...POST field or header (query string is deprecated and logged)
HTTP 400 — Missing Parameters
❌ Wrong (will fail)✅ Correct
phone, number, torecipient
message, msg, bodytext
image, photofile (images) or document (any file)
✅ Correct Example
curl -X POST https://smess.io/api/send \
  -H "X-API-Key: SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -d "recipient=+233000000000" \
  -d "text=Hello, this works!"
❌ Incorrect Example (will return 401)
# WRONG - these parameter names are NOT accepted
curl -X POST https://smess.io/api/send \
  -H "Content-Type: application/json" \
  -d '{"api_key":"SM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","phone":"+233000000000","message":"This will fail"}'