Send leads to a webhook

Send Riddle leads as JSON payloads to any HTTP endpoint in real time. Use webhooks when you need to connect to a system that is not covered by the built-in integrations – your own backend, a data warehouse, a custom CRM, or any service that accepts HTTP POST requests. Here's some more information about webhooks and APIs.

Add a webhook

  1. Create a Riddle. To collect contact details such as an email address, add a form block – without one, the webhook still receives the answers, but identifier is null.
    create riddle
  2. Set up a URL for a webhook on your web server. For testing purposes we recommend creating a free webhook on https://webhook.site as this will allow you to inspect the data Riddle sends to the webhook.
    set up test webhook
  3. In Riddle, go to PUBLISH and click on Save and connect data.
    save and connect data
  4. Under Add integrations, find Webhook, and click on ADD.
    add webhook integration
  5. Enter the URL of your webhook endpoint into the Webhook URL field. If the receiving system requires authentication, add it under Add HTTP headers (see Add custom HTTP headers). Then click on CONTINUE.
    enter webhook url
  6. A test lead will be sent automatically but you can retest by clicking on SEND.
    test lead for webhook
  7. Click on CONTINUE. The test lead will appear on your webhook site.
    test lead on webhook site
  8. Play your published Riddle. Every completed Riddle sends one request to your webhook. If double opt-in is enabled, the request is only sent after the email address is confirmed. Here is the payload of our example Riddle:
    {
      "uniqueId": "613361996f3862696d2b60506e756144689e6ebf6a5c666f6127bf8b4f5e9d0ae4",
      "confirmed": true,
      "riddleId": "4kTPnvQ3",
      "identifier": "charlotte@riddle.com",
      "version": 1,
      "createdAt": "2024-04-25 08:54:43",
      "data": {
        "riddle": [
          {
            "title": "What do you enjoy most?",
            "blockId": 3,
            "value": [
              "Type the sport you enjoy most."
            ]
          },
          {
            "title": "What was the last event you went to as a spectator?",
            "blockId": 7,
            "value": [
              "A running event"
            ]
          },
          {
            "title": "What was the last event you participated in?",
            "blockId": 5,
            "value": [
              "A running event"
            ]
          }
        ],
        "form": [
          {
            "title": "Name",
            "blockId": 6,
            "blockTitle": "Your details",
            "fieldId": 0,
            "customId": "first_name",
            "value": "Charlotte"
          },
          {
            "title": "Email",
            "blockId": 6,
            "blockTitle": "Your details",
            "fieldId": 1,
            "customId": "email",
            "value": "charlotte@riddle.com"
          },
          {
            "title": "Checkbox",
            "blockId": 6,
            "blockTitle": "Your details",
            "fieldId": 2,
            "customId": "newsletter_optin",
            "value": true
          }
        ],
        "time": 0,
        "result": {
          "score": 0,
          "maxScore": 0,
          "blockId": 1,
          "title": "Thanks for sharing."
        }
      }
    }
    
  9. When you add a webhook, Riddle automatically generates a Signature key, shown in the Test your integration dialog. You can use it to check that a request really comes from Riddle – see Verify the signature.

Secure your webhook

Anyone who knows your webhook URL can send data to it, so the receiving side should check that each request really comes from Riddle. You have two options, and you can combine them:

  • Custom HTTP headers – for middleware and SaaS endpoints such as n8n, Make, Pipedream or a CRM's inbound API. These tools expect a static key in a request header, for example Authorization: Bearer <key> or X-API-Key: <key>, and check it for you. No code needed.
  • Signature validation – for your own backend. Riddle signs each request with your signature key (HMAC-SHA256), and your code recalculates the signature to confirm the request is genuine.

If you are connecting Riddle to a no-code or middleware tool, use custom headers. You don't need to implement signature validation for that.

Add custom HTTP headers

You can send any number of static HTTP headers with every webhook request.

  1. In the Webhook dialog, below the Webhook URL field, click on ADD HEADER under Add HTTP headers.
  2. Enter the header name into Header key, for example Authorization, and its value into Header value, for example Bearer 7f3k9x2m.
  3. To add another header, click on ADD HEADER again.
  4. Click on CONTINUE. The test requests already include your headers. If the receiving system rejects them (for example with HTTP code 401 or 403), check the header name and value.

