Webhooks
When a form submission completes, Form Plant can POST the submitted data as JSON to external URLs (added in v1.4.0). This is the entry point for connecting external services such as Zapier, Make, Google Apps Script, or your own API.
Setup
- Open the "Integrations" tab on the form edit screen
- Click "Add webhook" (up to 3 per form)
- Configure each webhook and save the form
| Setting | Description |
|---|---|
| Enable / Disable | Turns delivery on or off |
| Destination URL | Where the JSON is POSTed. HTTPS only |
| Signing secret | Generated automatically. Used for verifying authenticity on the receiving side (can be regenerated) |
| Send test | Sends a dummy payload immediately and shows the result (HTTP status) inline |
The test payload uses the event name submission.test and contains a single sample field.
To verify your real field structure, submit the form itself.
The request
Headers
Content-Type: application/json; charset=utf-8
X-FPlant-Event: submission.completed
X-FPlant-Delivery: {UUID unique per delivery; the retry reuses the same value}
X-FPlant-Signature: sha256={HMAC-SHA256 signature}
Body (JSON)
{
"event": "submission.completed",
"form_id": 973,
"form_title": "Contact",
"submission_id": 1234,
"submitted_at": "2026-07-12T18:30:00+09:00",
"site_url": "https://example.com",
"fields": [
{ "key": "your_name", "label": "Name", "type": "name_parts", "value": "Taro Yamada" },
{ "key": "topics", "label": "Topics", "type": "checkbox", "value": ["Brochure", "Quote"] }
]
}
valuecontains the same formatted value used in email bodies. Multi-select fields (checkboxes) are arrays- File uploads expose the file name only (file URLs are omitted on purpose — they would be reachable by anyone who sees the payload)
submitted_atis ISO 8601 in the site's timezone
Delivery behavior
- When it fires: after the notification emails. Forms with a confirmation screen fire only on the final submission. Editor preview submissions and submissions blocked by spam protection never fire
- Success: HTTP 2xx / 3xx is recorded as success (redirects are not followed). Services that respond with a redirect, such as Google Apps Script (302), are recorded as successful
- Automatic retry: on failure (connection error / timeout / 4xx / 5xx), one retry runs automatically 60 seconds later
- Delivery log: the submission detail screen shows the result (success / failure and the HTTP code) per URL
When the form is set not to store submissions, delivery results are not recorded and the automatic retry does not run (there is nowhere to store them). The delivery itself still happens.
What the signing secret is for
A webhook URL can leak or be guessed, and anyone who knows it can POST fake JSON to it. The receiving side therefore needs a way to confirm that a request really came from your form.
Form Plant signs every request body with HMAC-SHA256 using the secret and sends the result in
the X-FPlant-Signature header. If the receiver computes the same signature and compares,
forged requests and tampered payloads are detected.
// Verification example in PHP (receiving side)
$raw_body = file_get_contents( 'php://input' );
$signature = 'sha256=' . hash_hmac( 'sha256', $raw_body, $secret ); // $secret = the secret shown in the settings
if ( ! hash_equals( $signature, $_SERVER['HTTP_X_FPLANT_SIGNATURE'] ?? '' ) ) {
http_response_code( 401 ); // signature mismatch = forged, reject
exit;
}
How to treat the secret per receiver:
| Receiver | Signing secret |
|---|---|
| Google Apps Script | Cannot be used (GAS web apps cannot read request headers). Security relies on the URL being hard to guess |
| Zapier / Make | Usually unused (relies on keeping the receiving URL private). Verification is possible with extra steps |
| Your own API (PHP etc.) | Verification recommended — the code above rejects forged or tampered requests |
Google Apps Script example (log to a spreadsheet)
This example appends one row per submission to a Google Sheet.
- In the spreadsheet, open "Extensions" → "Apps Script" and paste the code below
- "Deploy" → "New deployment" → type "Web app", set "Who has access" to "Anyone", then deploy
- Set the web app URL as the destination URL in Form Plant
function doPost(e) {
try {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var data = JSON.parse(e.postData.contents);
// Row 1 is the header row; create the base columns on an empty sheet
var lastCol = sheet.getLastColumn();
var headers = lastCol > 0 ? sheet.getRange(1, 1, 1, lastCol).getValues()[0] : [];
if (headers.length === 0) {
headers = ['Received at', 'Submission ID'];
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
}
// Build the row, aligning values with the header positions
var row = [];
row[0] = new Date();
row[1] = data.submission_id;
(data.fields || []).forEach(function (field) {
var header = field.label || field.key; // field labels become column headers
var col = headers.indexOf(header);
if (col === -1) {
// Unknown fields get a new column on the right
headers.push(header);
col = headers.length - 1;
sheet.getRange(1, col + 1).setValue(header);
}
// Join multi-select values (checkboxes) with commas
row[col] = Array.isArray(field.value) ? field.value.join(', ') : field.value;
});
// Fill gaps with empty strings before appending
for (var i = 0; i < headers.length; i++) {
if (row[i] === undefined) row[i] = '';
}
sheet.appendRow(row);
return ContentService.createTextOutput(JSON.stringify({ status: 'success' }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
console.error(error);
return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: error.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}
The header row is managed automatically: adding a field to the form adds a column on the right.
- The delivery log shows "HTTP 302 / success" — GAS always responds with a redirect after the script runs successfully
- Redeploy after changing the code. Saving in the editor is not enough: use "Deploy" → "Manage deployments" → pencil icon → "Version: New version" → "Deploy" (this keeps the URL; creating a new deployment changes the URL and requires updating Form Plant)
- To skip logging test deliveries, add a guard like
if (data.event !== 'submission.completed') return ...
Zapier / Make
Both services provide a webhook trigger:
- Zapier: choose "Webhooks by Zapier" → "Catch Hook" and set the displayed URL as the destination
- Make: create "Webhooks" → "Custom webhook" and set the displayed URL as the destination
After submitting the form once (or using the test button), the field structure is recognized and the values become available to later steps.
For developers: customization filters
| Filter | Purpose |
|---|---|
fplant_webhook_should_send | Conditionally skip a delivery (return false) |
fplant_webhook_payload | Modify the payload (add / remove items) |
fplant_webhook_request_args | Change the HTTP request arguments (timeout etc.) |
fplant_webhook_allow_http | Allow http:// URLs (local development) |
See the PHP hook reference for details.