How to Send SurveyMonkey Response Push Notifications to a Shared Room
Verify SurveyMonkey's signed webhook, keep answers private, and send one fixed Ping when a completed response arrives.

To send a SurveyMonkey response notification to a shared phone-alert Room, subscribe a small HTTPS relay to SurveyMonkey's response_completed webhook. The relay should handle SurveyMonkey's callback check, verify the signed request, accept only one survey, and send fixed copy to a PingRoom incoming webhook.
This guide sends New SurveyMonkey response — Workshop feedback received a completed response. It forwards no answers, respondent details, or links. It is a webhook recipe you configure, not a native SurveyMonkey integration.
Choose the smallest alert path
SurveyMonkey already offers response notifications. The mobile app can manage new-response alerts, and current web notification options depend on plan. Start there when SurveyMonkey itself reaches the right person.
| Need | Better fit |
|---|---|
| One signed-in account holder wants a native mobile alert | SurveyMonkey's app alert |
| The survey owner wants an email or Notification Center message | SurveyMonkey's current notification options |
| A selected team needs one short alert in a shared Room | The signed-webhook relay below |
| A no-code workflow needs response fields and many destinations | A reviewed automation tool; Zapier documents its SurveyMonkey plan requirements |
The Room is only an attention channel. SurveyMonkey remains the response record and the place where authorized people review answers.
Keep answers out of the Ping
Choose a short, non-sensitive label such as Workshop feedback, Volunteer check-in, or Equipment return. The relay uses that fixed label instead of fetching the response.
For an illustrative workshop survey, the Room receives:
New SurveyMonkey response
Workshop feedback received a completed response.
The team follows its normal authorized route to SurveyMonkey for details. The alert does not identify the respondent or imply that someone reviewed, approved, or acted on the response.
Pingo the Pigeon, our fictional CMO, appears beside one blank signal token and a closed survey archive. He cannot see survey answers, enter the private Room, or send a Ping. People choose the copy, Room membership, and follow-up.
Prepare the PingRoom destination
The destination Room's owner needs active PingRoom Pro for incoming webhooks. Room members who want native alerts need PingRoom on a supported device with notifications allowed.
Follow the incoming webhook setup: open the Room's Settings → Connections → From another app, add a webhook named SurveyMonkey, choose Custom message, and copy its URL. The complete URL is a credential. Store it as a server-side secret; never put it in survey copy, browser code, source control, screenshots, or logs.
Your relay also needs these server-side values:
SURVEYMONKEY_CLIENT_IDandSURVEYMONKEY_API_SECRETfrom your SurveyMonkey app- a long random
SURVEYMONKEY_WEBHOOK_AUTHORIZATIONvalue that you also configure on the webhook - the one allowed
SURVEYMONKEY_SURVEY_ID - a fixed
SURVEYMONKEY_SURVEY_LABEL - the
PINGROOM_WEBHOOK_URL
Anyone with a SurveyMonkey account can register a draft app for 90 days. Long-term deployment has separate public- and private-app rules. Private-app users must belong to the same SurveyMonkey team and have a paid plan with direct API access. Check the current requirements before treating a draft recipe as production infrastructure.
Verify, minimize, and forward
SurveyMonkey signs the exact callback body with HMAC-SHA1 using client_id&api_secret, sends the client ID in Sm-Apikey, and can include a configured Authorization header. This Node-compatible handler checks all three before parsing the event.
import { createHash, createHmac, timingSafeEqual } from "node:crypto"; function secureEqual(left, right) { const a = Buffer.from(left); const b = Buffer.from(right); return a.length === b.length && timingSafeEqual(a, b); } function validPingRoomUrl(value) { return /^https:\/\/api\.pingroom\.io\/api\/webhooks\/[^/?#\s]+\/[^/?#\s]+$/.test( value || "", ); } export async function handleSurveyMonkey(request, env, fetchImpl = fetch) { if (request.method === "HEAD") { return new Response(null, { status: 200 }); } if (request.method !== "POST") { return new Response("method not allowed", { status: 405 }); } const label = (env.SURVEYMONKEY_SURVEY_LABEL || "").trim(); if ( !env.SURVEYMONKEY_CLIENT_ID || !env.SURVEYMONKEY_API_SECRET || !env.SURVEYMONKEY_WEBHOOK_AUTHORIZATION || !env.SURVEYMONKEY_SURVEY_ID || !label || Array.from(label).length > 70 || /[\r\n\t]/.test(label) || !validPingRoomUrl(env.PINGROOM_WEBHOOK_URL) ) { return new Response("server configuration error", { status: 500 }); } const rawBody = Buffer.from(await request.arrayBuffer()); if (rawBody.length > 32_768) { return new Response("payload too large", { status: 413 }); } const receivedClientId = request.headers.get("sm-apikey") || ""; const receivedAuthorization = request.headers.get("authorization") || ""; const receivedSignature = request.headers.get("sm-signature") || ""; const expectedSignature = createHmac( "sha1", `${env.SURVEYMONKEY_CLIENT_ID}&${env.SURVEYMONKEY_API_SECRET}`, ) .update(rawBody) .digest("base64"); if ( !secureEqual(receivedClientId, env.SURVEYMONKEY_CLIENT_ID) || !secureEqual( receivedAuthorization, env.SURVEYMONKEY_WEBHOOK_AUTHORIZATION, ) || !secureEqual(receivedSignature, expectedSignature) ) { return new Response("invalid request", { status: 401 }); } let event; try { event = JSON.parse(rawBody.toString("utf8")); } catch { return new Response("invalid JSON", { status: 400 }); } if ( event.event_type !== "response_completed" || event.object_type !== "response" ) { return new Response("ignored", { status: 202 }); } const surveyId = event.resources?.survey_id; if (surveyId !== env.SURVEYMONKEY_SURVEY_ID) { return new Response("ignored", { status: 202 }); } if ( typeof event.event_id !== "string" || !event.event_id || typeof event.object_id !== "string" || !event.object_id ) { return new Response("invalid event", { status: 400 }); } const digest = createHash("sha256") .update(`${surveyId}:${event.event_id}:${event.object_id}`) .digest("hex"); const eventKey = `surveymonkey-${digest}`; const pingBody = JSON.stringify({ action: null, title: "New SurveyMonkey response", message: `${label} received a completed response.`, correlation_id: eventKey, }); let pingResponse; try { pingResponse = await fetchImpl(env.PINGROOM_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json", "Idempotency-Key": eventKey, }, body: pingBody, redirect: "error", signal: AbortSignal.timeout(8000), }); } catch { return new Response("upstream unavailable", { status: 502 }); } let accepted = false; try { accepted = (await pingResponse.json()).success === true; } catch { // A non-JSON response cannot confirm acceptance. } if (pingResponse.status !== 200 || !accepted) { return new Response("upstream unavailable", { status: 502 }); } return new Response("accepted", { status: 200 }); }
The callback includes event and object identifiers plus related resource IDs. The handler uses only the event ID, response object ID, and allowed survey ID for stable identity. It never requests response details and never reads answer fields.
If SurveyMonkey repeats the same callback, the fixed body and stable PingRoom Idempotency-Key let PingRoom return the previously accepted result instead of creating another Ping. Do not add a current timestamp to the body; an exact retry must remain exact.

AI-generated editorial illustration. Pingo is PingRoom's fictional mascot; the people and workflow are illustrative.
Register one narrow webhook
SurveyMonkey requires a unique HTTPS subscription URL that accepts a HEAD request and returns 200. Create the webhook through your app's correct API access URL. Accounts in the EU and Canadian data centers use different API hosts, so do not assume the US host.
Submit this shape to POST /v3/webhooks with your app's access token and webhooks_write scope:
{ "name": "PingRoom completed-response alert", "subscription_url": "https://relay.example.test/surveymonkey", "authorization": "Bearer replace-with-a-long-random-value", "verify_ssl": true, "event_type": "response_completed", "object_type": "survey", "object_ids": ["replace-with-one-survey-id"] }
Use the exact authorization value as SURVEYMONKEY_WEBHOOK_AUTHORIZATION. Keep verify_ssl enabled. The API's webhook reference says the subscription URL must be unique for each webhook and documents the response_completed event and survey-object filter.
Test the authentication and privacy boundaries
Before using a real survey, submit a harmless completed response and check:
- The subscription's
HEADprobe receives200without creating a Ping. - A valid signed
response_completedcallback for the allowed survey creates one fixed-copy Ping. - The Ping body contains no answers, respondent or recipient IDs, email address, collector link, app secret, or access token.
- Replaying the identical callback creates no second Ping.
- A wrong HMAC, client ID, authorization value, survey ID, or changed body sends nothing.
- A PingRoom rejection makes the relay return a non-2xx response instead of claiming success.
For this article, we ran the exact embedded handler locally with signed SurveyMonkey fixtures and a stateful mock of PingRoom's documented webhook contract. The check covered the HEAD probe, valid delivery, exact replay, a new event, ignored event types and surveys, missing or altered credentials, malformed JSON, oversized bodies, invalid configuration, PingRoom rejection, malformed upstream responses, and network failure. It made no live SurveyMonkey, PingRoom, or phone request.
Know what acceptance means
SurveyMonkey's callback is a notification that a response event occurred, not the answer record itself. Keep the app secret, access token, configured authorization value, and PingRoom webhook URL separate and server-side. Rotate any value that is exposed.
PingRoom permits up to four incoming webhooks per Room, applies the Room's webhook cooldown, and enforces the request limits in the webhook documentation. A PingRoom response with HTTP 200 and success: true means the Ping was accepted and queued. It does not guarantee that every device displayed an alert under every network, Focus, mute, battery, or notification setting.
If the survey supports healthcare, safeguarding, payment, legal, emergency, or regulated work, keep the approved case system, access controls, audit record, and escalation path authoritative. This alert is an attention cue, not proof that a response was reviewed or acted on.
Start with one harmless survey and a small PingRoom Room. If your intake lives in Typeform, use the separate Typeform response-notification recipe.
PingRoom
Short-form communication.


