Receive replies with webhooks

When someone texts your team number, RallyText can POST it to your server within about a minute. The same works for opt-outs, new members and delivery receipts. Webhooks are included on Team Plus and up.

1. Create a webhook

Add one in the dashboard under Settings → Developers, or with the API. Pick the events you want:

EventSent when
message.receivedSomeone texts your team number.
message.statusA text you sent is delivered or fails, once per recipient.
member.joinedSomeone joins your roster.
member.opted_outA member texts STOP, support opts them out for you, or your integration records an opt-out.
member.opted_inA member who opted out texts START.
webhook.testYou click Send test event in Settings → Developers. You can't subscribe to it.

The response includes a signing secret (whsec_...). It is shown once, so store it with your other secrets.

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"]}'

2. Verify every request

Each delivery is an HTTP POST with a JSON body and these headers:

HeaderValue
X-RallyText-EventThe event name, for example message.received.
X-RallyText-DeliveryA unique id for this delivery to this webhook.
X-RallyText-Signaturet=<unix seconds>,v1=<hex>

v1 is the hex HMAC-SHA256 of the timestamp, a period, and the raw body, keyed with your whole secret as UTF-8 text, whsec_ prefix included. Don't base64-decode the secret. Check the signature over the exact bytes you received, before parsing JSON. Compare in constant time, and reject timestamps more than 5 minutes from now. Each retry is signed again with a fresh timestamp, so a retried delivery still passes.

# Compute the signature RallyText would send, to compare with X-RallyText-Signature.
# Set SECRET (your whole whsec_... secret), BODY (the exact raw body) and T (the t= value).
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

3. Handle the event

The body is an envelope: id (the event id), type, created_at, team (id and code) and data. For message.received, data has from, to, body, keyword, group_id, member (or null if the number isn't on your roster) and received_at.

Reply with any 2xx within 10 seconds. Redirects are not followed. Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then that delivery is dropped. Because of retries, the same event can arrive twice, so ignore event ids you have already handled. A retried event can also arrive after newer ones, so sort on created_at if order matters. 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.

# Send your own endpoint a signed test event (SECRET as in the signature example).
BODY='{"id":"evt_test","type":"webhook.test","data":{"message":"hello"}}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST https://example.com/rallytext-webhook \
  -H "Content-Type: application/json" \
  -H "X-RallyText-Event: webhook.test" \
  -H "X-RallyText-Signature: t=$T,v1=$SIG" \
  -d "$BODY"

What you can do with replies

Webhooks tell you about replies, but your code can't answer them through the API: replies go from the dashboard or a staff phone, which keeps Safe Sport rules in place. With the API you can mark texts read (POST /api/v1/inbox/{message_id}/read) so staff know they've been handled. Every event and field is listed in the webhooks reference.