Webhooks to save to Airtable or Google Sheets

Reading the information about forms, there is mention of Zapier, Make, or Pipedream for using web hooks but I want to be able to save to a Google sheet or better yet, an Airtable base.

How does one achieve that?

With Classic, I have successfully been using Formloom which makes it quite easy to save to a Google sheet.

Oh, also, in the Youtube tutorial, it mentions this is only in the Pro version. Is that still the case as I have the Plus version?

Webhooks are now available in all versions of Elements :slightly_smiling_face:

Here’s how it works:

  1. Get a Webhook URL from Zapier In Zapier, create a new Zap and choose Webhook as the trigger. Zapier will generate a unique URL for you.
  2. Add the Webhook to your Elements Form In Elements, open your form settings and paste the Zapier URL into the Webhook field. This tells Elements where to send the data when someone submits the form.
  3. Submit the Form When a user fills out your form and hits submit, the data is automatically sent via the webhook to Zapier.
  4. Process the Data in Zapier From there, you can connect Zapier to thousands of services. For example:
    • Send the data straight into Google Sheets
    • Add new entries to Airtable
    • Trigger emails, Slack notifications, or just about anything else Zapier supports

This setup takes a few minutes, but it’s incredibly powerful and flexible. Once connected, you can automate workflows, keep your data synced, and push form submissions wherever you need them.

Thanks Dan. I understand that and had watched your video explaining the process but what if I don’t want to use Zapier? I do not want another subscription for a simple thing like this.

1 Like

For personal interest, I developed a component to allow you to GET/POST information to a website. Would that be useful. The code is ugly a.t.m. but I can clean it up if required

Yes - not much else to say

After a deeper look into Google Sheets i have created a lokal solution.

  1. Elements form sends webhook to /webhook.php
  2. webhook.php has the Table-Code and the Sheet-Name to push the form-data
  3. Google Sheet hast an Apps Script to put the data into a sheet

Interested ? Please post.

The webhook feature in the Form component allows you to enter any URL.

The data from your form will be submitted via a POST request to the URL you entered in Elements — this means you are free to use another service or even write your own script that processes the form data.

Hope that helps :slight_smile:

  1. Create a Sheet on Google Sheets.
  2. Save the SHEET_ID stored in the URL before /edit.

image

  1. Paste this as code.gs in the editor
/***** Einstellungen *****/
// Felder/Spalten, die NICHT angelegt/geschrieben werden
const RESERVED_KEYS = [
  'token',
  'hp',
  'config_path',
  'source',
  'Raw JSON',
  '_content_type',
  '_remote_ip',
  '_server_ts',
  '_source',
  '_user_agent',
  'SHEET_ID',
  'SHEET_NAME'
];

// Bereits vorhandene verbotene Spalten physisch entfernen?
const TRIM_RESERVED_COLUMNS = true;

/***** Helpers *****/
function parseRequest_(e) {
  if (!e) return {};
  if (e.postData && e.postData.type && e.postData.type.indexOf('application/json') !== -1) {
    try { return JSON.parse(e.postData.contents || '{}'); } catch (_) { return {}; }
  }
  return e.parameter || {};
}
function readHeader_(sh) {
  const lastCol = sh.getLastColumn();
  if (sh.getLastRow() === 0 || lastCol === 0) return [];
  return sh.getRange(1, 1, 1, lastCol).getValues()[0].map(v => String(v || ''));
}
function writeHeader_(sh, header) {
  if (header.length === 0) header = [''];
  sh.getRange(1, 1, 1, header.length).setValues([header]);
}
function trimReservedColumns_(sh, header) {
  const toDelete = [];
  header.forEach((t, idx) => { if (RESERVED_KEYS.includes(t)) toDelete.push(idx + 1); });
  toDelete.sort((a, b) => b - a).forEach(colIdx => sh.deleteColumn(colIdx));
}
function ensureHeader_(sh, incomingKeys) {
  let header = readHeader_(sh);

  // Optional: verbotene Spalten entfernen
  if (TRIM_RESERVED_COLUMNS && header.length > 0) {
    const before = header.slice();
    trimReservedColumns_(sh, header);
    header = readHeader_(sh);
    if (header.join('|') !== before.join('|')) writeHeader_(sh, header);
  }

  // Neue, erlaubte Keys ergänzen
  const toAdd = [];
  for (const k of incomingKeys) {
    if (!k) continue;
    if (RESERVED_KEYS.includes(k)) continue;
    if (!header.includes(k)) toAdd.push(k);
  }

  if (header.length === 0) {
    header = toAdd.slice();
    writeHeader_(sh, header);
  } else if (toAdd.length) {
    header = header.concat(toAdd);
    writeHeader_(sh, header);
  }
  return header;
}