Enter the complete value, including any prefix. Some systems expect only the key, others expect a word such as Bearer or Basic in front of it. For Authorization: Bearer <token>, type Bearer 7f3k9x2m into the value field, not just 7f3k9x2m.

Common header setups:

The receiving system expectsHeader nameHeader value
A bearer tokenAuthorizationBearer <your-token>
An API key headerX-API-Key (or the name the tool specifies)<your-key>
HTTP basic authAuthorizationBasic <base64 of username:password>

Verify the signature

When you add a webhook, Riddle generates a Signature key and shows it in the Test your integration dialog. Riddle uses it to sign every request:

PropertyValue
HeaderX-Riddle-Signature
AlgorithmHMAC-SHA256, with your signature key as the secret
Signed contentThe raw request body
FormatLowercase hex string (64 characters)

To verify a request, calculate the HMAC-SHA256 of the raw body with your signature key and compare the result with the X-Riddle-Signature header. If they match, the request comes from Riddle and was not changed on the way. Use a constant-time comparison such as hash_equals (PHP) or crypto.timingSafeEqual (Node.js).

Hash the raw request body. Calculate the HMAC over the body exactly as you received it, before you parse it. Don't decode the JSON and encode it again: Riddle escapes special characters (for example Jörg/Test is sent as J\u00f6rg\/Test), so a re-encoded body often won't match.

Treat the signature key like a password: store it in your server configuration, never in client-side code. The connection test and the sample lead are signed too, so you can test your verification before any real lead comes in.

PHP

<?php

$signatureKey = 'YOUR_SIGNATURE_KEY'; // from the Test your integration dialog

$rawPayload = file_get_contents('php://input'); // the raw body, before json_decode()
$receivedSignature = $_SERVER['HTTP_X_RIDDLE_SIGNATURE'] ?? '';
$expectedSignature = hash_hmac('sha256', $rawPayload, $signatureKey);

if (!hash_equals($expectedSignature, $receivedSignature)) {
    http_response_code(401);
    die('Invalid signature');
}

$payload = json_decode($rawPayload, true);

JavaScript (Node.js)

const crypto = require("crypto");
const http = require("http");

const SIGNATURE_KEY = "YOUR_SIGNATURE_KEY"; // from the Test your integration dialog

const isValidSignature = (rawBody, signature) => {
  const expected = crypto.createHmac("sha256", SIGNATURE_KEY).update(rawBody).digest("hex");
  const received = String(signature || "");
  return received.length === expected.length && crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
};

const server = http.createServer((req, res) => {
  const chunks = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", () => {
    const rawBody = Buffer.concat(chunks); // the raw body, not parsed JSON
    if (!isValidSignature(rawBody, req.headers["x-riddle-signature"])) {
      res.writeHead(401);
      return res.end("Invalid signature");
    }
    const payload = JSON.parse(rawBody.toString("utf8"));
    // process the payload
    res.writeHead(200);
    res.end();
  });
});

server.listen(8000);

Connect Riddle to n8n

A webhook with a custom header is the easiest way to send Riddle leads to tools that have no built-in Riddle integration, such as Braze or many other CRM and marketing platforms. Middleware like n8n receives the lead and passes it on. Here's how to set it up in n8n:

  1. In n8n, create a workflow and add a Webhook node as the trigger.
  2. Set HTTP Method to POST.
  3. Under Authentication, select Header auth and create a new credential. Enter a Name, for example X-API-Key, and a long random string as the Value.
  4. Copy the webhook URL from the node. Use the Test URL while you build the workflow and the Production URL once the workflow is saved and published.
  5. In Riddle, add a webhook with that URL. Under Add HTTP headers, click on ADD HEADER and enter the same name and value as in the n8n credential.
  6. In n8n, click on Listen for test event. The Test URL now listens for 120 seconds, so send the test lead from Riddle right away (click on SEND in the test dialog to resend it). The lead appears in the Webhook node, and the Riddle payload is under body.
  7. Add the next nodes to your workflow, for example an HTTP Request node that sends the lead's email address and answers to your CRM's API.

