Quickstart
From no key to a delivered text in about ten minutes. Every example below comes in curl, Python, Node.js and PHP. Pick a language once and every example on the page switches.
1. Create a key
The API is included on Team Plus and up. A head coach or team director opens the dashboard, goes to Settings → Developers, and clicks Create key. The key starts with rt_live_ and is shown once, so copy it somewhere safe. It acts on behalf of the person who created it.
Keep the key out of your code. The Python, Node.js and PHP examples read it from an environment variable:
export RALLYTEXT_API_KEY="rt_live_your_key_here"
2. Check the key
GET /api/v1/team returns your team's name, number, plan and credits. Every request sends the key as Authorization: Bearer. A 401 invalid_key means the key is wrong or revoked. A 403 plan_required means the team is below Team Plus.
curl https://rallytext.app/api/v1/team \
-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"]}
resp = requests.get(API + "/team", headers=HEADERS, timeout=30)
resp.raise_for_status()
print(resp.json()["data"])
const API = "https://rallytext.app/api/v1";
const headers = { Authorization: `Bearer ${process.env.RALLYTEXT_API_KEY}` };
(async () => {
const resp = await fetch(`${API}/team`, { headers });
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
console.log((await resp.json()).data);
})();
<?php
$ch = curl_init("https://rallytext.app/api/v1/team");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("RALLYTEXT_API_KEY")],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) { throw new RuntimeException("HTTP $status: $body"); }
print_r(json_decode($body, true)["data"]);
3. Add a member
Numbers can be in any common US format, and RallyText stores them encrypted. If the number is already on your roster you get 409 member_exists with existing_member_id. If it texted STOP you get 409 opted_out, and only the member can opt back in, by texting START. Changes need a card on file (403 card_required).
curl -X POST https://rallytext.app/api/v1/members \
-H "Authorization: Bearer rt_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"name": "Jordan Lee", "phone": "(906) 555-1234", "role": "athlete"}'
import os
import requests
API = "https://rallytext.app/api/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["RALLYTEXT_API_KEY"]}
resp = requests.post(API + "/members", headers=HEADERS, timeout=30,
json={"name": "Jordan Lee", "phone": "(906) 555-1234", "role": "athlete"})
if resp.status_code == 409:
print("Not added:", resp.json()["error"]) # member_exists or opted_out
else:
resp.raise_for_status()
print("Added member", resp.json()["data"]["id"])
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}/members`, {
method: "POST",
headers,
body: JSON.stringify({ name: "Jordan Lee", phone: "(906) 555-1234", role: "athlete" }),
});
const body = await resp.json();
if (resp.status === 409) console.log("Not added:", body.error); // member_exists or opted_out
else if (!resp.ok) throw new Error(JSON.stringify(body.error));
else console.log("Added member", body.data.id);
})();
<?php
$ch = curl_init("https://rallytext.app/api/v1/members");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("RALLYTEXT_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode(["name" => "Jordan Lee", "phone" => "(906) 555-1234", "role" => "athlete"]),
CURLOPT_RETURNTRANSFER => true,
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status === 409) { print_r($body["error"]); } // member_exists or opted_out
elseif ($status >= 400) { throw new RuntimeException(json_encode($body["error"])); }
else { echo "Added member " . $body["data"]["id"] . "\n"; }
4. Send a text
audience is all, staff, families or group (with group_id). API sends are always broadcasts to one of those audiences. You can't text one person through the API. RallyText adds your team prefix and the opt-out line, and never texts anyone who opted out. If your team requires approval for this send, the response says pending_approval and the text waits in the dashboard's Approvals queue.
Two things only work from the dashboard: urgent alerts, and merge tags such as {first_name}. A send with merge tags returns 400 merge_tags_unsupported.
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";
5. See who got it
Use the id from a queued send. (A send waiting for approval has no message id yet, so id is null.) Delivery counts update as carriers report back. The delivery report lists each recipient with a masked number, such as ***1234, and the matching member ids.
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"; }
}
Next
- API reference: every endpoint, with try-it-out for GET requests.
- Postman collection: every endpoint, ready to run.
- Webhooks: get replies and opt-outs on your server as they happen.
- API overview: error codes, rate limits, and every endpoint on one page.