Webhooks
Gravity SMS uses two layers of webhooks: RingCentral-to-gateway (inbound SMS delivery) and gateway-to-app (fan-out to your applications). This guide explains the architecture and how to set up reliable webhook handling.
Webhook architecture
Two distinct webhook flows exist in the system:
- RingCentral → Gateway — RingCentral sends inbound SMS events to the gateway's webhook endpoint. The gateway handles handshake, verification, and event processing.
- Gateway → Your App — After processing an inbound SMS, the gateway fans out the message to every app in the tenant that has a
webhookUrlconfigured.
RingCentral webhook flow
When the gateway connects to RingCentral, it creates a webhook subscription for instant SMS events. RingCentral uses a two-phase verification process:
- Handshake — RingCentral sends a request with a
Validation-Tokenheader. The gateway echoes this header back in the response to confirm the endpoint. - Event delivery — Subsequent events include a
Verification-Tokenheader. The gateway verifies it against the stored token before processing.
App webhook fan-out
When an inbound SMS is processed, the gateway delivers it to your application:
- The gateway sends an HTTP POST to each app's
webhookUrlwith a JSON payload. - The request has a 5-second timeout. Return a 2xx response as quickly as possible and process the message asynchronously.
- If your endpoint is unreachable or times out, the message is not retried to your webhook but remains available via the messages API and WebSocket.
Inbound payload shape
{"type": "sms_inbound","messageId": "msg_a1b2c3d4e5f6","from": "+15559876543","to": "+15551234567","body": "Yes, I confirm","rcMessageId": "123456789","tenantId": "tenant_abc123def456abcd","timestamp": "2026-03-01T14:30:00.000Z"}
Configuring your webhook URL
Set the webhookUrl during app registration or update it afterwards:
curl -X PUT https://smsgateway-api.onrender.com/v1/apps/app_abc123def456abcd \-H "Authorization: Bearer YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{ "webhookUrl": "https://your-app.example.com/webhooks/sms" }'
To remove the webhook URL and stop receiving webhook deliveries:
curl -X PUT https://smsgateway-api.onrender.com/v1/apps/app_abc123def456abcd \-H "Authorization: Bearer YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{ "webhookUrl": null }'
Webhook URL requirements
| Requirement | Details |
|---|---|
| Protocol | HTTPS required in production. HTTP accepted in non-production environments. |
| Maximum length | 2,000 characters. |
| Format | Must be a valid URL. |
Testing webhooks
Use the test-webhook endpoint to send a test payload to your configured URL and verify it responds correctly:
curl -X POST https://smsgateway-api.onrender.com/v1/apps/app_abc123def456abcd/test-webhook \-H "Authorization: Bearer YOUR_API_KEY"
The response includes the HTTP status code and body returned by your endpoint.
Verifying webhook signatures
Every webhook delivery includes two headers that allow you to verify the request is genuine and hasn't been tampered with:
| Header | Description |
|---|---|
X-Gravity-Timestamp | Unix timestamp (seconds) when the webhook was sent. |
X-Gravity-Signature | HMAC-SHA256 signature prefixed with sha256=. |
Verification steps
- Extract headers — Read
X-Gravity-TimestampandX-Gravity-Signaturefrom the request. - Check timestamp freshness — Reject requests where the timestamp is more than 5 minutes old to prevent replay attacks.
- Compute expected signature — Build the signing payload as
timestamp + "." + rawBody, then computeHMAC-SHA256(webhookSecret, payload). - Compare signatures — Strip the
sha256=prefix from the header and use a timing-safe comparison against your computed signature.
import { createHmac, timingSafeEqual } from 'node:crypto';function verifyWebhookSignature(rawBody, timestamp, signatureHeader, webhookSecret) {// 1. Check timestamp freshness (5 minute window)const ts = parseInt(timestamp, 10);if (isNaN(ts) || Math.abs(Date.now() - ts * 1000) > 5 * 60 * 1000) {return false; // Stale or invalid timestamp}// 2. Compute expected signatureconst payload = timestamp + '.' + rawBody;const expected = createHmac('sha256', webhookSecret).update(payload).digest('hex');// 3. Compare using timing-safe comparisonconst signature = signatureHeader.startsWith('sha256=')? signatureHeader.slice(7): signatureHeader;const sigBuf = Buffer.from(signature, 'utf8');const expBuf = Buffer.from(expected, 'utf8');if (sigBuf.length !== expBuf.length) return false;return timingSafeEqual(sigBuf, expBuf);}
X-Gravity-Timestamp header before verifying the signature. The 5-minute window prevents attackers from replaying captured webhook payloads at a later time.webhookSecret (prefixed with whsec_) is returned once when the app is registered. Store it securely. You can rotate it via the rotate-webhook-secret endpoint if compromised.Best practices
- Respond quickly — Return a 2xx status within 5 seconds. Queue heavy processing for later.
- Handle duplicates — Use the
messageIdfield to deduplicate messages in your system. - Verify signatures — Always verify the
X-Gravity-Signatureheader before processing webhook payloads. - Use HTTPS — Always use TLS-secured endpoints in production.
- Fall back to polling — If webhooks fail, use
GET /v1/sms/messageswithdirection=inboundto retrieve missed messages.
Related docs
- Webhooks API Reference — RingCentral webhook endpoint
- Receiving SMS — Full inbound message flow
- Apps API Reference — Configure webhook URLs
- WebSocket Events — Alternative real-time delivery