SignalRoute API · v1
API reference
REST + JSON. API keys with scopes. Cursor pagination. Idempotency keys. Stripe-style webhooks with HMAC SHA-256 signing. Request IDs on every response.
#Overview
The SignalRoute API is the same surface our dashboard uses. It models the resource graph: Organization → Business → Routes → Sessions → (Ratings, Public clicks, Private feedback). Every customer-facing review link is a Route; every customer journey through a Route is a Session.
- REST + JSON over HTTPS. No GraphQL.
- Snake-case field names. Opaque IDs (
biz_…,route_…). - Cursor pagination on all list endpoints.
- Idempotency keys on writes that integrations might retry.
- Webhooks for asynchronous workflows (Zapier, n8n, Make, custom).
- Public route endpoints are unauthenticated and rate-limited.
#Authentication
Two flavors of auth. Pick the right one for the caller.
1. API key (external integrations)
Send an Authorization header with a Bearer token. Keys start with sr_live_ or sr_test_.
Authorization: Bearer sr_live_xxxxxxxxxxxxxxxxxxxxxxxxKeys are scoped to a single organization and carry a list of permission scopes. Mint keys from the dashboard or via POST /v1/api-keys (session-only). Plaintext is shown exactly once at creation — store it in a secrets manager.
2. Session auth (first-party dashboard)
When the API is called from the SignalRoute dashboard, the request carries the session cookie set at login. Session callers see every organization they're a member of and have implicit access to every scope.
Never ship API keys to the browser. The widget and customer-facing review pages use unauthenticated public endpoints, not API keys.
#API key scopes
API keys grant access only to scopes assigned at creation. Read scopes return data; write scopes mutate. Use the narrowest set the integration needs.
- businesses:read
- businesses:write
- platform_links:read
- platform_links:write
- routes:read
- routes:write
- sessions:read
- sessions:write
- feedback:read
- feedback:write
- review_requests:read
- review_requests:write
- campaigns:read
- campaigns:write
- team_members:read
- team_members:write
- print_materials:read
- print_materials:write
- webhooks:read
- webhooks:write
- reports:read
- agency_clients:read
- agency_clients:write
- integrations:read
- integrations:write
- analytics:read
#Errors
Every error response uses the same shape. request_id matches the SignalRoute-Request-Id response header — include it when reporting bugs.
{
"error": {
"code": "route_not_found",
"message": "No active route was found for this ID.",
"request_id": "req_3y9hf...",
"details": []
}
}| code | status | when |
|---|---|---|
| unauthorized | 401 | Missing or invalid API key / session. |
| forbidden | 403 | Authenticated but lacks permission or scope. |
| not_found | 404 | Resource does not exist or is not visible. |
| validation_error | 422 | Request body or query failed schema validation. |
| rate_limited | 429 | Too many requests; respect Retry-After. |
| conflict | 409 | State conflict — e.g. duplicate slug, double-rate. |
| idempotency_conflict | 409 | Same Idempotency-Key reused with a different body. |
| integration_error | 502 | Downstream integration provider failed. |
| webhook_delivery_failed | 502 | Webhook receiver returned non-2xx. |
| internal_error | 500 | Unexpected server error — check request_id. |
#Pagination
List endpoints accept ?limit= (default 25, max 100) and ?cursor=.
GET /v1/sessions?business_id=biz_123&limit=50
{
"items": [ /* … */ ],
"next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAy..."
}Pass next_cursor back as cursor on the next request. When next_cursor is null you've reached the end.
#Idempotency
Send Idempotency-Key on retryable writes. Recent results (24h) are replayed verbatim. Reusing a key with a different body returns 409 idempotency_conflict.
Supported on:
POST /v1/review-requestsPOST /v1/routes/:routeId/sessionsPOST /v1/sessions/:sessionId/private-feedback
Idempotency-Key: housecall-job-5821-review-request#Rate limits
Public, unauthenticated customer-facing endpoints are rate-limited per IP. When you hit a limit you get 429 with a Retry-After header.
GET /v1/public/routes/:slug/config— 60/min/IPPOST /v1/routes/:routeId/sessions— 30/min/IPPOST /v1/sessions/:sessionId/rating— 10/min/IPPOST /v1/sessions/:sessionId/public-click— 10/min/IPPOST /v1/sessions/:sessionId/private-feedback— 5/min/IP
#Request ID
Every response carries SignalRoute-Request-Id: req_…. The same value is echoed in error bodies as request_id. Log it on your side; share it when reporting issues.
#Organizations
An organization is the top-level tenant. Type is business (a single merchant), agency (manages other businesses), or admin.
/v1/organizationsCreate a new organization. Session auth only — API keys can't escalate.
{
"name": "Wade's Plumbing & Septic",
"type": "business",
"owner": { "name": "Byron Wade", "email": "bw@wadesinc.io" }
}/v1/organizations/:organizationId/v1/organizations/:organizationId/v1/organizations/:organizationIdSoft-archives the organization (sets archived_at).
#Businesses
A business is a merchant. Routes and Platform Links live under it. Status is active · paused · archived · suspended · demo.
/v1/businesses{
"organization_id": "org_123",
"name": "Wade's Plumbing & Septic",
"slug": "wades-plumbing",
"industry": "plumbing",
"website_url": "https://wadesplumbingandseptic.com",
"phone": "831-430-6011",
"email": "bw@wadesinc.io",
"timezone": "America/New_York",
"address": {
"line1": "Jasper", "city": "Jasper",
"state": "GA", "postal_code": "30143", "country": "US"
}
}/v1/businessesorganization_idstringstatusenumlimitnumbercursorstring/v1/businesses/:businessId/v1/businesses/:businessId/v1/businesses/:businessId/archive#Platform links
The public review destinations a Route routes happy customers to. Built-in platforms: google, yelp, facebook, tripadvisor, angi, bbb, trustpilot, custom.
/v1/businesses/:businessId/platform-links{
"platform": "google",
"label": "Google",
"url": "https://g.page/r/example-review-link",
"enabled": true,
"sort_order": 1
}/v1/businesses/:businessId/platform-links/v1/platform-links/:platformLinkId/v1/platform-links/:platformLinkId#Routes
The heart of the product. A Route is the customer-facing review link / QR code. Types: main, campaign, team_member, location, print, widget, integration.
/v1/routes{
"business_id": "biz_123",
"name": "Main Review Link",
"slug": "wades-plumbing",
"type": "main",
"rating_threshold": 4,
"private_feedback_enabled": true,
"public_platforms_enabled": true,
"platform_link_ids": ["plink_123"],
"copy": {
"headline": "How was your experience?",
"subheadline": "Your feedback helps us improve.",
"private_feedback_headline": "Tell us what went wrong",
"private_feedback_body": "We appreciate the chance to make things right.",
"positive_review_headline": "Thanks! Where would you like to leave a review?"
}
}/v1/routesbusiness_idstringrequired/v1/routes/:routeId/v1/routes/by-slug/:slug/v1/routes/:routeId/v1/routes/:routeId/archive#QR codes
Render a QR code for a route as PNG or SVG. URL points at the public route page so the QR resolves directly without an extra hop.
/v1/routes/:routeId/qr-code.pngsizenumberdefault: 1024marginnumberdefault: 2/v1/routes/:routeId/qr-code.svg#Public route config
No auth · rate-limited · safe for browsers
/v1/public/routes/:slug/config{
"slug": "wades-plumbing",
"business_name": "Wade's Plumbing & Septic",
"logo_url": "https://cdn.getsignalroute.com/logos/wades.png",
"headline": "How was your experience?",
"subheadline": "Your feedback helps us improve.",
"rating_threshold": 4,
"theme": { "primary_color": "#111827", "background_color": "#ffffff" }
}#Review sessions
A session represents one customer journey through a Route. Created when the route page loads or the QR is scanned. Statuses: started · rated · public_clicked · private_feedback_submitted · completed · abandoned.
/v1/routes/:routeId/sessions{
"source": "qr",
"source_detail": "invoice-card",
"customer": {
"name": "Marcus Johnson",
"email": "marcus@example.com",
"phone": "555-555-5555"
},
"metadata": {
"invoice_id": "INV-1048",
"job_id": "JOB-5821",
"technician": "Byron"
}
}/v1/sessions/:sessionId/v1/sessionsbusiness_idstringrequiredroute_idstringstatusenum#Ratings
Submitting a rating advances the session. The response branches on whether the rating meets the route threshold.
/v1/sessions/:sessionId/rating// Request
{ "rating": 5 }
// Response (positive — route to public review)
{
"session_id": "sess_123",
"rating": 5,
"next_step": "public_review",
"public_platforms": [
{ "id": "plink_123", "platform": "google", "label": "Google",
"url": "https://g.page/r/example-review-link" }
],
"created_at": "2026-05-04T12:00:00.000Z"
}
// Response (negative — route to private feedback)
{
"session_id": "sess_123",
"rating": 2,
"next_step": "private_feedback",
"private_feedback": {
"headline": "Tell us what went wrong",
"body": "We appreciate the chance to make things right."
},
"created_at": "2026-05-04T12:00:00.000Z"
}#Public clicks
Record that a customer clicked through to a public review platform. The response echoes redirect_url so the frontend can navigate there.
/v1/sessions/:sessionId/public-click// Request
{ "platform_link_id": "plink_123" }
// Response
{
"id": "pclick_123",
"session_id": "sess_123",
"business_id": "biz_123",
"route_id": "route_123",
"platform": "google",
"redirect_url": "https://g.page/r/example-review-link",
"created_at": "2026-05-04T12:00:00.000Z"
}#Private feedback
Captured when a rating falls below the route threshold. Statuses: new, seen, in_progress, resolved, ignored, archived.
/v1/sessions/:sessionId/private-feedback{
"message": "Service was slower than expected and the part wasn't in stock.",
"contact_permission": true,
"customer": {
"name": "Marcus Johnson",
"email": "marcus@example.com",
"phone": "555-555-5555"
}
}/v1/private-feedbackbusiness_idstringrequiredstatusenum/v1/private-feedback/:feedbackId/v1/private-feedback/:feedbackId{ "status": "resolved", "internal_note": "Called customer; partial refund issued." }#Review requests
The integration-friendly endpoint. External tools (Housecall Pro, Jobber, Square, Zapier, n8n) call this after a job completes to send the customer a review link. Channels: sms · email · manual · api · qr.
/v1/review-requests{
"business_id": "biz_123",
"route_id": "route_123",
"customer": {
"name": "Marcus Johnson",
"email": "marcus@example.com",
"phone": "555-555-5555"
},
"channel": "sms",
"send_at": "2026-05-04T18:00:00.000Z",
"message": "Thanks for choosing Wade's. How was it? {{review_link}}",
"metadata": {
"source": "housecall_pro",
"job_id": "JOB-5821",
"invoice_id": "INV-1048"
}
}/v1/review-requestsbusiness_idstringrequiredstatusenum/v1/review-requests/:reviewRequestId/v1/review-requests/:reviewRequestId/sendMark the request as sent. Use after your downstream sender (Twilio, Resend, etc.) delivers.
/v1/review-requests/:reviewRequestId/cancel#Campaigns
Group routes by program — e.g. "Water heater install QR sticker run".
/v1/campaigns/v1/campaignsbusiness_idstringrequired/v1/campaigns/:campaignId/v1/campaigns/:campaignId/v1/campaigns/:campaignId/archive#Team members
Optional: track per-tech or per-rep routes. Pair with a Route of type team_member to attribute reviews back to the person who served the customer.
/v1/businesses/:businessId/team-members/v1/businesses/:businessId/team-members/v1/team-members/:teamMemberId/v1/team-members/:teamMemberId/v1/team-members/:teamMemberId/archive#Print materials
Templates for QR-code-bearing print. Types: business_card, invoice_card, counter_sign, table_tent, sticker, vehicle_sticker, equipment_sticker, receipt_insert, key_tag, door_hanger, flyer.
/v1/print-materials/v1/print-materials/v1/print-materials/:printMaterialId/v1/print-materials/:printMaterialId/v1/print-materials/:printMaterialId/generateQueue the material for rendering. Poll the resource for status: "generated" and download_url.
#Webhooks
Subscribe a URL to receive POSTs when events happen. Payloads are signed Stripe-style.
/v1/webhook-endpoints{
"business_id": "biz_123",
"url": "https://example.com/api/signalroute-webhook",
"events": [
"private_feedback.created",
"rating.submitted",
"public_click.created",
"review_request.sent"
]
}The response includes signing_secret exactly once. Subsequent reads omit it.
/v1/webhook-endpoints/v1/webhook-endpoints/:webhookEndpointId/v1/webhook-endpoints/:webhookEndpointId/v1/webhook-endpoints/:webhookEndpointId/v1/webhook-endpoints/:webhookEndpointId/testSend a synthetic ping event to confirm receivers handle our signature scheme.
Payload shape
{
"id": "evt_123",
"type": "private_feedback.created",
"created_at": "2026-05-04T12:00:00.000Z",
"data": {
"id": "fb_123",
"business_id": "biz_123",
"route_id": "route_123",
"session_id": "sess_123",
"rating": 2,
"message": "...",
"status": "new"
}
}Headers
SignalRoute-Signature: t=1777905600,v1=abcdef123456...
SignalRoute-Event-Id: evt_123Verifying signatures (HMAC SHA-256)
import crypto from "node:crypto"
export function verifySignalRouteSignature(opts: {
rawBody: string // raw request body string
signatureHeader: string // "t=...,v1=..."
secret: string // whsec_… from endpoint creation
toleranceSec?: number // default 300
}): boolean {
const parts = Object.fromEntries(
opts.signatureHeader.split(",").map(p => p.split("=") as [string,string])
)
const t = Number(parts.t)
const v1 = parts.v1
if (!t || !v1) return false
if (Math.abs(Math.floor(Date.now()/1000) - t) > (opts.toleranceSec ?? 300)) return false
const expected = crypto
.createHmac("sha256", opts.secret)
.update(`${t}.${opts.rawBody}`)
.digest("hex")
return crypto.timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex"))
}Event types
- business.created
- business.updated
- route.created
- route.updated
- route.archived
- session.started
- rating.submitted
- private_feedback.created
- private_feedback.updated
- private_feedback.resolved
- public_click.created
- review_request.created
- review_request.scheduled
- review_request.sent
- review_request.failed
- review_request.opened
- review_request.completed
- team_member.created
- team_member.updated
- report.generated
Retries & status
We POST with a 5s timeout. Non-2xx or network errors increment failure_count. After 5 consecutive failures the endpoint flips to failing. A successful delivery resets the counter.
#Integration connections
Bookkeeping resource for native integrations (Housecall Pro, Jobber, ServiceTitan, Square, Stripe, QuickBooks, GBP, GoHighLevel, WordPress, Webflow, Zapier, Make, n8n). Stores per-tenant settings and credentials so dashboard UIs stay in sync.
/v1/integration-connections{
"business_id": "biz_123",
"provider": "housecall_pro",
"status": "active",
"settings": {
"send_after_job_completed": true,
"delay_minutes": 120,
"route_id": "route_123",
"channel": "sms"
}
}/v1/integration-connections/v1/integration-connections/:integrationConnectionId/v1/integration-connections/:integrationConnectionId/v1/integration-connections/:integrationConnectionId#Analytics
/v1/analytics/summarybusiness_idstringrequiredstart_dateYYYY-MM-DDrequiredend_dateYYYY-MM-DDrequired{
"business_id": "biz_123",
"period": { "start_date": "2026-05-01", "end_date": "2026-05-31" },
"metrics": {
"route_views": 428,
"ratings_submitted": 133,
"average_rating": 4.62,
"positive_ratings": 119,
"private_feedback_count": 14,
"public_clicks": 93,
"google_clicks": 81,
"facebook_clicks": 8,
"yelp_clicks": 4,
"resolved_feedback_count": 9,
"review_protection_score": 82
}
}#Reports
/v1/reports/monthly{ "business_id": "biz_123", "month": "2026-05", "format": "pdf" }Returns a queued report. Workers flip status to generated and set download_url.
/v1/reportsbusiness_idstringrequired/v1/reports/:reportId#Agency clients
Thin wrapper around businesses for agency workflows. Creating an agency client spins up a new client organization and a business under it, linking the agency.
/v1/agency-clients{
"agency_organization_id": "org_agency_123",
"business": {
"name": "Main Street Auto Repair",
"slug": "main-street-auto",
"industry": "auto_repair",
"website_url": "https://mainstreetauto.example",
"email": "owner@example.com",
"phone": "555-555-5555",
"timezone": "America/New_York"
}
}/v1/agency-clients/v1/agency-clients/:agencyClientId/v1/agency-clients/:agencyClientId/v1/agency-clients/:agencyClientId/summaryCompact month-to-date summary for the dashboard.
#Zapier-friendly endpoints
Slim, paginated-free dropdown endpoints for Zapier / Make / n8n integration UIs, plus the auth-test (/v1/me).
/v1/zapier/businesses/v1/zapier/routesbusiness_idstringrequired#API keys
Session auth required. Plaintext is returned exactly once at create time.
/v1/api-keys{
"organization_id": "org_123",
"name": "Zapier production",
"scopes": ["review_requests:write", "businesses:read", "routes:read"],
"livemode": true
}/v1/api-keysorganization_idstringrequired/v1/api-keys/:apiKeyIdSoft-revokes the key. The key cannot be reactivated.
#/v1/me — auth test
Echoes the caller. Use it from Zapier to validate an API key at connection time.
/v1/meNeed an integration we don't list yet? Email bw@wadesinc.io or spin up a sandbox account and try the API today.