/***** Web-App *****/
function doPost(e) {
  const req = parseRequest_(e);

  // Pflicht: Token & Sheet-Parameter
  const tokenCfg = PropertiesService.getScriptProperties().getProperty('WEBHOOK_TOKEN') || '';
  const provided = (req && (req.token || req.TOKEN)) || (e && e.parameter && e.parameter.token) || '';
  if (!tokenCfg || provided !== tokenCfg) {
    return ContentService.createTextOutput(JSON.stringify({ ok: false, error: 'unauthorized' }))
      .setMimeType(ContentService.MimeType.JSON);
  }
  const sheetId   = req.SHEET_ID   || (e && e.parameter && e.parameter.SHEET_ID);
  const sheetName = req.SHEET_NAME || (e && e.parameter && e.parameter.SHEET_NAME);
  if (!sheetId || !sheetName) {
    return ContentService.createTextOutput(JSON.stringify({ ok: false, error: 'missing SHEET_ID/SHEET_NAME' }))
      .setMimeType(ContentService.MimeType.JSON);
  }

  const ss = SpreadsheetApp.openById(String(sheetId));
  const sh = ss.getSheetByName(String(sheetName)) || ss.insertSheet(String(sheetName));

  const incomingKeys = Object.keys(req || {});
  const header = ensureHeader_(sh, incomingKeys);
  if (header.length === 0) {
    return ContentService.createTextOutput(JSON.stringify({ ok: true, note: 'no allowed fields' }))
      .setMimeType(ContentService.MimeType.JSON);
  }

  const row = header.map(h => Object.prototype.hasOwnProperty.call(req, h) ? String(req[h]) : '');
  sh.appendRow(row);

  return ContentService.createTextOutput(JSON.stringify({ ok: true }))
    .setMimeType(ContentService.MimeType.JSON);
}

function doGet() {
  return ContentService.createTextOutput(JSON.stringify({ ok: true, when: (new Date()).toISOString() }))
    .setMimeType(ContentService.MimeType.JSON);
}
  1. Choose BereitstellenNeue Bereitstellung

  1. Set the Parameters:

  1. Klick Bereitstellen
  2. Save the Web-App URL
  3. At the Project-Settings add a Script-Attribut WEBHOOK_TOKEN (save this)
  4. Add a file webhook.php in Elements Root
<?php
/**
 * webhook.php — Forwarder zu Google Apps Script (Pflicht-Parameter)
 * Erfordert in der URL:
 *   ?APPS_URL=...&TOKEN_FALLBACK=...&SHEET_ID=...&SHEET_NAME=...
 * Nimmt POST (x-www-form-urlencoded/multipart) und JSON an,
 * hängt token + SHEET_ID/SHEET_NAME an und postet zu Apps Script /exec.
 * Folgt 302 NICHT. Antwort ist JSON.
 */

declare(strict_types=1);

const CA_BUNDLE       = ''; // optional, falls php.ini kein curl.cainfo/openssl.cafile gesetzt hat
const LOG_FILE        = ''; // optional: z. B. 'C:\inetpub\logs\webhook-forwarder\webhook.log'
const ADD_META_FIELDS = true;

@date_default_timezone_set('Europe/Berlin');
header('Content-Type: application/json; charset=utf-8');

/* ---------- helpers ---------- */
function log_line(string $level, string $msg, array $ctx = []): void
{
    if (!LOG_FILE) return;
    $line = sprintf("[%s] %s: %s %s\n",
        (new DateTimeImmutable())->format('Y-m-d\TH:i:s.uP'),
        $level, $msg,
        $ctx ? json_encode($ctx, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) : ''
    );
    @file_put_contents(LOG_FILE, $line, FILE_APPEND|LOCK_EX);
}
function json_out(int $code, array $data): never
{
    http_response_code($code);
    echo json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
    exit;
}
function validate_apps_url(string $url): bool
{
    $p = @parse_url($url);
    if (!$p) return false;
    if (($p['scheme'] ?? '') !== 'https') return false;
    if (($p['host'] ?? '') !== 'script.google.com') return false;
    return (bool)preg_match('~^/macros/s/[A-Za-z0-9_-]+/exec$~', $p['path'] ?? '');
}
function require_query_params(): array
{
    $raw = $_SERVER['QUERY_STRING'] ?? '';
    parse_str(str_replace(';','&',$raw), $qs);

    $appsUrl = $qs['APPS_URL']       ?? null;
    $token   = $qs['TOKEN_FALLBACK'] ?? null;
    $sheetId = $qs['SHEET_ID']       ?? null;
    $sheetNm = $qs['SHEET_NAME']     ?? null;

    if (!$appsUrl || !$token || !$sheetId || !$sheetNm) {
        json_out(400, ['ok'=>false,'error'=>'Missing URL params: APPS_URL, TOKEN_FALLBACK, SHEET_ID, SHEET_NAME']);
    }
    if (!validate_apps_url($appsUrl)) {
        json_out(400, ['ok'=>false,'error'=>'Invalid APPS_URL']);
    }
    // Sheet-ID grob validieren (Buchstaben/Zahlen/_-; Länge > 20)
    if (!preg_match('~^[A-Za-z0-9_-]{20,}$~', $sheetId)) {
        json_out(400, ['ok'=>false,'error'=>'Invalid SHEET_ID format']);
    }
    // Sheet-Name >0 Zeichen (Umlaute/Spaces ok)
    if (!is_string($sheetNm) || $sheetNm === '') {
        json_out(400, ['ok'=>false,'error'=>'Invalid SHEET_NAME']);
    }
    return [$appsUrl, (string)$token, (string)$sheetId, (string)$sheetNm];
}

