RallyNPS Webhooks

Real-time event notifications for your NPS workflow

Overview

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.

Payload Versioning

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.

FieldTypeDescription
version string Payload schema version. Currently "1". Present in all events.
Tip: Always check the version field before processing a webhook payload. This lets you safely handle future schema changes without breaking your integration.

Available Events

response.received

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"
}
FieldTypeDescription
versionstringPayload schema version (currently "1")
surveyIdUUIDThe survey this response belongs to
responseIdUUIDUnique response identifier
scoreintegerNormalized score (0-10)
hasCommentbooleanWhether the response includes a text comment
surveyTypestringSurvey type: nps, csat, ces, star5, thumbs, etc. (employee-NPS flavor is nps with type_config.audience='employee')
timestampISO 8601When the response was recorded

detractor.detected

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"
}
FieldTypeDescription
versionstringPayload schema version (currently "1")
surveyIdUUIDThe survey this response belongs to
responseIdUUIDUnique response identifier
scoreintegerThe detractor score
commentstring | nullFirst 100 characters of the comment, PII-redacted. Null if no comment.
timestampISO 8601When the response was recorded
PII Safety: The comment field is automatically redacted — emails, phone numbers, and names are replaced with placeholders like [email], [phone], [name] before delivery.

survey.completed

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"
}
FieldTypeDescription
versionstringPayload schema version (currently "1")
surveyIdUUIDThe completed survey
namestringSurvey name
responseCountintegerTotal responses collected
npsinteger | nullFinal NPS score (-100 to 100), null if no responses
completionRatenumberPercentage of contacts who responded
timestampISO 8601When the survey was completed

summary.generated

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"
}
FieldTypeDescription
versionstringPayload schema version (currently "1")
reportTypestringType of report generated (e.g. "executive-summary")
npsinteger | nullNPS score for the report period
topThemesstring[]Up to 5 top themes from responses
timestampISO 8601When the report was generated

Request Headers

Every webhook delivery includes these headers:

HeaderDescriptionExample
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

Signature Verification

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.

Node.js

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' });
}

Python

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

Integration Examples

Pipe detractor alerts to Slack

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);

Create a Linear ticket from a response

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 } }
      }`
    })
  });
}

Managing Webhooks

Use the RallyNPS API to create, update, list, and delete webhook subscriptions.

Create a webhook

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.

List available events

GET /api/webhooks/events

// Returns:
{
  "events": [
    { "type": "response.received", "description": "..." },
    { "type": "detractor.detected", "description": "..." },
    { "type": "survey.completed", "description": "..." },
    { "type": "summary.generated", "description": "..." }
  ]
}

Test a webhook

POST /api/webhooks/:id/test

// Sends a test event to your URL so you can verify connectivity and signature checking.

View delivery history

GET /api/webhooks/:id/deliveries

// Returns the last 50 deliveries with status codes and response bodies.
Retry policy: Failed deliveries are retried once after 5 seconds. If the retry also fails, the failure count increments. After 3 consecutive failures, the webhook is automatically deactivated. You can re-activate it via PUT /api/webhooks/:id with {"active": true}.