Signed webhooks
On Pro, makeform.live can send a small, signed message to your own server or an automation tool whenever a form receives an inquiry.
On this page
All guides
Connect a tool
A webhook is an alert that sends JSON instead of a chat message. Like chat alerts, it says an inquiry arrived and links to it. It never contains your visitor’s answers. A webhook counts towards Pro’s 3 alerts, and only the account owner can add one in Settings → Integrations.
Google Sheets
This adds one row for each new inquiry: the time, the form, the project and the private link. It uses a short script in your own spreadsheet, and no Google sign-in to makeform.live.
- Open or create a Google Sheet, then choose Extensions → Apps Script.
- Delete the sample code, paste the script below and choose Save.
- Choose Deploy → New deployment. Next to Select type, choose the gear, then Web app.
- Set Execute as to Me and Who has access to Anyone, then choose Deploy.
- Choose Authorise access and pick your Google account. Google shows “Google hasn’t verified this app”, because it’s your own script rather than a published app.
- Choose Advanced, then Go to (your project name), then Allow. The script can only edit this spreadsheet.
- Copy the Web app URL. It ends with /exec.
- In makeform.live, open Settings → Integrations → Add an alert, choose Webhook, paste the link and choose Send test and add.
- A row saying “Test from makeform.live” appears in a sheet called Inquiries. You can delete that row.
/** @OnlyCurrentDoc */
// makeform.live → this spreadsheet: one row per new inquiry.
// It writes the time, form, project and private link. makeform.live sends no answers.
// It logs nothing, so messages and the web app link don't appear in Executions.
const SHEET = "Inquiries";
function doPost(e) {
let body;
try {
body = JSON.parse(e.postData.contents);
} catch (error) {
return reply("ignored");
}
const data = body.data || {};
const inquiry = data.inquiry || {};
const test = body.type === "alert.test";
if (!test && body.type !== "inquiry.received") return reply("ignored");
if (!test && !String(inquiry.url).startsWith("https://makeform.live/forms/")) {
return reply("ignored");
}
const cache = CacheService.getScriptCache();
if (!test && cache.get(inquiry.id)) return reply("ok"); // a retry we already saved
const book = SpreadsheetApp.getActiveSpreadsheet();
const sheet = book.getSheetByName(SHEET) || book.insertSheet(SHEET);
if (sheet.getLastRow() === 0) {
sheet.appendRow(["Received (UTC)", "Form", "Project", "Inquiry link"]);
}
sheet.appendRow([
text(inquiry.receivedAt || body.timestamp),
text(test ? "Test from makeform.live" : data.form && data.form.name),
text(data.project ? data.project.name : ""),
text(test ? "" : inquiry.url),
]);
if (!test) cache.put(inquiry.id, "1", 21600);
return reply("ok");
}
// Text that starts like a formula is saved as plain text.
function text(value) {
const s = value == null ? "" : String(value);
return /^[=+\-@]/.test(s) ? "'" + s : s;
}
function reply(message) {
return ContentService.createTextOutput(message);
}Anyone who can open the spreadsheet sees the links. Opening one still needs a makeform.live sign-in.
Change the script later
To change the script later, choose Deploy → Manage deployments, edit the deployment, choose New version, then Deploy. The link stays the same.
Make.com
- In Make.com, create a scenario and add the Webhooks module Custom webhook.
- Choose Add, name the webhook “makeform.live” and choose Save. Make.com shows the webhook address and starts listening.
- Copy the address. In makeform.live, add a Webhook alert, paste it and choose Send test and add. Make.com shows that it has determined the data structure.
- Add the next steps, for example Google Sheets → Add a row or Notion → Create a page, and map
data.form.name,data.inquiry.receivedAtanddata.inquiry.url. - Save the scenario and turn it on.
- Each run uses some of your Make.com plan’s monthly credits.
- To bring in the visitor’s answers, add an HTTP step that calls
data.inquiry.apiUrl(see Read the answers).
If Make.com turns the webhook off
If a Make.com webhook isn’t attached to a scenario for 5 days, Make.com turns it off. We then stop the alert and email you. Turn the scenario on again, then choose Try again in Settings → Integrations.
Airtable
- In your base, create a table called Inquiries with the fields Received, Form, Project and Link.
- Open Automations and create an automation. Choose the trigger When webhook received and copy its link.
- In makeform.live, add a Webhook alert, paste the link and choose Send test and add.
- In Airtable, choose Test trigger. Airtable shows our test message.
- Add the action Create record in Inquiries, and map Received to
data.inquiry.receivedAt, Form todata.form.name, Project todata.project.nameand Link todata.inquiry.url. - Turn the automation on. Delete any test record.
If Airtable stops adding records
Airtable’s free plan includes a limited number of automation runs a month per workspace, and failed runs count. Each inquiry uses one run. When the runs are used up, Airtable stops adding records and may not tell makeform.live, so check your usage in Airtable.
n8n and your own server
Use n8n’s Webhook node (its production URL), or any public https:// endpoint. Check the signature as shown below.
What we send
{
"type": "inquiry.received",
"timestamp": "2026-10-12T14:05:09Z",
"data": {
"inquiry": {
"id": "3f6c1e2a-8b7d-4c11-9e0f-5a2b7c9d1e44",
"linkId": "R8vT3mQaZ1cE",
"receivedAt": "2026-10-12T14:05:09Z",
"url": "https://makeform.live/forms/k7Q2xLp9Wd3T/responses/R8vT3mQaZ1cE",
"apiUrl": "https://makeform.live/v1/forms/9d2a4b6c-1e3f-4a5b-8c7d-0e1f2a3b4c5d/submissions/3f6c1e2a-8b7d-4c11-9e0f-5a2b7c9d1e44"
},
"form": {
"id": "9d2a4b6c-1e3f-4a5b-8c7d-0e1f2a3b4c5d",
"linkId": "k7Q2xLp9Wd3T",
"name": "Website contact"
},
"project": { "id": "5b8e2f1a-7c3d-4e9a-b6f0-2d4c8a1e3f57", "name": "Fernwood Fitness" },
"alert": { "id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f" }
}
}What each field contains
| Field | What it contains |
|---|---|
type | inquiry.received, or alert.test for Send test. |
inquiry | Its id, link id, when it arrived (UTC), the private link (sign-in needed) and apiUrl, the API address to read it with a token. |
form | Its id, link id and internal name. |
project | Its id and name, or null. |
alert | The alert’s id, and test: true for Send test. |
Send test sends the same shape with example values, so tools like Make.com and Airtable can learn the fields. Ignore alert.test in your own code.
Check the signature
Each message has three headers: webhook-id, webhook-timestamp and webhook-signature. They follow the open Standard Webhooks format, and its libraries can check them for you, for example the standardwebhooks package for Node.js.
Check it yourself
To check it yourself, take the raw request body exactly as received, join webhook-id, webhook-timestamp and the body with full stops, and compute an HMAC-SHA256 with your signing secret. The secret is the part after whsec_, base64-decoded. Compare the result, base64-encoded, with each v1 value in webhook-signature. Reject messages whose timestamp is more than 5 minutes old.
import crypto from "node:crypto";
// rawBody: the request body as a string, before any JSON parsing.
export function fromMakeform(rawBody, headers, secret) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !timestamp || !signatures) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = crypto
.createHmac("sha256", key)
.update(`${id}.${timestamp}.${rawBody}`)
.digest();
return signatures.split(" ").some((entry) => {
const [version, value] = entry.split(",");
if (version !== "v1" || !value) return false;
const given = Buffer.from(value, "base64");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}Your signing secret is shown once, when you add the webhook.

To replace it, choose New signing secret in Settings → Integrations. The old secret stops working straight away.
Answer quickly; retries
Answer with any 2xx status within 10 seconds. We don’t read your response body. We don’t follow redirects, except Google Apps Script’s own.
| When | What we do |
|---|---|
| Your app doesn’t answer or returns an error | We try again several times over about an hour, waiting longer each time. Every retry of one message has the same webhook-id, so you can ignore repeats. Messages can arrive out of order. |
| Your app answers 410 Gone | We stop the webhook straight away and email you. Other errors are retried. |
| 3 inquiries in a row can’t be delivered | We stop it and email you. See If an alert stops. |
We don’t publish a fixed list of IP addresses; check the signature instead.
Read the answers
Webhooks never contain answers. To read them, create an API token in Settings → Integrations with permission to read inquiries for the forms you need, then call inquiry.apiUrl with the header Authorization: Bearer YOUR_TOKEN. Tokens expire; create a new one before yours does.