/* ---------- request ---------- */
log_line('INFO','incoming',[
    'method'=>$_SERVER['REQUEST_METHOD'] ?? '',
    'ip'=>$_SERVER['REMOTE_ADDR'] ?? '',
    'ctype'=>$_SERVER['CONTENT_TYPE'] ?? ($_SERVER['HTTP_CONTENT_TYPE'] ?? ''),
    'ua'=>$_SERVER['HTTP_USER_AGENT'] ?? '',
    'uri'=>$_SERVER['REQUEST_URI'] ?? '',
]);

if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
    json_out(405, ['ok'=>false,'error'=>'Only POST allowed']);
}

[$appsUrl, $token, $sheetId, $sheetName] = require_query_params();

$ctype   = $_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? '';
$rawBody = file_get_contents('php://input') ?: '';
$payload = [];

// JSON oder Form
if (stripos($ctype, 'application/json') !== false) {
    $tmp = json_decode($rawBody, true);
    if (is_array($tmp)) $payload = $tmp;
}
if (!$payload) $payload = $_POST ?: [];

// Honeypot → stiller Erfolg
if (!empty($payload['hp'])) {
    log_line('INFO','honeypot'); json_out(200, ['ok'=>true]);
}

// optionale Meta
if (ADD_META_FIELDS) {
    $payload['_server_ts']    = (new DateTimeImmutable())->format('c');
    $payload['_remote_ip']    = $_SERVER['REMOTE_ADDR'] ?? '';
    $payload['_content_type'] = $ctype;
    $payload['_user_agent']   = $_SERVER['HTTP_USER_AGENT'] ?? '';
    $payload['_source']       = 'rw_webhook_php';
}

// Pflichtwerte einfügen/überschreiben
$payload['token']      = $token;
$payload['SHEET_ID']   = $sheetId;
$payload['SHEET_NAME'] = $sheetName;

/* ---------- forward ---------- */
$postFields = http_build_query($payload, '', '&', PHP_QUERY_RFC3986);

$ch = curl_init($appsUrl);
if ($ch === false) json_out(500, ['ok'=>false,'error'=>'cURL init failed']);

$opts = [
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/x-www-form-urlencoded; charset=UTF-8'],
    CURLOPT_POSTFIELDS     => $postFields,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 20,
    CURLOPT_FOLLOWLOCATION => false, // 302 nicht folgen
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_USERAGENT      => 'cornrow-webhook/1.0',
];
if (CA_BUNDLE) $opts[CURLOPT_CAINFO] = CA_BUNDLE;

curl_setopt_array($ch, $opts);

$respBody = curl_exec($ch);
$errno    = curl_errno($ch);
$error    = $errno ? curl_error($ch) : '';
$status   = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($errno) {
    $hint = ($errno === 60) ? ' (CA bundle fehlt? php.ini curl.cainfo/openssl.cafile setzen)' : '';
    log_line('ERROR','curl', ['errno'=>$errno,'error'=>$error]);
    json_out(502, ['ok'=>false,'error'=>"Upstream TLS/HTTP error: $error$hint"]);
}
if ($status >= 200 && $status < 400) {
    log_line('INFO','forwarded',['status'=>$status]);
    json_out(200, ['ok'=>true,'status'=>$status]);
}
log_line('ERROR','upstream',['status'=>$status,'body'=>$respBody]);
json_out($status ?: 500, ['ok'=>false,'error'=>'Upstream HTTP '.($status ?: 0),'body'=>$respBody]);
  1. Create a Form with Fields
  2. Use Webhook in your form. The URL should be like
    https://www.yourdomain.tld/webhook.php?TOKEN_FALLBACK=aaa;APPS_URL=bbb;SHEET_ID=ccc;SHEET_NAME=ddd
  • aaa = Your WEBHOOK_TOKEN
  • bbb = Your APPS_URL
  • ccc = Your SHEET_ID
  • ddd = Sheet Name (it will be created)
1 Like

Why doing it this way?

  • Elements supports it
  • No need of an external service
  • It is possible

Thank you very much for the information. (or should I say vielen Dank)

Bei Fragen - fragen. Ich würde mich freuen, wenn das eingesetzt würde.