The same approach works in Make, Pipedream and similar tools: create a webhook trigger, copy its URL into Riddle, and add the header that the tool checks. If a lead doesn't arrive, see Failed leads.

Process the webhook

The examples below read every question, form field and the result from the payload. Parts that your Riddle doesn't have are missing from the payload – a form, for example, has no questions and no result – so the code checks for them before using them. With webhook.site you get a good overview of all the data your Riddle sends to the webhook.

PHP

<?php

// read the raw payload from the request
$rawPayload = file_get_contents('php://input');

if (!$rawPayload) {
    http_response_code(400);
    die('No payload received');
}

// optional: verify the signature first, see "Verify the signature" above

$payload = json_decode($rawPayload, true);

if (!is_array($payload)) {
    http_response_code(400);
    die('Invalid JSON');
}

// basic information about the lead itself
$uniqueId = $payload['uniqueId'];
$riddleId = $payload['riddleId'];
$confirmed = $payload['confirmed'];
$identifier = $payload['identifier']; // null if the Riddle has no form
$version = $payload['version'];
$createdAt = $payload['createdAt'];

$data = $payload['data'] ?? [];

// questions and answers – missing if the Riddle has no questions, for example a form
foreach ($data['riddle'] ?? [] as $block) {
    $questionTitle = $block['title'];
    $questionBlockId = $block['blockId'];
    $answers = $block['value']; // array, one entry per selected or entered answer
    // do something with the question and its answers
}

// form fields – missing if the Riddle has no form
foreach ($data['form'] ?? [] as $field) {
    $fieldTitle = $field['title'];
    $fieldBlockTitle = $field['blockTitle'];
    $fieldCustomId = $field['customId']; // stable named identifier set in the form block options, can be null
    $fieldValue = $field['value']; // string, number or boolean (for example a checkbox)
    // do something with the field
}

// result – missing if the Riddle has no result page, for example a form
if (isset($data['result'])) {
    $resultBlockId = $data['result']['blockId'];
    $resultTitle = $data['result']['title'];
    $resultScore = $data['result']['score'];
    $resultMaxScore = $data['result']['maxScore'];
}

// NOW: do whatever you want with the data, for example save it to a MySQL database
$sql = "INSERT INTO [...]";

http_response_code(200);

JavaScript (Node.js)

const http = require("http");

const server = http.createServer((req, res) => {
  if (req.method !== "POST") {
    res.writeHead(405);
    return res.end();
  }

  const chunks = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", () => {
    // optional: verify the signature first, see "Verify the signature" above
    let payload;
    try {
      payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
    } catch (err) {
      res.writeHead(400);
      return res.end("Invalid JSON");
    }

    try {
      processLead(payload);
    } catch (err) {
      console.error(err);
      res.writeHead(500);
      return res.end();
    }

    res.writeHead(200);
    res.end();
  });
});

const processLead = (payload) => {
  // basic information about the lead itself
  console.log("uniqueId: ", payload.uniqueId);
  console.log("riddleId: ", payload.riddleId);
  console.log("confirmed: ", payload.confirmed);
  console.log("identifier: ", payload.identifier); // null if the Riddle has no form
  console.log("version: ", payload.version);
  console.log("createdAt: ", payload.createdAt);

  const data = payload.data || {};

  // questions and answers – missing if the Riddle has no questions, for example a form
  (data.riddle || []).forEach((block) => {
    console.log("question: ", block.title, "| blockId: ", block.blockId);
    block.value.forEach((answer) => {
      console.log("answer: ", answer); // one entry per selected or entered answer
    });
  });

  // form fields – missing if the Riddle has no form
  (data.form || []).forEach((field) => {
    console.log("field: ", field.title, "| block: ", field.blockTitle, "| customId: ", field.customId);
    console.log("value: ", field.value); // string, number or boolean (for example a checkbox)
  });

  // result – missing if the Riddle has no result page, for example a form
  if (data.result) {
    console.log("result: ", data.result.title, "| score: ", data.result.score, "/", data.result.maxScore);
  }

  // NOW: do whatever you want with the data, for example save it to a database
};

server.listen(8000, () => {
  console.log("Server is running on port 8000");
});