Docs

Webhooks

Last updated October 19, 2026

Webhooks send each response, and each coupon it earned, to a URL you choose as it happens: a Zapier or Make scenario, or your own server. They are included on the Business and Enterprise plans. Add one under Integrations in the app.

1.Events

response.created
A customer submits a response. Sent for every stored response, including ones whose coupon was withheld.
reward.issued
A customer receives a coupon. The payload includes the code, the message and the expiry.

Each webhook can listen to one or both events, for every survey in the account or only the surveys you pick.

2.Payload

Every delivery is an HTTPS POST with a JSON body. Free text and contact details are included: it is your data, going to a URL you configured.

answers lists the questions the customer answered, in the survey’s question order, labelled with the wording of the version they answered. Choice answers show labels, with the stable option ids in optionIds; file answers add a download link.

fields holds the same answers flattened for Zapier, Make and spreadsheets: one text value per question of the version answered, keyed by the question id. A skipped question is still there, as an empty string, so a column you map stays in place. Choices are their labels, several separated by “; ”; rankings read “First > Second”; grids read “Row: Column; Row: Column”; contact questions read “Label: value; …” and also get one key per detail, the question id followed by _name, _email and so on; file questions list the file names. Statement blocks take no answer and have no key.

questions has the same keys as fields, each holding the question’s wording in the version answered, so a tool that only shows key names can still tell the questions apart. A contact detail’s key holds the question and the detail, as in “Your details — Email”.

Question ids don’t change when you reword a question, so mappings keep working; only the wording in questions changes. A question you add later arrives as a new key.

response.created (reward is present on reward.issued)
{
  "version": 1,
  "id": "evt_4f1c…",
  "event": "response.created",
  "createdAt": "2026-10-01T09:30:12.411Z",
  "accountId": "acct_…",
  "survey": { "id": "k3P9xQ", "title": "How was your visit?", "version": 3, "brandId": "default" },
  "response": {
    "id": "7c0e…",
    "createdAt": "2026-10-01T09:30:11.902Z",
    "source": "counter-qr",
    "ref": null,
    "device": "mobile",
    "language": "en",
    "completionMs": 41250,
    "suspect": false,
    "ending": { "id": "e_thanks", "title": "Thanks for your feedback" },
    "hidden": { "email": "asha@example.com", "plan": "pro" },
    "embed": "popup",
    "pageUrl": "https://yourshop.com/thank-you"
  },
  "answers": [
    { "questionId": "q_Xp3k9QmZ2a", "question": "How was your visit today?", "type": "rating", "value": 5 },
    { "questionId": "q_7RtB2wLc0e", "question": "What did you order?", "type": "checkbox", "value": ["Cortado", "Croissant"], "optionIds": ["o_k2m9x1aa", "o_p3q8z0bb"] },
    { "questionId": "q_Jd4nV8sYq1", "question": "Anything we should add to the menu?", "type": "comment", "value": "Oat milk cortado" }
  ],
  "fields": {
    "q_Xp3k9QmZ2a": "5",
    "q_7RtB2wLc0e": "Cortado; Croissant",
    "q_Jd4nV8sYq1": "Oat milk cortado",
    "q_M0wZ6hTa3f": ""
  },
  "questions": {
    "q_Xp3k9QmZ2a": "How was your visit today?",
    "q_7RtB2wLc0e": "What did you order?",
    "q_Jd4nV8sYq1": "Anything we should add to the menu?",
    "q_M0wZ6hTa3f": "Can we have your name?"
  },
  "reward": { "code": "BREW15-7KQ2", "message": "15% off your next visit", "expiresOn": "2026-12-31" }
}

3.Verifying the signature

Every delivery carries a X-PuppetRush-Signature header of the form t=<unix seconds>,v1=<signature>. The signature is the hex HMAC-SHA256 of "<t>.<raw body>" with your webhook's secret, shown once when you create the webhook or ask for a new one.

Compare signatures in constant time and reject deliveries whose timestamp is more than five minutes old, which stops replays.

Node (Express)
import crypto from 'node:crypto';
import express from 'express';

const app = express();
// Verify against the raw body, before any JSON parsing.
app.post('/hearsay', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('X-PuppetRush-Signature') || '';
  const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto.createHmac('sha256', process.env.HEARSAY_SECRET)
    .update(`${t}.${req.body}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  const valid = v1?.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
  if (!fresh || !valid) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  // … store event.fields, send event.reward.code to your CRM …
  res.sendStatus(200);
});
Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/hearsay")
def hearsay():
    header = request.headers.get("X-PuppetRush-Signature", "")
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    body = request.get_data()  # the raw bytes
    expected = hmac.new(os.environ["HEARSAY_SECRET"].encode(),
                        f"{parts.get('t')}.".encode() + body, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(parts.get("t", 0))) < 300
    if not fresh or not hmac.compare_digest(parts.get("v1", ""), expected):
        abort(400)
    event = request.get_json()
    return "", 200

4.Retries and switching off

Answer with any 2xx status within 10 seconds. Anything else is a failed attempt, and the event is tried again with growing backoff: 5 attempts in all.

An event that fails all 5 attempts is one failed delivery; a delivery that gets through resets the count. After 25 failed deliveries in a row the webhook is switched off, the Integrations page says why, and the account’s owners and admins get an email. Fix the endpoint and switch it back on; the count starts over.

Deliveries can arrive more than once and out of order, and every attempt carries a new top-level id. To ignore repeats, use the event together with response.id: each response sends response.created once and, when it earns a coupon, reward.issued once. The last seven days of deliveries, with status codes, are listed under each webhook.

Send test posts an event of type test right away, built from one of the webhook’s surveys: the latest real response to it, or sample answers to its questions when it has none yet. If the webhook listens to reward.issued, the test includes that response’s coupon, or the code TEST-CODE. Tests aren’t retried and don’t count towards switching off.

If the account moves to a plan without webhooks, they stop sending. They stay listed under Integrations, where you can switch them off or delete them.

5.Zapier

Webhooks by Zapier is one of Zapier’s premium apps, so the Zap needs a paid Zapier plan. Create a Zap with the trigger “Webhooks by Zapier” and the event “Catch Hook”. Copy the URL it gives you, add it as a webhook in Hearsay for the survey you want, and press Send test so Zapier sees that survey’s questions. Then add an action, for example “Google Sheets: Create spreadsheet row”, and map a value from fields to each column, along with the response id and time and, for coupons, the reward code.

Zapier lists each value under its key, the question id, with the test answer beside it. To see which question a key is, find the same id under questions, which holds the question’s wording. This also works for a question the test response skipped, whose field is empty.

Map fields rather than answers: Zapier joins a list like answers into comma-separated text, and the positions in it move when a question is skipped. Each survey has its own question ids, so a webhook per survey keeps a Zap simple.

6.Make

Add a “Webhooks: Custom webhook” module, copy its address into a Hearsay webhook, and press Send test so Make learns the data structure, including your survey’s fields. Then connect the next module, for example your CRM or an email step with reward.code.

7.Help

Questions about webhooks? Email support@hearsay.co.in. We reply within one business day.