How to Use the PulseSMS API: Send SMS, Launch Campaigns & Manage Devices
A practical guide to authenticating with the PulseSMS REST API, connecting your Android gateway device, creating bulk campaigns, and sending individual messages — with real code examples.
PulseSMS exposes a clean REST API that lets you send SMS messages, launch bulk campaigns, and monitor your gateway devices programmatically — from any language or platform. This guide walks through the three most common tasks with copy-paste ready examples.
The full interactive reference is at pulse-sms.com/reference.
Before you start
You'll need:
- A PulseSMS account (sign up free)
- An API key — find it under Settings → API Keys in your dashboard (it starts with
pk_live_...) - At least one Android device connected as a gateway (more on this below)
Step 1 — Authenticate and get a token
Every API request is authenticated with a short-lived JWT Bearer token. Exchange your API key for one with a single POST /api/auth/token:
curl -X POST https://pulse-sms.com/api/auth/token \
-H "Content-Type: application/json" \
-d '{"apiKey": "pk_live_4f8a2c1d9e3b7f6a0c5e2d8b4a9f1c3e"}'Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdJZCI6Im9yZ18wMXh5ejEyMyIsInN1YiI6ImFwaUtleV8wMWFiYyIsImlhdCI6MTc0MzUwMDAwMCwiZXhwIjoxNzQzNTg2NDAwfQ.abc123sig",
"expiresIn": 86400,
"organizationId": "org_01xyz123"
}Tokens are valid for 24 hours. Cache the token and reuse it — don't generate a new one on every request.
TypeScript — reusable API client
Build a small client module once and import it everywhere. Store the token in memory and refresh it only when it expires.
// lib/pulse.ts
const BASE_URL = "https://pulse-sms.com";
interface TokenCache {
token: string;
expiresAt: number;
}
let cache: TokenCache | null = null;
async function getToken(): Promise<string> {
if (cache && Date.now() < cache.expiresAt - 60_000) {
return cache.token;
}
const res = await fetch(`${BASE_URL}/api/auth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ apiKey: process.env.PULSE_API_KEY }),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`PulseSMS auth failed: ${error}`);
}
const { token, expiresIn } = await res.json();
cache = { token, expiresAt: Date.now() + expiresIn * 1000 };
return token;
}
async function pulseRequest<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const token = await getToken();
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`PulseSMS ${options.method ?? "GET"} ${path}: ${error}`);
}
return res.json();
}
export { pulseRequest };Python — reusable session
# pulse.py
import os
import time
import requests
BASE_URL = "https://pulse-sms.com"
class PulseClient:
def __init__(self, api_key: str):
self._api_key = api_key
self._token: str | None = None
self._expires_at: float = 0
self._session = requests.Session()
def _get_token(self) -> str:
if self._token and time.time() < self._expires_at - 60:
return self._token
resp = self._session.post(
f"{BASE_URL}/api/auth/token",
json={"apiKey": self._api_key},
)
resp.raise_for_status()
data = resp.json()
self._token = data["token"]
self._expires_at = time.time() + data["expiresIn"]
return self._token
def get(self, path: str) -> dict:
resp = self._session.get(
f"{BASE_URL}{path}",
headers={"Authorization": f"Bearer {self._get_token()}"},
)
resp.raise_for_status()
return resp.json()
def post(self, path: str, body: dict) -> dict:
resp = self._session.post(
f"{BASE_URL}{path}",
json=body,
headers={"Authorization": f"Bearer {self._get_token()}"},
)
resp.raise_for_status()
return resp.json()
# Usage
pulse = PulseClient(api_key=os.environ["PULSE_API_KEY"])Step 2 — Check your gateway devices
Before sending any SMS, confirm at least one Android device is online. The /api/devices endpoint lists all registered gateways.
curl https://pulse-sms.com/api/devices \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."Response:
[
{
"id": "dvc_01a2b3c4d5e6f7g8",
"deviceName": "Pixel 8 Pro — Office SIM",
"phoneNumber": "+14155552671",
"model": "Pixel 8 Pro",
"osVersion": "15",
"appVersion": "1.4.2",
"status": "online",
"smsSent": 1847,
"lastSeenAt": "2026-04-01T10:58:33Z"
},
{
"id": "dvc_09h8g7f6e5d4c3b2",
"deviceName": "Samsung A55 — Backup",
"phoneNumber": "+14155558342",
"model": "Samsung Galaxy A55",
"osVersion": "14",
"appVersion": "1.4.2",
"status": "offline",
"smsSent": 234,
"lastSeenAt": "2026-03-29T14:12:05Z"
}
]If no device has "status": "online" the send request will fail with a 400. Connect one first.
How to connect an Android device
- Download the PulseSMS Gateway APK from pulse-sms.com/download
- Install and open it on your Android phone
- In your dashboard go to Devices → Connect device
- Scan the QR code shown in the dashboard with the app
- Your device appears in the list with
status: onlinewithin seconds
TypeScript — assert a device is available
// lib/pulse.ts (continued)
interface Device {
id: string;
deviceName: string;
phoneNumber: string;
status: "online" | "offline";
smsSent: number;
lastSeenAt: string;
}
export async function getOnlineDevice(): Promise<Device> {
const devices = await pulseRequest<Device[]>("/api/devices");
const online = devices.find((d) => d.status === "online");
if (!online) {
throw new Error(
"No online gateway device. Open the PulseSMS app on your Android phone."
);
}
return online;
}Step 3 — Send a message
For transactional or one-off sends to 1–500 recipients, use POST /api/sms/send. No contact list required.
curl -X POST https://pulse-sms.com/api/sms/send \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"recipients": [
{ "phone": "+14155552671", "name": "Alice" }
],
"message": "Your PulseSMS verification code is 482910. It expires in 10 minutes."
}'Response:
{
"campaignId": "cmp_01abc9xyz7kq2000",
"recipientCount": 1,
"message": "Campaign created with 1 recipients"
}Batch send (up to 500 recipients)
curl -X POST https://pulse-sms.com/api/sms/send \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"recipients": [
{ "phone": "+14155552671", "name": "Alice" },
{ "phone": "+14155558342", "name": "Bob" },
{ "phone": "+14155550192", "name": "Carol" }
],
"message": "Flash sale! 50% off everything today only. Shop now: https://example.com/sale"
}'Limits: 1–1,600 characters per message. Rate limited to 10 requests/minute. Free accounts cap at 100 SMS/month — the
429response includes the remaining count.
TypeScript — typed send helper
// lib/pulse.ts (continued)
interface Recipient {
phone: string;
name?: string;
}
interface SendResult {
campaignId: string;
recipientCount: number;
message: string;
}
export async function sendSMS(
recipients: Recipient[],
message: string
): Promise<SendResult> {
if (recipients.length === 0) throw new Error("At least one recipient is required");
if (recipients.length > 500) throw new Error("Maximum 500 recipients per request");
if (!message.trim()) throw new Error("Message body cannot be empty");
return pulseRequest<SendResult>("/api/sms/send", {
method: "POST",
body: JSON.stringify({ recipients, message }),
});
}
// Example — send an OTP
const result = await sendSMS(
[{ phone: "+14155552671", name: "Alice" }],
"Your verification code is 482910. It expires in 10 minutes."
);
console.log(`Queued as campaign ${result.campaignId}`);Python — send a single SMS
# Using the PulseClient from Step 1
result = pulse.post("/api/sms/send", {
"recipients": [
{"phone": "+14155552671", "name": "Alice"},
],
"message": "Your verification code is 482910. It expires in 10 minutes.",
})
print(f"Queued: {result['campaignId']} ({result['recipientCount']} recipient)")Step 4 — Launch a bulk campaign
Campaigns target a contact list created in the dashboard (or imported via CSV). They support {{firstName}} and {{lastName}} template variables for personalisation.
curl -X POST https://pulse-sms.com/api/campaigns \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"name": "Spring Sale 2026",
"message": "Hi {{firstName}}, enjoy 30% off storewide. Use code SPRING30 at checkout: https://example.com/sale",
"contactListId": "cl_01abc9xyz7kq2000"
}'Response:
{
"id": "cmp_01xyz9abc4kq8000",
"name": "Spring Sale 2026",
"status": "running",
"totalMessages": 248,
"sentCount": 0,
"deliveredCount": 0,
"failedCount": 0,
"contactList": {
"name": "Newsletter subscribers",
"contactCount": 248
},
"createdAt": "2026-04-01T11:00:00Z",
"updatedAt": "2026-04-01T11:00:00Z"
}Schedule for later
Pass scheduledAt as an ISO 8601 timestamp to queue the campaign instead of launching immediately. The campaign stays in draft until the scheduled time.
curl -X POST https://pulse-sms.com/api/campaigns \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"name": "Spring Sale 2026 — Scheduled",
"message": "Hi {{firstName}}, our Spring Sale starts NOW. 30% off everything: https://example.com/sale",
"contactListId": "cl_01abc9xyz7kq2000",
"scheduledAt": "2026-04-05T09:00:00Z"
}'TypeScript — launch campaign helper
// lib/pulse.ts (continued)
interface Campaign {
id: string;
name: string;
status: "draft" | "scheduled" | "running" | "paused" | "completed" | "cancelled";
totalMessages: number;
sentCount: number;
deliveredCount: number;
failedCount: number;
createdAt: string;
}
interface LaunchCampaignOptions {
name: string;
message: string;
contactListId: string;
scheduledAt?: string; // ISO 8601 — omit to launch immediately
}
export async function launchCampaign(
options: LaunchCampaignOptions
): Promise<Campaign> {
return pulseRequest<Campaign>("/api/campaigns", {
method: "POST",
body: JSON.stringify(options),
});
}
// Example usage
const campaign = await launchCampaign({
name: "Spring Sale 2026",
message:
"Hi {{firstName}}, enjoy 30% off storewide. Use code SPRING30: https://example.com/sale",
contactListId: "cl_01abc9xyz7kq2000",
});
console.log(`Campaign ${campaign.id} is ${campaign.status}`);
console.log(`Sending to ${campaign.totalMessages} contacts`);Step 5 — Track delivery status
Use the campaign ID returned in Steps 3 or 4 to fetch delivery stats and individual message statuses.
curl https://pulse-sms.com/api/campaigns/cmp_01xyz9abc4kq8000 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."Response:
{
"id": "cmp_01xyz9abc4kq8000",
"name": "Spring Sale 2026",
"status": "completed",
"totalMessages": 3,
"sentCount": 3,
"deliveredCount": 2,
"failedCount": 1,
"startedAt": "2026-04-01T11:00:03Z",
"completedAt": "2026-04-01T11:00:41Z",
"messages": [
{
"id": "msg_01aaa111bbb222cc",
"contactPhone": "+14155552671",
"contactName": "Alice",
"body": "Hi Alice, enjoy 30% off storewide. Use code SPRING30.",
"status": "delivered",
"sentAt": "2026-04-01T11:00:05Z",
"deliveredAt": "2026-04-01T11:00:09Z"
},
{
"id": "msg_01bbb222ccc333dd",
"contactPhone": "+14155558342",
"contactName": "Bob",
"body": "Hi Bob, enjoy 30% off storewide. Use code SPRING30.",
"status": "delivered",
"sentAt": "2026-04-01T11:00:07Z",
"deliveredAt": "2026-04-01T11:00:13Z"
},
{
"id": "msg_01ccc333ddd444ee",
"contactPhone": "+14155550192",
"contactName": "Carol",
"body": "Hi Carol, enjoy 30% off storewide. Use code SPRING30.",
"status": "failed",
"error": "Unreachable number",
"sentAt": "2026-04-01T11:00:09Z",
"deliveredAt": null
}
]
}Message status lifecycle: queued → sending → sent → delivered (or failed).
TypeScript — poll until complete
// lib/pulse.ts (continued)
interface Message {
id: string;
contactPhone: string;
contactName: string | null;
body: string;
status: "queued" | "sending" | "sent" | "delivered" | "failed";
sentAt: string | null;
deliveredAt: string | null;
error: string | null;
}
interface CampaignDetail extends Campaign {
messages: Message[];
}
const TERMINAL_STATUSES = new Set(["completed", "cancelled"]);
export async function waitForCampaign(
campaignId: string,
pollIntervalMs = 3000
): Promise<CampaignDetail> {
while (true) {
const detail = await pulseRequest<CampaignDetail>(
`/api/campaigns/${campaignId}`
);
const { status, deliveredCount, failedCount, totalMessages } = detail;
console.log(
`[${campaignId}] ${status} — ${deliveredCount} delivered, ${failedCount} failed / ${totalMessages} total`
);
if (TERMINAL_STATUSES.has(status)) return detail;
await new Promise((r) => setTimeout(r, pollIntervalMs));
}
}
// Full flow
const { campaignId } = await sendSMS(
[{ phone: "+14155552671", name: "Alice" }],
"Your order #8821 has shipped. Track it: https://example.com/track/8821"
);
const result = await waitForCampaign(campaignId);
console.log(result.status); // "completed"Error reference
| HTTP status | When it happens | What to do |
|---|---|---|
400 | Validation error, empty recipient list, no online device | Read the error field — fix the request |
401 | Token expired or invalid API key | Re-authenticate via POST /api/auth/token |
403 | No active subscription | Upgrade your plan in the dashboard |
404 | Campaign or contact list not found | Double-check the ID |
429 | Rate limit or monthly SMS cap hit | Back off and retry; upgrade if at monthly cap |
All error responses follow the same shape:
{
"error": "No online devices. Connect at least one device to send SMS.",
"details": "Check the Devices page in your dashboard."
}Full end-to-end example
Here's the complete flow — authenticate, verify a device is online, launch a campaign, and wait for completion — in both TypeScript and Python.
TypeScript
import { getOnlineDevice, launchCampaign, waitForCampaign } from "./lib/pulse";
async function run() {
// 1. Assert gateway is ready
const device = await getOnlineDevice();
console.log(`Using device: ${device.deviceName} (${device.phoneNumber})`);
// 2. Launch campaign
const campaign = await launchCampaign({
name: "Spring Sale 2026",
message:
"Hi {{firstName}}, enjoy 30% off storewide. Use code SPRING30: https://example.com/sale",
contactListId: "cl_01abc9xyz7kq2000",
});
console.log(`Campaign ${campaign.id} launched — ${campaign.totalMessages} messages`);
// 3. Wait for completion
const result = await waitForCampaign(campaign.id);
console.log(`Done: ${result.deliveredCount} delivered, ${result.failedCount} failed`);
}
run().catch(console.error);Python
import os
import time
from pulse import PulseClient
pulse = PulseClient(api_key=os.environ["PULSE_API_KEY"])
# 1. Assert a device is online
devices = pulse.get("/api/devices")
online = [d for d in devices if d["status"] == "online"]
if not online:
raise SystemExit("No online gateway device found. Open the PulseSMS app.")
print(f"Using: {online[0]['deviceName']} ({online[0]['phoneNumber']})")
# 2. Launch campaign
campaign = pulse.post("/api/campaigns", {
"name": "Spring Sale 2026",
"message": "Hi {{firstName}}, enjoy 30% off storewide. Use code SPRING30: https://example.com/sale",
"contactListId": "cl_01abc9xyz7kq2000",
})
print(f"Campaign {campaign['id']} launched — {campaign['totalMessages']} messages")
# 3. Poll until complete
terminal = {"completed", "cancelled"}
while True:
time.sleep(3)
detail = pulse.get(f"/api/campaigns/{campaign['id']}")
print(
f" {detail['status']} — "
f"{detail['deliveredCount']} delivered, "
f"{detail['failedCount']} failed / "
f"{detail['totalMessages']} total"
)
if detail["status"] in terminal:
break
print("Done.")Next steps
- Webhooks — instead of polling, subscribe to
message.delivered,message.failed, andcampaign.completedevents. Set them up under Settings → Webhooks in your dashboard. - Interactive docs — try every endpoint live in your browser at pulse-sms.com/reference.
- Contact lists — import a CSV from the dashboard to build lists you can target with the campaign API.
If you run into anything, use the Feedback button in your dashboard and we'll get back to you.
Want more SMS gateway tips?
Get practical guides on running free SMS campaigns from your own number — delivered to your inbox.
No credit card required. Unsubscribe any time.