Real-time event notifications for your NPS workflow
RallyNPS webhooks deliver real-time HTTP POST notifications to your server whenever key events happen in your account. Use webhooks to pipe survey data into your CRM, trigger alerts, create tickets, or build custom automation.
Each webhook delivery is signed with HMAC-SHA256 so you can verify it came from RallyNPS. Failed deliveries are retried once after 5 seconds. After 3 consecutive failures, the webhook is automatically deactivated.
Every webhook payload includes a version field (string) that
indicates the payload schema version. The current version is
"1". This field is automatically injected by RallyNPS
and is always present in every event delivery.
{
"version": "1",
"surveyId": "a1b2c3d4-...",
// ... other event-specific fields
}
When we introduce breaking changes to a payload schema, the version
number will increment. Your integration should check the
version field and handle unknown versions gracefully
(e.g., log and skip). Existing fields will not be removed or renamed
within the same version.
| Field | Type | Description |
|---|---|---|
version |
string | Payload schema version. Currently "1". Present in all events. |
version field before
processing a webhook payload. This lets you safely handle future schema
changes without breaking your integration.
Fired when a new survey response is submitted (via email link or shareable URL).
Payload:{
"version": "1",
"surveyId": "a1b2c3d4-...",
"responseId": "e5f6g7h8-...",
"score": 8,
"hasComment": true,
"surveyType": "nps",
"timestamp": "2026-04-13T14:30:00.000Z"
}
| Field | Type | Description |
|---|---|---|
version | string | Payload schema version (currently "1") |
surveyId | UUID | The survey this response belongs to |
responseId | UUID | Unique response identifier |
score | integer | Normalized score (0-10) |
hasComment | boolean | Whether the response includes a text comment |
surveyType | string | Survey type: nps, csat, ces, star5, thumbs, etc. (employee-NPS flavor is nps with type_config.audience='employee') |
timestamp | ISO 8601 | When the response was recorded |
Fired when a response with a detractor-level score is received. Detractor thresholds vary by survey type (NPS: 0-6, CSAT: 1-2, etc.).
Payload:{
"version": "1",
"surveyId": "a1b2c3d4-...",
"responseId": "e5f6g7h8-...",
"score": 3,
"comment": "The onboarding was confusing and I had to...",
"timestamp": "2026-04-13T14:30:00.000Z"
}
| Field | Type | Description |
|---|---|---|
version | string | Payload schema version (currently "1") |
surveyId | UUID | The survey this response belongs to |
responseId | UUID | Unique response identifier |
score | integer | The detractor score |
comment | string | null | First 100 characters of the comment, PII-redacted. Null if no comment. |
timestamp | ISO 8601 | When the response was recorded |
comment field is automatically
redacted — emails, phone numbers, and names are replaced with
placeholders like [email], [phone],
[name] before delivery.
Fired when a survey is marked as completed (all contacts responded or manually closed).
Payload:{
"version": "1",
"surveyId": "a1b2c3d4-...",
"name": "Q1 2026 Customer Survey",
"responseCount": 142,
"nps": 42,
"completionRate": 71.0,
"timestamp": "2026-04-13T14:30:00.000Z"
}
| Field | Type | Description |
|---|---|---|
version | string | Payload schema version (currently "1") |
surveyId | UUID | The completed survey |
name | string | Survey name |
responseCount | integer | Total responses collected |
nps | integer | null | Final NPS score (-100 to 100), null if no responses |
completionRate | number | Percentage of contacts who responded |
timestamp | ISO 8601 | When the survey was completed |
Fired when an AI executive summary report is generated.
Payload:{
"version": "1",
"reportType": "executive-summary",
"nps": 42,
"topThemes": ["shipping", "support", "pricing"],
"timestamp": "2026-04-13T14:30:00.000Z"
}
| Field | Type | Description |
|---|---|---|
version | string | Payload schema version (currently "1") |
reportType | string | Type of report generated (e.g. "executive-summary") |
nps | integer | null | NPS score for the report period |
topThemes | string[] | Up to 5 top themes from responses |
timestamp | ISO 8601 | When the report was generated |
Every webhook delivery includes these headers:
| Header | Description | Example |
|---|---|---|
X-RallyNPS-Signature |
HMAC-SHA256 hex digest of the request body, signed with your webhook secret | a1b2c3d4e5... |
X-RallyNPS-Event |
The event type that triggered this delivery | response.received |
X-RallyNPS-Delivery-Id |
Unique delivery ID (UUID) for deduplication | f47ac10b-58cc-... |
Content-Type |
Always JSON | application/json |
Always verify the X-RallyNPS-Signature header to confirm
the delivery came from RallyNPS. The signature is a hex-encoded
HMAC-SHA256 digest of the raw request body, using your webhook
secret as the key.
const crypto = require('crypto');
function verifySignature(secret, body, signature) {
const expected = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signature, 'hex')
);
}
// In your Express/http handler:
const sig = req.headers['x-rallynps-signature'];
const rawBody = /* raw request body as string */;
if (!verifySignature(WEBHOOK_SECRET, rawBody, sig)) {
return res.status(401).json({ error: 'Invalid signature' });
}
import hmac
import hashlib
def verify_signature(secret: str, body: bytes, signature: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# In your Flask/FastAPI handler:
sig = request.headers.get('X-RallyNPS-Signature', '')
if not verify_signature(WEBHOOK_SECRET, request.data, sig):
return {"error": "Invalid signature"}, 401
Create a Slack Incoming Webhook URL, then set up a small server
(or serverless function) that receives detractor.detected
events and forwards them to Slack.
// Minimal Node.js handler — receives RallyNPS webhook, posts to Slack
const http = require('http');
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;
http.createServer(async (req, res) => {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const body = Buffer.concat(chunks).toString();
// Verify signature first (see above)
const event = req.headers['x-rallynps-event'];
if (event === 'detractor.detected') {
const data = JSON.parse(body);
await fetch(SLACK_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `Detractor alert: score ${data.score} on survey ${data.surveyId}\n${data.comment || '(no comment)'}`
})
});
}
res.writeHead(200);
res.end('ok');
}).listen(3000);
Subscribe to response.received and create a Linear
issue for every response that includes a comment.
// Receives response.received, creates Linear issue if there's a comment
const LINEAR_API_KEY = process.env.LINEAR_API_KEY;
const TEAM_ID = process.env.LINEAR_TEAM_ID;
async function handleWebhook(body) {
const data = JSON.parse(body);
if (!data.hasComment) return; // skip score-only responses
await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': LINEAR_API_KEY,
},
body: JSON.stringify({
query: `mutation {
issueCreate(input: {
teamId: "${TEAM_ID}"
title: "NPS Response (Score: ${data.score})"
description: "Survey: ${data.surveyId}\\nScore: ${data.score}\\nType: ${data.surveyType}"
priority: ${data.score <= 6 ? 1 : 3}
}) { success issue { id url } }
}`
})
});
}
Use the RallyNPS API to create, update, list, and delete webhook subscriptions.
POST /api/webhooks
{
"org_id": "your-org-id",
"url": "https://your-server.com/webhooks/rallynps",
"events": ["response.received", "detractor.detected"]
}
// Response includes a `secret` — store it securely for signature verification.
GET /api/webhooks/events
// Returns:
{
"events": [
{ "type": "response.received", "description": "..." },
{ "type": "detractor.detected", "description": "..." },
{ "type": "survey.completed", "description": "..." },
{ "type": "summary.generated", "description": "..." }
]
}
POST /api/webhooks/:id/test
// Sends a test event to your URL so you can verify connectivity and signature checking.
GET /api/webhooks/:id/deliveries
// Returns the last 50 deliveries with status codes and response bodies.
PUT /api/webhooks/:id with
{"active": true}.