Skip to main content

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

  1. Open the "Integrations" tab on the form edit screen
  2. Click "Add webhook" (up to 3 per form)
  3. Configure each webhook and save the form
SettingDescription
Enable / DisableTurns delivery on or off
Destination URLWhere the JSON is POSTed. HTTPS only
Signing secretGenerated automatically. Used for verifying authenticity on the receiving side (can be regenerated)
Send testSends a dummy payload immediately and shows the result (HTTP status) inline
The test button sends dummy data

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"] }
]
}
  • value contains 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_at is 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
If submission storage is disabled

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:

ReceiverSigning secret
Google Apps ScriptCannot be used (GAS web apps cannot read request headers). Security relies on the URL being hard to guess
Zapier / MakeUsually 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.

  1. In the spreadsheet, open "Extensions" → "Apps Script" and paste the code below
  2. "Deploy" → "New deployment" → type "Web app", set "Who has access" to "Anyone", then deploy
  3. 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.

Notes for GAS
  • 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

FilterPurpose
fplant_webhook_should_sendConditionally skip a delivery (return false)
fplant_webhook_payloadModify the payload (add / remove items)
fplant_webhook_request_argsChange the HTTP request arguments (timeout etc.)
fplant_webhook_allow_httpAllow http:// URLs (local development)

See the PHP hook reference for details.