Send your first message
Check what a text will cost, send it, schedule it, and see who got it. You need a key first: see the Quickstart.
Pick an audience
Every API send is a broadcast to one audience. You can't text a single person through the API.
all: your whole team, the same people an ALL text from the dashboard reaches.families: athletes and their parents or guardians, no staff.staff: coaches and team directors.groupwithgroup_id: one group. List your groups withGET /api/v1/groups.
Urgent alerts aren't available through the API. Send those from the dashboard.
Check the cost first
POST /api/v1/messages/estimate takes the same body as a send and runs the same checks, but sends nothing, and it doesn't count toward the send limit. segments_per_recipient includes your team prefix and the opt-out line. So estimated_credits is an upper bound for your roster as it is now: the real send can use a little less, but more only if people join before you send.
curl -X POST https://rallytext.app/api/v1/messages/estimate \
-H "Authorization: Bearer rt_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"text": "Game moved to Field 3, 10am.", "audience": "families"}'
import os
import requests
API = "https://rallytext.app/api/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["RALLYTEXT_API_KEY"]}
resp = requests.post(API + "/messages/estimate", headers=HEADERS, timeout=30,
json={"text": "Game moved to Field 3, 10am.", "audience": "families"})
resp.raise_for_status()
est = resp.json()["data"]
print(est["expected_recipients"], "people x", est["segments_per_recipient"], "segments =",
est["estimated_credits"], "credits (at most)")
print("Needs approval:", est["needs_approval"])
print("Credits used this period:", est["credits"]["used"], "of", est["credits"]["limit"])
const API = "https://rallytext.app/api/v1";
const headers = { Authorization: `Bearer ${process.env.RALLYTEXT_API_KEY}`, "Content-Type": "application/json" };
(async () => {
const resp = await fetch(`${API}/messages/estimate`, {
method: "POST",
headers,
body: JSON.stringify({ text: "Game moved to Field 3, 10am.", audience: "families" }),
});
const body = await resp.json();
if (!resp.ok) throw new Error(JSON.stringify(body.error));
const data = body.data;
console.log(`${data.expected_recipients} people, at most ${data.estimated_credits} credits, needs approval ${data.needs_approval}`);
})();
<?php
$ch = curl_init("https://rallytext.app/api/v1/messages/estimate");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("RALLYTEXT_API_KEY"), "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["text" => "Game moved to Field 3, 10am.", "audience" => "families"]),
CURLOPT_RETURNTRANSFER => true,
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
if (isset($body["error"])) { throw new RuntimeException(json_encode($body["error"])); }
$data = $body["data"];
echo "{$data["expected_recipients"]} people, at most {$data["estimated_credits"]} credits\n";
Send it
curl -X POST https://rallytext.app/api/v1/messages \
-H "Authorization: Bearer rt_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"text": "Practice moved to 5pm today.", "audience": "all"}'
import os
import requests
API = "https://rallytext.app/api/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["RALLYTEXT_API_KEY"]}
resp = requests.post(API + "/messages", headers=HEADERS, timeout=30,
json={"text": "Practice moved to 5pm today.", "audience": "all"})
resp.raise_for_status()
sent = resp.json()["data"]
# status is queued (id is the message id) or pending_approval (id is null)
print(sent["status"], sent["id"], sent["expected_recipients"])
const API = "https://rallytext.app/api/v1";
const headers = {
Authorization: `Bearer ${process.env.RALLYTEXT_API_KEY}`,
"Content-Type": "application/json",
};
(async () => {
const resp = await fetch(`${API}/messages`, {
method: "POST",
headers,
body: JSON.stringify({ text: "Practice moved to 5pm today.", audience: "all" }),
});
const body = await resp.json();
if (!resp.ok) throw new Error(JSON.stringify(body.error));
console.log(body.data.status, body.data.id, body.data.expected_recipients);
})();
<?php
$ch = curl_init("https://rallytext.app/api/v1/messages");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("RALLYTEXT_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode(["text" => "Practice moved to 5pm today.", "audience" => "all"]),
CURLOPT_RETURNTRANSFER => true,
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
if (isset($body["error"])) { throw new RuntimeException(json_encode($body["error"])); }
echo $body["data"]["status"] . " " . $body["data"]["id"] . "\n";
You can also send a saved template with "template_id" instead of "text". The API refuses urgent alert templates (urgent_not_allowed). Merge tags such as {first_name} only work from the dashboard: a send that contains them, typed or from a template, returns 400 merge_tags_unsupported. If your team requires approval, the send comes back pending_approval and waits in the dashboard's Approvals queue.
Schedule it, or repeat it every week
scheduled_for needs a timezone offset and can be from 1 minute to 1 year ahead. repeat sends the same text every week until the date you give (a date in your team's time zone, up to a year ahead). Each week is checked like a new send when it goes out, including your team's approval rules. If a week is refused, for example because the key was revoked, the series stops. A week that waits for approval counts as sent, so the next week is scheduled even if that approval is rejected. List scheduled texts with GET /api/v1/scheduled, and cancel one with DELETE /api/v1/scheduled/{scheduled_id}, which also ends its series.
curl -X POST https://rallytext.app/api/v1/messages \
-H "Authorization: Bearer rt_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"text": "Practice tonight 6-7:30 at Lincoln Field.", "audience": "all",
"scheduled_for": "2027-03-02T15:00:00-05:00",
"repeat": {"every": "week", "until": "2027-05-25"}}'
import os
import requests
API = "https://rallytext.app/api/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["RALLYTEXT_API_KEY"]}
resp = requests.post(API + "/messages", headers=HEADERS, timeout=30, json={
"text": "Practice tonight 6-7:30 at Lincoln Field.",
"audience": "all",
"scheduled_for": "2027-03-02T15:00:00-05:00",
"repeat": {"every": "week", "until": "2027-05-25"},
})
resp.raise_for_status()
scheduled = resp.json()["data"]
print("Scheduled", scheduled["id"], "first send", scheduled["send_at"], scheduled["repeat"])
const API = "https://rallytext.app/api/v1";
const headers = { Authorization: `Bearer ${process.env.RALLYTEXT_API_KEY}`, "Content-Type": "application/json" };
(async () => {
const resp = await fetch(`${API}/messages`, {
method: "POST",
headers,
body: JSON.stringify({
text: "Practice tonight 6-7:30 at Lincoln Field.",
audience: "all",
scheduled_for: "2027-03-02T15:00:00-05:00",
repeat: { every: "week", until: "2027-05-25" },
}),
});
const body = await resp.json();
if (!resp.ok) throw new Error(JSON.stringify(body.error));
console.log("Scheduled", body.data.id, "first send", body.data.send_at, body.data.repeat);
})();
<?php
$ch = curl_init("https://rallytext.app/api/v1/messages");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("RALLYTEXT_API_KEY"), "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode([
"text" => "Practice tonight 6-7:30 at Lincoln Field.",
"audience" => "all",
"scheduled_for" => "2027-03-02T15:00:00-05:00",
"repeat" => ["every" => "week", "until" => "2027-05-25"],
]),
CURLOPT_RETURNTRANSFER => true,
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
if (isset($body["error"])) { throw new RuntimeException(json_encode($body["error"])); }
echo "Scheduled " . $body["data"]["id"] . " first send " . $body["data"]["send_at"] . "\n";
See who got it
Use the message id from a queued send. Numbers in the delivery report are masked, with the matching member ids so you can join them to your own records.
curl https://rallytext.app/api/v1/messages/MESSAGE_ID \
-H "Authorization: Bearer rt_live_your_key_here"
curl "https://rallytext.app/api/v1/messages/MESSAGE_ID/deliveries?limit=200" \
-H "Authorization: Bearer rt_live_your_key_here"
import os
import requests
API = "https://rallytext.app/api/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["RALLYTEXT_API_KEY"]}
message_id = "MESSAGE_ID"
resp = requests.get(f"{API}/messages/{message_id}", headers=HEADERS, timeout=30)
resp.raise_for_status()
print(resp.json()["data"]["deliveries"]) # total, queued, sent, delivered, failed
resp = requests.get(f"{API}/messages/{message_id}/deliveries", headers=HEADERS,
params={"limit": 200}, timeout=30)
resp.raise_for_status()
for row in resp.json()["data"]:
if row["bucket"] == "failed":
print("Not delivered:", row["to"], row["member_ids"], row["error_code"])
const API = "https://rallytext.app/api/v1";
const headers = { Authorization: `Bearer ${process.env.RALLYTEXT_API_KEY}` };
const messageId = "MESSAGE_ID";
(async () => {
const msg = await (await fetch(`${API}/messages/${messageId}`, { headers })).json();
console.log(msg.data.deliveries);
const report = await (await fetch(`${API}/messages/${messageId}/deliveries?limit=200`, { headers })).json();
for (const row of report.data) {
if (row.bucket === "failed") console.log("Not delivered:", row.to, row.member_ids, row.error_code);
}
})();
<?php
function rt_get(string $path): array {
$ch = curl_init("https://rallytext.app/api/v1" . $path);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("RALLYTEXT_API_KEY")],
CURLOPT_RETURNTRANSFER => true,
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
if (isset($body["error"])) { throw new RuntimeException(json_encode($body["error"])); }
return $body;
}
$messageId = "MESSAGE_ID";
print_r(rt_get("/messages/$messageId")["data"]["deliveries"]);
foreach (rt_get("/messages/$messageId/deliveries?limit=200")["data"] as $row) {
if ($row["bucket"] === "failed") { echo "Not delivered: {$row["to"]} {$row["error_code"]}\n"; }
}