RallyText API
Manage your roster and groups, send and schedule texts, and read delivery status from your own systems. The API is included on Team Plus and up: Team Plus, Organization and Business plans, monthly or season. Teams on lower plans get plan_required.
Authentication
A head coach or team director creates keys in the dashboard under Settings → Developers. A key is shown once. It acts on behalf of the person who created it. If that person stops being a head coach or team director on the team, or loses permission to send broadcasts, the key is revoked for good and its scheduled messages are not sent. Giving them the role back does not bring the key back. If their roster entry is blocked, the key stops working completely, reads included (sender_blocked). The key is not revoked: it works again once they are unblocked. You can revoke a key at any time, on any plan.
Authorization: Bearer rt_live_your_key_here
All requests and responses are JSON. Every path below starts with https://rallytext.app/api/v1. Paths have no trailing slash. Machine-readable description: /api/v1/openapi.json. Postman users can download a collection: set api_key to your key.
Responses and errors
Successful responses wrap the result in data. Lists also return pagination (limit up to 200, offset, has_more, next_offset). Errors look like this:
{"error": {"code": "plan_required", "message": "..."}}
Some errors add fields, for example existing_member_id on member_exists.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request, invalid_json, bad_request, invalid_phone, invalid_email, invalid_role, role_not_allowed, invalid_group, read_only_field, invalid_scheduled_for, invalid_repeat, keyword_required, merge_tags_unsupported, private_field, urgent_not_allowed, unknown_field | Something in the request needs fixing; the message says what. Merge tags such as {first_name} only work from the dashboard, so the API refuses them (merge_tags_unsupported). A tag for a field the team marked Private is refused with private_field. Staff roles can only be given from the dashboard (role_not_allowed), and a member's status can't be set (read_only_field). |
| 401 | unauthorized, invalid_key | No key, or the key is unknown or revoked. |
| 402 | payment_required, trial_limit | Sending is paused for billing, or a trial message would go past the team's remaining trial credits. |
| 403 | plan_required, key_owner_inactive, team_disabled, team_locked, org_hub_unsupported, card_required, sender_blocked, staff_member_protected, template_protected, private_field_locked, forbidden | The team or key can't do this. Changes need a card on file (card_required). A key whose creator is blocked on the roster is refused on every request until they are unblocked (sender_blocked). Staff, and any member who has a login, can only be changed from the dashboard (staff_member_protected). Urgent alert templates can only be changed in the dashboard (template_protected). A private custom field can only be made public or deleted in the dashboard (private_field_locked). |
| 404 | not_found | No such item on your team. Ids from another team also return this. |
| 405 | method_not_allowed | The endpoint doesn't support that HTTP method. |
| 409 | member_exists, opted_out, opted_out_member, opted_out_guardian, guardian_protected, no_sender, scheduled_limit_reached, template_exists, template_limit, field_exists, field_limit, campaign_state, conflict | Conflicts with your roster or limits. A member who texted STOP can only opt back in by texting START to your team number; the API can't re-subscribe them. |
| 413 | payload_too_large | The request body is too large. |
| 415 | unsupported_media_type | Send JSON with Content-Type: application/json. |
| 429 | rate_limited | Slow down and retry. |
| 5xx | server_error | Something went wrong on our side. Retry later. |
Rate limits
Each key gets 120 requests per minute across all endpoints. Sends on POST /api/v1/messages, scheduled ones included, are limited to 20 per minute and 200 per hour per key. Requests without a valid key are limited to 60 requests per minute per IP address. Over a limit you get 429 rate_limited. Bulk opt-outs, recipient checks, delivery reports and analytics also share a limit of 10 per minute per key.
What the API does and does not do
/api/v1/messagesand/api/v1/scheduledcover broadcasts only. Urgent alerts can't be sent through the API; use the dashboard.- Sends follow the same rules as the dashboard: approval policies, opt-outs, and the team prefix and opt-out text RallyText adds to each message.
- Members who texted STOP are never texted and can't be re-subscribed through the API: not by editing status, changing their number, adding the number again, or deleting and re-adding the member. You can record opt-outs with the API, but you can't remove them.
- Staff (head coach, assistant coach, coach, team director), board members, and anyone with a dashboard login are managed in the dashboard. The API returns
staff_member_protectedfor them. - A team can have up to 100 pending scheduled messages created through the API.
- Urgent alert templates can't be created, changed, deleted or sent through the API.
- Values in private custom fields can be written through the API but are never returned by it.
- Drip campaigns are created in the dashboard. The API can list, pause, resume and cancel them.
- The API can read the inbox and mark texts read, but it can't reply. Replies go from the dashboard or from a staff phone.
Endpoints
| Endpoint (full path) | What it does |
|---|---|
GET /api/v1/team | Team name, code, number, plan, and credits used and included. |
GET /api/v1/members | List members. Filters: group_id, role, status, phone (any common US format), email (not case-sensitive). Add sort=-created_at for newest first. |
POST /api/v1/members | Add a member: name, phone, email, role (athlete, guardian, outside, emergency_contact), group_ids. |
GET /api/v1/members/{member_id} | One member. |
PATCH /api/v1/members/{member_id} | Change name, phone, email, role or groups. Status is read-only. Staff are managed in the dashboard. |
DELETE /api/v1/members/{member_id} | Remove a member. Members who opted out are kept so their opt-out stays on record. |
GET /api/v1/groups | List groups. |
POST /api/v1/groups/{group_id}/members | Add a member to a group: {"member_id": "..."}. |
DELETE /api/v1/groups/{group_id}/members/{member_id} | Remove a member from a group. |
GET /api/v1/custom-fields | Your custom roster fields (the {key} merge tags), with private. |
POST /api/v1/custom-fields | Add a field: label, optional key and private. Up to 20 per team. |
PATCH /api/v1/custom-fields/{field_id} | Rename a field, or make it private. Only the dashboard can make a private field public. |
DELETE /api/v1/custom-fields/{field_id} | Delete a field and every member's value for it. Private fields can only be deleted in the dashboard. |
GET /api/v1/members/{member_id}/custom-fields | A member's values. Private fields are never returned. |
PATCH /api/v1/members/{member_id}/custom-fields | Set values: {"values": {"jersey_number": "12"}}. Private fields can be written but not read back. Staff are managed in the dashboard. |
POST /api/v1/messages | Send now, or schedule with scheduled_for. audience: all, staff, families or group (with group_id). Or send template_id instead of text. Repeat weekly with "repeat": {"every": "week", "until": "2026-12-31"}. |
POST /api/v1/messages/estimate | Same body as a send. Returns recipients, segments per text, estimated credits and whether approval is needed. Sends nothing. |
GET /api/v1/messages | Sent messages with delivery counts. |
GET /api/v1/messages/{message_id} | One message with delivery counts. |
GET /api/v1/messages/{message_id}/deliveries | Who got the message: one row per recipient with status and carrier error code. Numbers are masked (***1234); member_ids tells you who it was. |
GET /api/v1/analytics | Totals for from/to (UTC dates, default the last 30 days, up to 366): broadcasts, deliveries, delivery rate, texts received, new members, opt-outs. |
GET /api/v1/inbound-messages | Texts people sent to your team number, newest first. Same fields as the message.received webhook event. Texts that arrive while a renewal payment is overdue are held and show up here, and in message.received, once payment goes through. |
GET /api/v1/inbox | The inbox your staff see: received texts with read_at, replied_at and the latest staff reply. filter: all, unread or replied. |
GET /api/v1/inbox/unread-count | How many texts are unread. |
POST /api/v1/inbox/{message_id}/read | Mark one text read. Staff see the change in the dashboard. |
POST /api/v1/inbox/read-all | Mark every text read. |
GET /api/v1/scheduled | Pending scheduled messages. |
DELETE /api/v1/scheduled/{scheduled_id} | Cancel a scheduled message. For a weekly series, this ends the series. |
GET /api/v1/campaigns | Your drip campaigns with their steps and status. |
GET /api/v1/campaigns/{campaign_id} | One campaign. |
POST /api/v1/campaigns/{campaign_id}/pause | Pause a scheduled campaign. |
POST /api/v1/campaigns/{campaign_id}/resume | Resume a paused campaign. Campaigns with an urgent alert step are resumed from the dashboard. |
POST /api/v1/campaigns/{campaign_id}/cancel | Cancel a campaign. Messages not sent yet won't go out. |
GET /api/v1/templates | Your saved message templates. |
POST /api/v1/templates | Save a template: name, body, message_type (not urgent_alert). Reusing a deleted template's name brings it back. Up to 200 per team. |
GET /api/v1/templates/{template_id} | One template. |
PATCH /api/v1/templates/{template_id} | Change a template's name, body or type. Urgent alert templates are managed in the dashboard. |
DELETE /api/v1/templates/{template_id} | Delete a template. |
GET /api/v1/opt-outs | Members who texted STOP. |
POST /api/v1/opt-outs | Record an opt-out: {"phone": "..."} or {"member_id": "..."}. Every roster entry with that number is opted out for good, and the member gets no text. Works on any plan. There is no way to undo it through the API: only the member can opt back in, by texting START. |
POST /api/v1/opt-outs/bulk | Up to 500 opt-outs at once, for example the unsubscribe list from your old texting service. Each item gets its own result. |
POST /api/v1/recipients/validate | Check up to 100 numbers: valid or not, already on your roster, or opted out. Changes nothing. |
GET /api/v1/webhooks | Your webhooks (signing secrets are never returned). |
POST /api/v1/webhooks | Subscribe a URL to events: {"url": "https://...", "events": ["member.joined"]}. Returns the signing secret once. See Webhooks. |
DELETE /api/v1/webhooks/{webhook_id} | Stop sending to a webhook and delete it, including webhooks added in the dashboard. |
Examples
Add a member
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"}'
Send to everyone
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"}'
A message that needs approval under your team's policy waits in the dashboard's Approvals queue instead of going out.
Schedule for later
curl -X POST https://rallytext.app/api/v1/messages \
-H "Authorization: Bearer rt_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"text": "Reminder: picture day tomorrow.", "audience": "families", "scheduled_for": "2026-10-20T17:00:00-04:00"}'
scheduled_for is ISO 8601 with a timezone offset, from one minute to one year ahead.
Repeat every week
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 at 6.", "audience": "all", "scheduled_for": "2026-10-20T17:00:00-04:00", "repeat": {"every": "week", "until": "2026-12-15"}}'
Each week is checked and sent like a new message. If a week is refused (for example the key was revoked), the series stops. A week that waits in the approval queue counts as sent, so the next week is still scheduled even if that approval is rejected or expires.
Webhooks
Webhooks send events to your server, or to Zapier, as they happen. A head coach or team director adds them in the dashboard under Settings → Developers, or you can add them with POST /api/v1/webhooks. Each webhook has a signing secret that is shown once, when you create it. Webhooks are included on Team Plus and up: Team Plus, Organization and Business plans. A team can have up to 25.
Events
| Event | Sent when | data fields |
|---|---|---|
message.received | Someone texts your team number. to is the number they texted. | message_id, provider_message_id, from, to, body, keyword, group_id, member (id, name, role, or null if the number is not on your roster), received_at |
message.status | A text you sent is delivered or fails, once per recipient. | message_id, delivery_id, provider_message_id, to, status (usually delivered or failed; delivery_unconfirmed when the carrier never confirmed delivery), carrier_status, error_code, error_message, updated_at |
member.joined | Someone joins your roster: added in the dashboard, through the API or an import, signs up by text or on the web, or is approved from Pending. | member: id, name, phone, email, role, status, is_blocked, created_at |
member.opted_out | A member texts STOP, RallyText support opts a member out for you, or your integration records an opt-out with POST /api/v1/opt-outs. | member, as above |
member.opted_in | A member who opted out texts START. | member, as above |
webhook.test | You click Send test event in Settings → Developers. Sent only to that webhook, and you can't subscribe to it. | message |
Phone numbers are in E.164 format (for example +19065551234) and belong to your own members. A member who is on your roster twice (for example a parent of two athletes) gets one member.* event per roster entry.
The request
RallyText sends an HTTP POST with a JSON body and these headers:
| Header | Value |
|---|---|
X-RallyText-Event | The event name, for example message.received. |
X-RallyText-Delivery | A unique id for this delivery. The same event goes to each of your webhooks with its own delivery id. |
X-RallyText-Signature | t=<unix seconds>,v1=<signature> |
{"id": "evt_3f0c9a...", "type": "message.received", "created_at": "2026-10-08T14:03:11.402311Z",
"team": {"id": "6b1d...", "code": "HAWKS"},
"data": {"message_id": "a41e...", "provider_message_id": "40317f...", "from": "+19065551234",
"to": "+19065550000", "body": "Running 10 minutes late", "keyword": null, "group_id": null,
"member": {"id": "c9d2...", "name": "Jordan Lee", "role": "athlete"},
"received_at": "2026-10-08T14:03:11.398004Z"}}
Verifying the signature
v1 is the hex HMAC-SHA256 of the timestamp, a period, and the raw request body, using your whole webhook secret as the key: the full whsec_... string, prefix included, as UTF-8 text. Do not base64-decode it (some other webhook libraries do). Compute it over the exact bytes you received, before parsing the JSON. Compare in constant time, and reject requests whose timestamp is more than 5 minutes old.
X-RallyText-Signature: t=1760000000,v1=5f2b6c...
import hashlib, hmac, time
def verify(secret, header, body, tolerance=300):
try:
parts = dict(p.split("=", 1) for p in header.split(","))
t, sig = parts["t"], parts["v1"]
if abs(time.time() - int(t)) > tolerance:
return False
except (KeyError, ValueError):
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
Responses and retries
Reply with any 2xx status within 10 seconds. Redirects are not followed. If a delivery fails, RallyText tries again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then gives up on that delivery. Deliveries go out once a minute, so expect up to a minute's delay. The same event can arrive more than once, so use the event id to ignore duplicates. After 20 failed attempts in a row, the webhook is turned off and your head coaches and team directors get an email. Turn it back on in Settings → Developers once your endpoint works.
Plan changes and access
If your team moves below Team Plus, nothing is sent. Events that happen while you are below Team Plus are marked skipped and are not sent later, even after you upgrade again. You can still list and delete your webhooks, in the dashboard or with GET and DELETE /api/v1/webhooks, so an integration can unsubscribe.
Webhooks added with an API key are deleted when that key is revoked, including when the key is revoked because the person who created it is no longer a head coach or team director. Webhooks added in the dashboard are turned off when the person who added them is no longer a head coach or team director, or loses permission to send broadcasts. The webhook then shows disabled_reason creator_inactive, and another head coach or team director can turn it back on. Whoever turns it back on becomes the person it belongs to.
URL rules
Webhook URLs must use https:// and must resolve to a public internet address. URLs that point to private, loopback or link-local addresses are refused when you save them (blocked_url) and again before every delivery. Other mistakes in the URL return invalid_url, and an empty or unknown event list returns invalid_events. Past 25 webhooks you get webhook_limit. If we cannot look up the host name right away, you get a 503 dns_busy; try again in a minute.
Add a webhook with the API
curl -X POST https://rallytext.app/api/v1/webhooks \
-H "Authorization: Bearer rt_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/rallytext-webhook", "events": ["message.received", "member.opted_out"]}'
Response (201): the webhook, including "secret": "whsec_...". Store the secret; it is not shown again. Webhooks added with a key are deleted if that key is revoked, including when it is revoked because its creator stopped being a head coach or team director. Webhooks added in the dashboard are not affected.
Help
Questions: support@rallytext.app.