Skip to content
All posts
apidevelopercampaignsSMSguideintegration

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 TeamApril 1, 2026
How to Use the PulseSMS API: Send SMS, Launch Campaigns & Manage Devices

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:

bash
curl -X POST https://pulse-sms.com/api/auth/token \
  -H "Content-Type: application/json" \
  -d '{"apiKey": "pk_live_4f8a2c1d9e3b7f6a0c5e2d8b4a9f1c3e"}'

Response:

json
{
  "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.

typescript
// 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

python
# 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.

bash
curl https://pulse-sms.com/api/devices \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Response:

json
[
  {
    "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.

PulseSMS devices page showing a connected Android gateway

How to connect an Android device

  1. Download the PulseSMS Gateway APK from pulse-sms.com/download
  2. Install and open it on your Android phone
  3. In your dashboard go to Devices → Connect device
  4. Scan the QR code shown in the dashboard with the app
  5. Your device appears in the list with status: online within seconds

TypeScript — assert a device is available

typescript
// 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.

bash
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:

json
{
  "campaignId": "cmp_01abc9xyz7kq2000",
  "recipientCount": 1,
  "message": "Campaign created with 1 recipients"
}

Batch send (up to 500 recipients)

bash
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 429 response includes the remaining count.

TypeScript — typed send helper

typescript
// 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

python
# 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.

bash
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:

json
{
  "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.

bash
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

typescript
// 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`);

Campaign dashboard showing running status and delivery counts


Step 5 — Track delivery status

Use the campaign ID returned in Steps 3 or 4 to fetch delivery stats and individual message statuses.

bash
curl https://pulse-sms.com/api/campaigns/cmp_01xyz9abc4kq8000 \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Response:

json
{
  "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

typescript
// 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 statusWhen it happensWhat to do
400Validation error, empty recipient list, no online deviceRead the error field — fix the request
401Token expired or invalid API keyRe-authenticate via POST /api/auth/token
403No active subscriptionUpgrade your plan in the dashboard
404Campaign or contact list not foundDouble-check the ID
429Rate limit or monthly SMS cap hitBack off and retry; upgrade if at monthly cap

All error responses follow the same shape:

json
{
  "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

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

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, and campaign.completed events. 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.