# Webhooks to save to Airtable or Google Sheets

**URL:** <https://forums.realmacsoftware.com/t/webhooks-to-save-to-airtable-or-google-sheets/51297>\
**Category:** Elements\
**Created:** [September 25, 2025, 6:57am UTC](https://forums.realmacsoftware.com/t/webhooks-to-save-to-airtable-or-google-sheets/51297 "2025-09-25T06:57:59Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![martef86](https://avatars.discourse-cdn.com/v4/letter/m/898d66/32.png) [@martef86](https://forums.realmacsoftware.com/u/martef86)\
**Post date:** [September 25, 2025, 6:57am UTC](https://forums.realmacsoftware.com/t/webhooks-to-save-to-airtable-or-google-sheets/51297/1 "2025-09-25T06:57:59Z")

</div>

Reading the information about forms, there is mention of [Zapier](https://zapier.com/), [Make](https://www.make.com/en), or [Pipedream](https://pipedream.com/) 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?

---

<div class="post-metadata">

**Author:** ![dan](https://dub1.discourse-cdn.com/flex005/user_avatar/forums.realmacsoftware.com/dan/32/28516_2.png) [@dan](https://forums.realmacsoftware.com/u/dan)\
**Post date:** [September 25, 2025, 8:20am UTC](https://forums.realmacsoftware.com/t/webhooks-to-save-to-airtable-or-google-sheets/51297/2 "2025-09-25T08:20:25Z")

</div>

Webhooks are now available in **all versions of Elements** 🙂

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.

---

<div class="post-metadata">

**Author:** ![martef86](https://avatars.discourse-cdn.com/v4/letter/m/898d66/32.png) [@martef86](https://forums.realmacsoftware.com/u/martef86)\
**Post date:** [September 25, 2025, 11:21am UTC](https://forums.realmacsoftware.com/t/webhooks-to-save-to-airtable-or-google-sheets/51297/3 "2025-09-25T11:21:45Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![RussOggi255](https://avatars.discourse-cdn.com/v4/letter/r/ee7513/32.png) [@RussOggi255](https://forums.realmacsoftware.com/u/RussOggi255)\
**Post date:** [September 26, 2025, 7:26am UTC](https://forums.realmacsoftware.com/t/webhooks-to-save-to-airtable-or-google-sheets/51297/4 "2025-09-26T07:26:09Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![upssjw](https://dub1.discourse-cdn.com/flex005/user_avatar/forums.realmacsoftware.com/upssjw/32/26063_2.png) [@upssjw](https://forums.realmacsoftware.com/u/upssjw)\
**Post date:** [September 26, 2025, 7:48am UTC](https://forums.realmacsoftware.com/t/webhooks-to-save-to-airtable-or-google-sheets/51297/5 "2025-09-26T07:48:33Z")

</div>

Yes - not much else to say

---

<div class="post-metadata">

**Author:** ![Ulrich](https://dub1.discourse-cdn.com/flex005/user_avatar/forums.realmacsoftware.com/ulrich/32/29764_2.png) [@Ulrich](https://forums.realmacsoftware.com/u/Ulrich)\
**Post date:** [September 26, 2025, 11:44am UTC](https://forums.realmacsoftware.com/t/webhooks-to-save-to-airtable-or-google-sheets/51297/6 "2025-09-26T11:44:32Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![ben](https://dub1.discourse-cdn.com/flex005/user_avatar/forums.realmacsoftware.com/ben/32/28488_2.png) [@ben](https://forums.realmacsoftware.com/u/ben)\
**Post date:** [September 26, 2025, 12:28pm UTC](https://forums.realmacsoftware.com/t/webhooks-to-save-to-airtable-or-google-sheets/51297/7 "2025-09-26T12:28:35Z")

</div>

> [@martef86](#):
>
> what if I don’t want to use Zapier?

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 🙂

---

<div class="post-metadata">

**Author:** ![Ulrich](https://dub1.discourse-cdn.com/flex005/user_avatar/forums.realmacsoftware.com/ulrich/32/29764_2.png) [@Ulrich](https://forums.realmacsoftware.com/u/Ulrich)\
**Post date:** [September 26, 2025, 3:19pm UTC](https://forums.realmacsoftware.com/t/webhooks-to-save-to-airtable-or-google-sheets/51297/8 "2025-09-26T15:19:00Z")

</div>

1. Create a Sheet on Google Sheets.
2. Save the SHEET\_ID stored in the URL before `/edit`.

![image](https://europe1.discourse-cdn.com/flex005/uploads/realmacsoftware1/original/3X/e/8/e866ae1c81eb46d1c982142f928635628040a33e.png)

1. Paste this as `code.gs` in the editor

```auto
/ *****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 `Bereitstellen` – `Neue Bereitstellung`

 ![image](https://europe1.discourse-cdn.com/flex005/uploads/realmacsoftware1/original/3X/4/9/49bb9a6c1639fc612f5128f50ac32c263f8e3fc7.png)

1. Set the Parameters:

 ![image](https://europe1.discourse-cdn.com/flex005/uploads/realmacsoftware1/original/3X/d/0/d0617e19f0ec9b3c95cb3a3838ac5bc97e46549a.png)

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

```auto
<?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)

---

<div class="post-metadata">

**Author:** ![Ulrich](https://dub1.discourse-cdn.com/flex005/user_avatar/forums.realmacsoftware.com/ulrich/32/29764_2.png) [@Ulrich](https://forums.realmacsoftware.com/u/Ulrich)\
**Post date:** [September 26, 2025, 3:21pm UTC](https://forums.realmacsoftware.com/t/webhooks-to-save-to-airtable-or-google-sheets/51297/9 "2025-09-26T15:21:59Z")

</div>

Why doing it this way?

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

---

<div class="post-metadata">

**Author:** ![martef86](https://avatars.discourse-cdn.com/v4/letter/m/898d66/32.png) [@martef86](https://forums.realmacsoftware.com/u/martef86)\
**Post date:** [September 27, 2025, 6:41am UTC](https://forums.realmacsoftware.com/t/webhooks-to-save-to-airtable-or-google-sheets/51297/10 "2025-09-27T06:41:52Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![Ulrich](https://dub1.discourse-cdn.com/flex005/user_avatar/forums.realmacsoftware.com/ulrich/32/29764_2.png) [@Ulrich](https://forums.realmacsoftware.com/u/Ulrich)\
**Post date:** [September 27, 2025, 7:54am UTC](https://forums.realmacsoftware.com/t/webhooks-to-save-to-airtable-or-google-sheets/51297/11 "2025-09-27T07:54:30Z")

</div>

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