# formbase Docs — Integrations

# Integrations overview

Connect formbase to scheduling, analytics, spreadsheets, messaging, project tools, and custom webhooks.

## Connect your workflow

Schedule appointments, stream analytics, deliver submissions, and connect formbase to tools your team already uses.

<h2 id="three-models">Three connection models</h2>

<p>
  Integration setup differs by feature. Knowing which model you are using explains where configuration lives and what starts happening after
  you connect.
</p>

<h2 id="available">Available connections</h2>

<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Cal.com](/integrations/cal-com) — Book appointments inside forms
  - [Analytics streaming](/integrations/analytics-streaming) — Send events to GA4 or Axiom
  - [Google Analytics (GA4)](/integrations/google-analytics) — Add form events to your GA4 property
  - [Axiom](/integrations/axiom) — Query events and optional respondent answers
  - [Google Sheets](/integrations/google-sheets) — Append submissions to a spreadsheet
  - [Airtable](/integrations/airtable) — Create records in an Airtable base
  - [Notion](/integrations/notion) — Create pages in a Notion database
  - [Slack](/integrations/slack) — Post submissions to Slack channels
  - [Discord](/integrations/discord) — Post submissions to Discord channels
  - [Linear](/integrations/linear) — Create issues from submissions
  - [GitHub](/integrations/github) — Create GitHub issues from submissions
  - [Webhooks](/integrations/webhooks) — Send signed JSON to your endpoint
</div>

<p>
  Zapier and Make also appear in the provider grid, marked <strong>Coming soon</strong>: the cards are visible, but setup is blocked until
  those apps ship. Zapier already has a formbase app you connect from inside Zapier; drive Make with its HTTP module.
</p>

> ℹ️ **Step by step in Zapier**
> <p>
>     The <a href="/guides/overview">Zapier guides</a> connect the formbase app for Zapier and walk through each trigger and action click by
>     click.
>   </p>

<h2 id="plans">Plan availability</h2>

<ul>
  <li>Cal.com and every submission integration are available on every plan.</li>
  <li>
    Sending abandoned submissions to integrations requires Pro or Business. Every submission integration except Webhook supports abandoned
    events.
  </li>
  <li>Google Analytics and Axiom analytics streaming are available on Free, Pro, and Business.</li>
  <li>Axiom respondent-answer streaming is included with analytics streaming, but stays off until enabled on a specific form.</li>
</ul>

> ⚠️ **Review data before sending it elsewhere**
> <p>
>     Integrations can send respondent data to services you control. Configure only destinations you trust, map only fields you need, and make
>     sure your respondent notice and processing agreements cover those transfers. Abandoned submissions are especially sensitive because the
>     respondent has not pressed Submit.
>   </p>

<h2 id="reliability">Delivery and connection health</h2>

<p>
  Submission integrations use a durable delivery queue: up to 5 attempts per submission with exponential backoff, an event log on each
  integration, a failure email, and a <strong>Retry all</strong> button for deliveries that ran out of attempts. OAuth connections are
  refreshed automatically when providers support refresh tokens. See <a href="/integrations/health-monitoring">Integration health</a>.
</p>

<p>
  When a destination supports field mapping or message templates, you can include <strong>PDF Link</strong> to send a link to the generated
  submission PDF with each delivery.
</p>

<p>
  Analytics event forwarding is intentionally different: it is best-effort and has no retry queue. Axiom respondent answers use durable
  delivery because each answer payload represents one submission.
</p>


# Cal.com scheduling

Let respondents choose and book a Cal.com appointment without leaving your form.

## Cal.com scheduling

Add a native Schedule appointment question, connect Cal.com, and let respondents book an available slot inside the form.

> ℹ️ **Available on every plan**
> <p>Cal.com scheduling is available on Free, Pro, and Business plans.</p>

<h2 id="native-workflow">What native scheduling means</h2>

<p>
  Schedule appointment is a formbase question, not an iframe. It uses your form's theme and language, participates in conditional logic, and
  records confirmed booking details with the submission. Cal.com supplies event types, availability, calendar invitations, and meeting
  links.
</p>

<ul>
  <li>Show or hide scheduling based on earlier answers.</li>
  <li>Require every visible Schedule appointment question to be booked before submission.</li>
  <li>Prefill attendee name and email from earlier questions.</li>
  <li>Show available dates and times in the respondent's time zone.</li>
  <li>Include booking details in submission views, PDFs, notifications, exports, and integrations.</li>
</ul>

<h2 id="connect">Connect Cal.com</h2>

<p>
  You can also connect accounts from <strong>Workspace → Integrations → Scheduling → Cal.com</strong>. Connections belong to the workspace,
  so workspace members can reuse them on other forms and connect additional Cal.com accounts.
</p>

<h2 id="configure">Configure the question</h2>

<p>
  If no fields are mapped, respondents enter their name and email in the scheduling question. When fields are mapped, their answers flow
  into booking details automatically.
</p>

<h2 id="respondent-experience">Respondent experience</h2>

<ol>
  <li>
    The respondent picks a date, then an available time. Times show in the time zone their browser reports; they can pick another from the{' '}
    <strong>Time zone</strong> list, and the booking uses the one they chose.
  </li>
  <li>
    They confirm attendee name and email, then press <strong>Book</strong>.
  </li>
  <li>Cal.com creates the booking and sends its normal confirmation and calendar invitation.</li>
  <li>
    formbase shows the booking back to them as <strong>What</strong>, <strong>When</strong>, <strong>Who</strong>, and{' '}
    <strong>Where</strong>, with the meeting link.
  </li>
  <li>
    Before submitting, they can press <strong>Cancel booking</strong> and choose another time.
  </li>
</ol>

> ℹ️ **Booking happens before form submission**
> <p>
>     Cal.com confirms the appointment when the respondent clicks Book. The booking then attaches to the form submission when they submit the
>     form. Until then the appointment exists on the host's calendar even if the respondent walks away. On a public link, formbase releases it
>     again when the form is still not submitted 24 hours after booking; on a request, when the request is cancelled or expires. It is also
>     released when you delete the submission or the form.
>   </p>

<p>
  If a time is taken by someone else between listing and booking, the respondent is told so and sees the times that are still free. If a
  booking attempt fails halfway, formbase cancels the calendar entry it created rather than leave the host with a booking the form does not
  know about, and a second device cannot book the same question twice.
</p>

<h2 id="logic">Use scheduling with logic</h2>

<p>
  Logic can show or hide Schedule appointment like any other question. Example: ask whether a lead has budget and authority; reveal booking
  only when both answers qualify.
</p>

<p>
  A visible Schedule appointment question must have a confirmed booking before submission. A hidden one does not block submission. Once a
  respondent books, the answers that decide whether the question shows are locked, so a later answer cannot hide the booking by accident; to
  change one, they cancel the booking first. If the question is hidden when the form is submitted anyway, for example because a branching
  rule skipped its page, that booking is cancelled on Cal.com, so the host's calendar matches the submission.
</p>

<h2 id="publish-checks">Publish checks</h2>

<p>Publishing is blocked while a Schedule appointment question is missing either half of its setup:</p>

<ul>
  <li>
    <strong>Connect Cal.com before publishing forms that schedule appointments.</strong> — an event type is selected but no account is.
  </li>
  <li>
    <strong>Select a scheduler event type before publishing this Schedule appointment question.</strong> — an account is selected but no
    event type is.
  </li>
  <li>
    <strong>Complete setup for this Schedule appointment question before publishing.</strong> — neither is selected.
  </li>
</ul>

<p>
  Each message points at the question that caused it. In the editor, the same three states show on the question as{' '}
  <strong>Not connected</strong>, <strong>Event missing</strong>, and <strong>Configured</strong> badges.
</p>

<p>
  An event type that needs more than a name, an email and a time cannot be booked from a form. Event types that require confirmation, recur,
  have seats, cost money, or ask required booking questions are listed as <strong>Not bookable from a form</strong> and cannot be selected;
  a form that still points at one is blocked from publishing until you choose another event type.
</p>

<h2 id="after-booking">After submission</h2>

<p>
  Booking details appear under the Schedule appointment question in the submission table and detail view, with the date and time in your own
  language. They are also available to notification emails, generated PDFs, CSV/Excel exports, webhooks, and mapped integrations. Webhooks,
  Zapier, n8n, Make, request callbacks and the API receive the booking as data: start and end time, time zone, attendee, meeting link,
  Cal.com booking id and status. See <a href="/developers/webhooks-reference#bookings-and-payments">Bookings and payments</a>.
</p>

<p>
  formbase keeps listening after submission. When the respondent or the host reschedules or cancels through Cal.com, or the host rejects the
  booking or marks a no-show, the booking on the submission is updated: its status changes, and the text shown for it starts with{' '}
  <strong>Rescheduled</strong>, <strong>Cancelled</strong>, <strong>Rejected</strong>, or <strong>No-show</strong>. The table, exports, the
  API and regenerated PDFs show the current state. Integrations are not sent again: the submission itself did not change.
</p>

<p>
  Deleting a submission, or the whole form, cancels its appointments on Cal.com so the host does not keep a slot for answers that no longer
  exist.
</p>

> ⚠️ **Edit after submission is disabled**
> <p>
>     Forms containing Payment, Signature, or Schedule appointment questions cannot let respondents edit answers after submission. formbase
>     turns that setting off during publish so external payment, signature, and booking records cannot drift from submitted answers.
>   </p>

<h2 id="connection-health">Connection health</h2>

<p>
  A daily job refreshes Cal.com OAuth tokens before they expire. A temporary Cal.com outage is retried the next day and changes nothing. If
  access is revoked, the connection is marked expired: the question shows a <strong>Disconnected</strong> badge, booking stops working for
  respondents, and the person who connected the account gets an email naming each published form that books through it. Reconnect Cal.com
  from Workspace → Integrations → Scheduling, then reselect the account and confirm the event type on each affected question.
</p>

<p>
  Disconnecting a Cal.com account from Workspace → Integrations lists the published forms that schedule appointments through it. Those forms
  stop accepting bookings, and their owner is emailed. Reconnecting the same Cal.com account restores them: the questions keep the account,
  so there is nothing to select again.
</p>

<h2 id="next-steps">Next steps</h2>

<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Conditional logic](/building-forms/conditional-logic) — Reveal booking only for qualifying answers
  - [Question types](/building-forms/field-types) — See every question available in formbase
  - [Integration health](/integrations/health-monitoring) — Reconnect expired OAuth accounts
  - [Submission inbox](/submissions-analytics/submission-inbox) — View booking details with submissions
</div>


# Google Sheets

Auto-sync every submission to a Google spreadsheet with custom column mapping.

## Google Sheets

Send every submission to a Google spreadsheet as a new row. Map fields to custom columns, connect multiple Google accounts, and backfill past responses.

<h2 id="what-you-get">What you get</h2>

<ul>
  <li>One row per submission, synced in real time</li>
  <li>Field-level column mapping with custom labels</li>
  <li>Metadata columns (submitted at, respondent email, submission ID, form name, submission status, PDF link, channel, request ID)</li>
  <li>Auto-created spreadsheet per form</li>
  <li>Backfill existing submissions on connect</li>
  <li>Multiple Google accounts per workspace</li>
  <li>Test row to verify the connection before real submissions arrive</li>
</ul>

<h2 id="connect">Connect Google Sheets</h2>

<h2 id="column-mapping">Column mapping</h2>

<p>
  Toggle <strong>Export all form fields</strong> to include every question, or turn it off to pick specific fields and set custom column
  headers.
</p>

<p>
  Column labels default to the question title but can be overridden — helpful when your spreadsheet feeds into another tool that expects
  specific header names.
</p>

<p>
  Values are written by column <em>position</em>, never by matching the header text. Renaming a header in Google Sheets is harmless;
  reordering or deleting a column is not, and puts values in the wrong place. When your mapping or your form's questions change, formbase
  rewrites row 1 of the spreadsheet it created so the header keeps matching what it appends.
</p>

<h3 id="repeating-groups">Repeating groups</h3>

<p>
  A <a href="/building-forms/repeating-groups">repeating group</a> lets respondents add as many entries as they need — like a list of
  guests, each with a name and age. In the field picker you choose how to map it:
</p>

<ul>
  <li>
    <strong>The whole group</strong> maps to one column. Every entry shows as numbered, labeled rows, like "1. Full name: Jane Appleseed,
    Age: 34, 2. Full name: Marcus Lee, Age: 29".
  </li>
  <li>
    <strong>Each member field</strong> maps to its own column. The values from every entry join with <code> · </code>, so the Full name
    column reads "Jane Appleseed · Marcus Lee".
  </li>
</ul>

<p>
  Pick one, the other, or both. If you remove a member field from your form later, it still appears in the picker so past submissions keep
  their column — your mapping never breaks.
</p>

<h3 id="metadata-columns">Metadata columns</h3>

<p>
  By default, Submitted at and Submitter are included. Add or remove any metadata column during setup, and rename any of them like a normal
  column header.
</p>

<h2 id="multiple-accounts">Multiple Google accounts</h2>

<p>
  A workspace can have multiple Google accounts connected, and each form picks one. This works well when different teams manage their own
  Google Drive or you want client spreadsheets in separate accounts.
</p>

<p>
  To add another account, start a new integration setup and authorize with a different Google email. Existing integrations keep their
  current account.
</p>

<h2 id="backfill">Backfill existing submissions</h2>

<p>
  When you first connect, you can export all existing submissions into the spreadsheet. The backfill runs in the background in batches of 50
  rows, with progress shown in the integration panel.
</p>

<p>Rows are written in submission order (oldest first) so the spreadsheet reads chronologically.</p>

<h2 id="auto-create">Auto-created spreadsheets</h2>

<p>
  formbase creates a spreadsheet in your Google Drive when the first submission arrives (not on save). The spreadsheet uses your form's
  name, or the custom name you set during setup. If you delete the spreadsheet, formbase creates a new one on the next submission — you
  don't lose any data.
</p>

<h2 id="test-event">Test event</h2>

<p>
  The integration detail panel has a <strong>Send test</strong> button. It appends a placeholder row to the spreadsheet so you can verify
  column mapping without waiting for a real submission. Question columns show
  <code>[Question Title]</code> placeholders; metadata columns show synthesized values.
</p>

<h2 id="abandoned-responses">Abandoned response events</h2>

<p>
  If a respondent starts your form but doesn't finish, formbase can still sync the partial data. On the{' '}
  <strong>Abandoned submissions</strong> step, set <strong>Send abandoned event after</strong> to 12 hours, 1 day, 3 days, or 1 week. An
  hourly sweep then appends a row with whatever the respondent filled in, using the same column mapping. The Submission status column reads
  "Partial" so you can filter incomplete responses.
</p>

<p>
  Only drafts from a share link are swept, and <strong>Required fields</strong> narrows it further: the row is appended only when at least
  one of the fields you pick was filled in.
</p>

> ℹ️ **Pro feature**
> <p>
>     Abandoned response delivery requires a Pro or Business plan. The core Google Sheets integration is available on all plans. See{' '}
>     <a href="/subscription-billing/plans-pricing">plans & pricing</a>.
>   </p>

<h2 id="error-handling">Error handling</h2>

<p>
  After 5 consecutive failures of any kind, the integration auto-pauses with an error status and the person who set it up receives a
  notification email. Fix the underlying issue and re-enable the integration — it will resume from the next new submission.
</p>

<h2 id="faq">FAQ</h2>

  <p>No. formbase creates one in your Google Drive when the first submission arrives.</p>

  <p>formbase creates a new one on the next submission with the same column headers. No data is lost.</p>

  <p>
    Yes. Google accounts are connected at the workspace level. Any member can use any connected account when setting up an integration — no
    re-authorization needed.
  </p>

  <p>Yes. Authorize additional accounts during new integration setups. Each form picks which account to use.</p>

  <p>
    Permission to create and edit spreadsheets (Google Sheets API), create files in Google Drive (files only), and read your email and
    profile name. formbase cannot read your other Drive files or access anything outside the spreadsheets it creates.
  </p>

  <p>
    Both appear as rows in the same spreadsheet. The "Submission status" metadata column shows "Completed" or "Partial", so you can filter
    or sort by it.
  </p>

  <p>
    Yes. Open the integration and re-map fields. The next delivery rewrites the header row to match; existing rows keep the values they were
    written with.
  </p>

  <p>No. The spreadsheet is fixed once the integration is created. Delete it and create a new one.</p>

  <p>
    Go to Form settings → Integrations → Google Sheets and delete the integration. To also revoke formbase's access to your Google account,
    visit your <a href="https://myaccount.google.com/permissions">Google account permissions</a> page.
  </p>

<h2 id="next-steps">Next steps</h2>
<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Airtable](/integrations/airtable) — Push submissions into an Airtable base
  - [Notion](/integrations/notion) — Push submissions into a Notion database
  - [Webhooks](/integrations/webhooks) — POST signed JSON to any URL
  - [Repeating groups](/building-forms/repeating-groups) — Let respondents add multiple entries
  - [Exporting submissions](/submissions-analytics/exports) — Download CSV, Excel, or PDF
</div>


# Notion

Push submissions to a Notion database with field-level mapping.

## Notion

Send every submission to a Notion database as a new page. Map fields to properties, compose rich page bodies, and backfill past responses.

<h2 id="what-you-get">What you get</h2>

<ul>
  <li>One Notion page per submission</li>
  <li>Field-level mapping: form field → database property</li>
  <li>Optional rich page body composed from answers</li>
  <li>Backfill existing submissions on demand</li>
  <li>Auto-recovery when database properties are renamed</li>
</ul>

<h2 id="connect">Connect Notion</h2>

<h2 id="test-event">Test event</h2>

<p>
  The Finalize step and the integration detail panel both have a <strong>Send test</strong> button that creates a real page in your
  database. Text and title properties get <code>[Question Title]</code> placeholders, numbers get
  <code>0</code>, emails get <code>test@example.com</code>, dates get the current timestamp, selects use the first available option, and
  files get a tiny placeholder image. Delete the test page after verifying.
</p>

<h2 id="backfill">Backfill existing submissions</h2>

<p>
  After connecting, you can backfill past responses into the database. Pages are created one at a time (~3 per second to stay within
  Notion's rate limits), with progress logged every 10 pages.
</p>

<h2 id="property-types">Property types</h2>

<h3 id="metadata-properties">Metadata properties</h3>

<p>
  Channel reads "Public link" or "Request", and Request ID holds the id of the request a response answered, so a database that collects both
  share-link and request responses can tell them apart and join back to the request.
</p>

<h2 id="repeating-groups">Repeating groups</h2>

<p>
  If your form has a <a href="/building-forms/repeating-groups">repeating group</a>, the field picker gives you two ways to map it,
  depending on whether you want the whole group in one property or each member field broken out.
</p>

<ul>
  <li>
    <strong>The whole group → one property</strong> — pick the group itself to map every entry into a single Rich text (or Title) property.
    Each entry shows as a numbered, labeled row, like "1. Full name: Jane Appleseed, Age: 34, 2. Full name: Marcus Lee, Age: 29".
  </li>
  <li>
    <strong>Each member field → its own property</strong> — pick a member field to give it a dedicated property. The values from every entry
    join with " · " (for example "Jane Appleseed · Marcus Lee").
  </li>
</ul>

<p>
  Member fields that no longer exist in the form but appear in past submissions stay mappable, so syncing never breaks for older responses.
</p>

<h2 id="rich-page-body">Rich page body</h2>

<p>
  Beyond property mapping, you can compose a rich page body with the template editor. Type <strong>@</strong> to insert any form field value
  or metadata such as PDF Link — the editor shows them as mention chips that resolve at sync time.
</p>

<p>
  Page bodies support paragraphs, <strong>bold</strong>, <em>italic</em>, <u>underline</u>, and links. Headings, lists, and images are not
  supported — they render as plain paragraphs. Text is automatically chunked at 2,000 characters per rich-text block (Notion's limit).
</p>

<h2 id="abandoned-responses">Abandoned response events</h2>

<p>
  If a respondent starts your form but doesn't finish, formbase can still sync the partial data. On the{' '}
  <strong>Abandoned submissions</strong> step, set <strong>Send abandoned event after</strong> to 12 hours, 1 day, 3 days, or 1 week. An
  hourly sweep then creates a page with whatever the respondent filled in, using the same property mapping as a completed submission. Map
  the "Submission status" metadata field to a Status or Select property to tell them apart in Notion.
</p>

<p>
  Only drafts from a share link are swept, and you can narrow it further with <strong>Required fields</strong> — the page is created only
  when at least one of the fields you pick was filled in.
</p>

> ℹ️ **Pro feature**
> <p>
>     Abandoned response delivery requires a Pro or Business plan. The core Notion integration is available on all plans. See{' '}
>     <a href="/subscription-billing/plans-pricing">plans & pricing</a>.
>   </p>

<h2 id="schema-drift">Schema changes and auto-recovery</h2>

<p>
  If you rename a property in Notion, formbase detects the change on the next submission and updates its mapping automatically. Properties
  are matched by their stable internal ID, so renames are transparent.
</p>

<p>
  If you delete a property, formbase drops it from the mapping and keeps syncing the remaining fields. If a property's type changes to
  something incompatible (e.g., number becomes select), the value may fail to sync — re-open the integration and re-map that field.
</p>

<h2 id="troubleshooting">Troubleshooting</h2>

<ul>
  <li>
    <strong>Database not visible</strong> — Re-authorize and share the database with formbase during the Notion consent screen.
  </li>
  <li>
    <strong>Property mismatch</strong> — Mismatched types prevent syncing. Re-open the integration to see which fields need attention.
  </li>
  <li>
    <strong>Rate limits</strong> — Notion's API has per-workspace limits. formbase retries automatically; backfills throttle to ~3 pages per
    second.
  </li>
  <li>
    <strong>Access revoked</strong> — If you remove formbase from Notion Settings → Connections, the integration stops with an error and the
    person who set it up receives an email. Reconnect to restore.
  </li>
</ul>

<h2 id="faq">FAQ</h2>

  <p>No. Create the database in Notion first, then share it with formbase during authorization.</p>

  <p>
    The next submission fails with a "not found" error and the integration stops. The database is fixed when the integration is created, so
    delete this integration and create a new one against the new database.
  </p>

  <p>Yes. Notion accounts are connected at the workspace level. Any member can use any connected account when setting up an integration.</p>

  <p>Yes. Authorize additional Notion workspaces during new integration setups. Each form picks which account to use.</p>

  <p>No. But if you remove formbase from Notion Settings → Connections, the token is revoked and you'll need to reconnect.</p>

  <p>
    Both appear as pages in the same database. The "Submission status" property reads "Completed", "Partial", or "Updated" when a respondent
    edited an earlier answer — use Notion's filtered views to separate them.
  </p>

  <p>
    Go to Form settings → Integrations → Notion and delete the integration. To also revoke formbase's access to your Notion workspace, go to
    Notion Settings → Connections and remove formbase.
  </p>

<h2 id="next-steps">Next steps</h2>
<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Slack](/integrations/slack) — Post submissions to a Slack channel
  - [Airtable](/integrations/airtable) — Push submissions into an Airtable base
  - [Repeating groups](/building-forms/repeating-groups) — Let respondents add as many entries as they need
</div>


# Slack

Post new submissions to a Slack channel with custom message templates.

## Slack

Post new submissions to a Slack channel with custom message templates, inline field values, and user or group mentions.

<h2 id="what-you-get">What you get</h2>

<ul>
  <li>Per-submission message in the channel of your choice</li>
  <li>
    Custom template — type <strong>@</strong> to insert any field value
  </li>
  <li>
    Mention users, user groups, and broadcasts (<code>@here</code>, <code>@channel</code>, <code>@everyone</code>)
  </li>
  <li>Separate template for abandoned responses</li>
  <li>Multiple integrations per form (different channels, different templates)</li>
</ul>

<h2 id="connect">Connect Slack</h2>

<h2 id="message-template">Message template</h2>

<p>
  The template editor lets you compose the exact message posted to Slack. Type <strong>@</strong> to open the mention menu and pick from:
</p>

<ul>
  <li>
    <strong>Form fields</strong> — resolved to the submitted value at delivery time
  </li>
  <li>
    <strong>Submission metadata</strong> — including PDF Link for the generated submission PDF
  </li>
  <li>
    <strong>Slack users</strong> — live search by name; sent as a real Slack mention that pings the user
  </li>
  <li>
    <strong>User groups</strong> — mention a group handle to notify the right team
  </li>
  <li>
    <strong>Broadcasts</strong> — <code>@here</code>, <code>@channel</code>, and <code>@everyone</code>
  </li>
</ul>

<p>
  <strong>Bold</strong>, <em>italic</em>, strikethrough, inline code, and links carry through as Slack mrkdwn. Underline does not — mrkdwn
  has no underline. The message limit is 3,000 characters; a longer message is truncated with an ellipsis.
</p>

<h2 id="abandoned-responses">Abandoned response events</h2>

<p>
  When a respondent starts your form but doesn't finish, formbase can post a separate message. Set{' '}
  <strong>Send abandoned event after</strong> to 12 hours, 1 day, 3 days, or 1 week, and compose a dedicated{' '}
  <strong>Abandoned message</strong> — it cannot be left empty. Optionally narrow it with <strong>Required fields</strong>: the message
  fires only when at least one of the fields you pick was filled in.
</p>

<p>
  A sweep runs every hour, so a message arrives at the first sweep after the idle window passes, not on the minute. Only drafts from a share
  link are swept — a <a href="/requests/overview">request</a> recipient who goes quiet is chased by the request's own reminders instead.
</p>

> ℹ️ **Pro feature**
> <p>
>     Abandoned response delivery requires a Pro or Business plan. The core Slack integration is available on all plans. See{' '}
>     <a href="/subscription-billing/plans-pricing">plans & pricing</a>.
>   </p>

<h2 id="error-handling">Error handling</h2>

<ul>
  <li>
    <strong>Token revoked</strong> — If the token is revoked or the app is reinstalled, the integration shows an error. Reconnect to
    restore.
  </li>
  <li>
    <strong>Channel archived or deleted</strong> — Shows an error. Delete the integration and create a new one targeting a different
    channel.
  </li>
  <li>
    <strong>Rate limited</strong> — formbase retries automatically with the delay Slack requests.
  </li>
  <li>
    <strong>5 consecutive failures</strong> — The integration auto-pauses and the person who set it up receives an email. Fix the issue,
    then press Resume. See <a href="/integrations/health-monitoring">Integration health</a>.
  </li>
</ul>

<h2 id="faq">FAQ</h2>

  <p>
    Yes, but you need to invite the formbase bot to the channel first. In Slack, open the private channel and run
    <code>/invite @formbase</code>. Then refresh the channel list in formbase — the private channel will appear.
  </p>

  <p>
    Yes. Each integration has its own channel and message template — for example, one channel for completed submissions and another for
    abandoned responses.
  </p>

  <p>Yes. Each OAuth install creates a separate connection. Start a new integration and authorize with a different workspace.</p>

  <p>
    Yes. Slack connections are workspace-scoped, not per-user. Any workspace member can use any connected Slack account when setting up an
    integration.
  </p>

  <p>
    No. Slack is a notification-style integration — only new submissions are posted after the integration is active. For historical data,
    use <a href="/integrations/google-sheets">Google Sheets</a> or
    <a href="/integrations/notion">Notion</a>.
  </p>

  <p>
    Post messages, post to public channels without membership, read channel lists, read user and group lists (for mentions), and read team
    info. It does not request admin permissions, message history, or file access.
  </p>

  <p>
    Go to Form settings → Integrations → Slack and delete the integration. To also remove the formbase bot from your Slack workspace, go to
    Slack's app management page and uninstall it.
  </p>

<h2 id="next-steps">Next steps</h2>
<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Discord](/integrations/discord) — Post submissions to a Discord channel
  - [Webhooks](/integrations/webhooks) — POST signed JSON to any URL
</div>


# Discord

Post each form submission to a Discord channel with mentions.

## Discord

Post each form submission to a Discord channel — mention users and roles, inline field values, and notify your team in real time.

<h2 id="overview">Overview</h2>

<p>
  The Discord integration posts submissions to a channel as soon as they arrive. Compose the message with inline mentions of form fields,
  Discord users, and roles.
</p>

<p>
  formbase connects via OAuth as a bot — no webhook URLs to copy, no IDs to paste. Once the bot is in your server, you can pick any text or
  announcement channel it has access to.
</p>

> ℹ️ **Notifications, not archive**
> <p>
>     Only new submissions are posted after the integration is active. For historical data, use{' '}
>     <a href="/integrations/google-sheets">Google Sheets</a> or <a href="/integrations/notion">Notion</a>.
>   </p>

<h2 id="set-up-discord">Set up Discord</h2>

<h2 id="features">Features</h2>

<ul>
  <li>
    <strong>Channel picker</strong> — Text and announcement channels, grouped by Discord category.
  </li>
  <li>
    <strong>User and role mentions</strong> — Mention users or roles so the right people get pinged.
    <code>@everyone</code> and <code>@here</code> are not supported (see FAQ).
  </li>
  <li>
    <strong>Formatting</strong> — Bold, italic, strikethrough, inline code, and links carry through as Discord markdown. Underline does not;
    Discord has no underline.
  </li>
  <li>
    <strong>Test message</strong> — Send a sample message with placeholder data to verify your setup.
  </li>
</ul>

<h2 id="limits">Limits</h2>

<ul>
  <li>
    <strong>Message content:</strong> 2,000 characters (truncated with ellipsis if exceeded)
  </li>
  <li>
    <strong>Mentions:</strong> users and roles only — <code>@everyone</code> and <code>@here</code> are not supported
  </li>
  <li>
    <strong>Channels:</strong> text and announcement channels only — formbase excludes voice, stage, forum, and thread channels
  </li>
</ul>

<h2 id="bot-permissions">Bot permissions</h2>

<p>formbase requests only the permissions needed to deliver messages.</p>

<p>
  <strong>The bot needs:</strong>
</p>
<ul>
  <li>View Channels</li>
  <li>Send Messages</li>
  <li>Send Messages in Threads</li>
  <li>Create Public Threads</li>
  <li>Embed Links</li>
</ul>

<p>
  <strong>The bot does not request:</strong>
</p>
<ul>
  <li>Mention Everyone</li>
  <li>Read Message History</li>
  <li>Administrator</li>
  <li>Manage Messages or Channels</li>
</ul>

> 💡 **Restrict the bot to one channel**
> <p>Restrict the bot to a single channel by adjusting Discord's channel permission overrides for the formbase bot role.</p>

<h2 id="abandoned-responses">Abandoned response events</h2>

<p>
  When a respondent starts your form but doesn't finish, formbase can post a separate message. Set{' '}
  <strong>Send abandoned event after</strong> to 12 hours, 1 day, 3 days, or 1 week and compose a dedicated{' '}
  <strong>Abandoned message</strong> — it cannot be left empty. <strong>Required fields</strong> narrows it further: the message fires only
  when at least one of the fields you pick was filled in.
</p>

<p>
  A sweep runs every hour, so the message arrives at the first sweep after the idle window passes. Only drafts from a share link are swept —
  a <a href="/requests/overview">request</a> recipient who goes quiet is chased by the request's own reminders instead.
</p>

> ℹ️ **Pro feature**
> <p>
>     Abandoned response delivery requires a Pro or Business plan. The core Discord integration is available on all plans. See{' '}
>     <a href="/subscription-billing/plans-pricing">plans & pricing</a>.
>   </p>

<h2 id="faq">FAQ</h2>

  <p>
    Click <strong>Invite bot</strong> during setup. Pick the target server, confirm permissions, then return to formbase and refresh the
    server list.
  </p>

  <p>Common causes:</p>
  <ul>
    <li>The bot was kicked from the server</li>
    <li>The target channel was deleted</li>
    <li>The bot's permissions were removed for that channel</li>
    <li>The OAuth connection was revoked</li>
  </ul>
  <p>
    When the integration enters an error state, the person who set it up receives a notification email. Reconnect or delete and recreate the
    integration. The event log in the detail sheet shows the specific error.
  </p>

  <p>
    No. formbase does not request the "Mention Everyone" permission, and implicit mentions are disabled at the API level. Use a role mention
    for broad notifications instead.
  </p>

  <p>The server and channel are fixed after creation. Delete the integration and create a new one pointing to the new channel.</p>

  <p>
    Yes. Each integration targets one server and one channel. To post to multiple servers, create multiple Discord integrations on the same
    form.
  </p>

  <p>
    Yes. Discord connections are workspace-scoped. Any workspace member can use any connected Discord account when setting up an
    integration.
  </p>

  <p>
    No. Only new submissions are posted after the integration is active. For historical data, use{' '}
    <a href="/integrations/google-sheets">Google Sheets</a> or <a href="/integrations/notion">Notion</a>.
  </p>

  <p>
    Go to Form settings → Integrations → Discord and delete the integration. To also remove the bot from your Discord server, go to Server
    Settings → Integrations in Discord and remove formbase.
  </p>

  <p>
    Yes, after about 7 days. formbase refreshes them right before each delivery rather than on the daily schedule other providers use. If
    the refresh fails — the connection was revoked, or the bot was removed — the connection is marked expired and every integration using it
    stops with an error. Press Reconnect to restore them all at once.
  </p>

<h2 id="next-steps">Next steps</h2>
<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Webhooks](/integrations/webhooks) — POST signed JSON to any URL
  - [Airtable](/integrations/airtable) — Push submissions into an Airtable base
</div>


# Custom webhooks

POST submission data to any HTTPS endpoint — your backend, serverless function, or proxy.

## Custom webhooks

Send each completed submission as a signed POST request to any HTTPS endpoint — your backend, automation platform, or serverless function.

<h2 id="how-it-works">How it works</h2>

<p>
  On every submission, formbase POSTs a JSON body to your webhook URL. The request is signed with HMAC-SHA256, retried on failure, and
  logged in the integration's event log. This page covers setup. The exact payload, headers, and signature algorithm live in the{' '}
  <a href="/developers/webhooks-reference">webhook reference</a>.
</p>

<h2 id="add">Add a webhook</h2>

> ℹ️ **Multiple webhooks**
> <p>You can attach multiple webhooks to a single form. Each fires independently for every event.</p>

<h2 id="url-rules">Which URLs formbase accepts</h2>

<ul>
  <li>
    <code>https://</code> everywhere, or <code>http://</code> for <code>localhost</code> and <code>*.localhost</code> during development.
  </li>
  <li>No credentials in the URL, and at most 2,048 characters.</li>
  <li>
    No private or internal addresses. The hostname is resolved and re-checked immediately before <em>every</em> delivery, so a DNS record
    repointed at an internal address after setup is still refused.
  </li>
</ul>

<p>A refused URL is a configuration problem, not a transient one: the delivery fails permanently instead of retrying.</p>

<h2 id="payload">What you receive</h2>

<p>
  Every request is the same event envelope — <code>id</code>, <code>type</code>, <code>createdAt</code>, <code>apiVersion</code>,{' '}
  <code>test</code> and <code>data</code>. Inside <code>data</code> sit the form, the submission (id, respondent email, submitted time, PDF
  link, language), an <code>answers</code> object keyed by <a href="/requests/field-keys">field key</a>, and a <code>display</code> object
  with the same keys as readable text. Each answer appears once, in each map.
</p>

<p>
  See the reference for the full <a href="/developers/webhooks-reference#payload">payload shape</a>,{' '}
  <a href="/developers/webhooks-reference#fields-vs-answers">answers and display</a>, and how a{' '}
  <a href="/building-forms/repeating-groups">repeating group</a> is represented.
</p>

<p>
  To preview the exact body for your form, open the integration and expand <strong>Example payload</strong> under the signing secret. It
  renders your current mapping with sample answers.
</p>

<h2 id="signatures">Verifying signatures</h2>

<p>
  Each request includes an <code>X-formbase-Signature</code> header: <code>t=TIMESTAMP,sha256=HEX</code>, an HMAC-SHA256 of{' '}
  <code>TIMESTAMP.BODY</code> computed with your signing secret. The secret itself is never sent. The reference has a{' '}
  <a href="/developers/webhooks-reference#signing">copy-pasteable verification snippet</a>.
</p>

> ⚠️ **Always verify in production**
> <p>
>     Without verification, anyone who discovers your URL can post fake submissions. Reject requests where the signature is missing or
>     invalid.
>   </p>

<h2 id="abandoned-responses">Abandoned response events</h2>

<p>
  Custom webhooks fire only for completed submissions and edits — never for abandoned drafts. A custom webhook is the one receiver that gets
  both: a Zapier, Make or n8n subscription picks first submissions or edits, never both. For abandoned drafts, use a provider that has an{' '}
  <strong>Abandoned submissions</strong> step: Google Sheets, Airtable, Notion, Slack, Discord, Linear, or GitHub Issues. Each takes its own
  idle window and, where it applies, its own message template. That step requires Pro or Business.
</p>

<h2 id="retries">Retries and failures</h2>

<ul>
  <li>
    A delivery succeeds on any <code>2xx</code>.
  </li>
  <li>
    Up to 5 attempts: the first fires immediately, retries wait at least 1, 2, 4, and 8 minutes. formbase looks for due retries every 30
    minutes, so the last attempt comes about two hours after the first. A <code>Retry-After</code> header on a <code>429</code> or{' '}
    <code>5xx</code> is honored when it asks for a longer wait.
  </li>
  <li>
    <code>429</code>, <code>5xx</code>, timeouts, and connection failures are retried. Every other <code>4xx</code> fails immediately.
  </li>
  <li>
    After 5 consecutive failures the integration auto-pauses and the person who set it up gets an email. Fix the endpoint, then press{' '}
    <strong>Resume</strong>.
  </li>
  <li>
    <code>401</code>, <code>403</code>, and <code>404</code> stop the integration right away with an error status and the same email — there
    is no waiting for five failures.
  </li>
  <li>
    Deliveries that used up all 5 attempts collect in a banner on the integration. <strong>Retry all</strong> re-queues them and reactivates
    a paused integration.
  </li>
</ul>

<h2 id="testing">Testing</h2>

<p>
  <strong>Send a test event</strong> appears on the Finalize step and again on the saved integration. It POSTs a synthesized example
  submission — <code>"John Doe"</code> for text, <code>42</code> for numbers, <code>john@example.com</code> for email — signed and with your
  custom headers, exactly like a real delivery. From the saved integration it also writes a connection-test entry to the event log.
</p>

<p>For local development, expose your dev server with a tunnel:</p>

```
# ngrok
ngrok http 3000

# cloudflare tunnel

cloudflared tunnel --url http://localhost:3000
```

<h2 id="faq">FAQ</h2>

  <p>Yes. Each has its own URL, signing secret, and custom headers. All active webhooks fire independently for every submission.</p>

  <p>
    Yes — up to 5, added during setup or later from the integration. <code>Content-Type</code> is set automatically and any attempt to
    override it is ignored.
  </p>

  <p>
    No. The signing secret is generated once when the webhook is created and cannot be changed. If you need a new secret, delete the webhook
    and create a new one.
  </p>

  <p>
    Custom webhooks (configured in Form settings) fire only for completed submissions and updates. For abandoned-draft events, use Slack,
    Discord, or a project-management integration — each has a dedicated abandoned-event tab. If you need abandoned events via HTTP, create a{' '}
    <code>submission_abandoned</code> subscription through the{' '}
    <a href="/developers/webhooks-reference#abandoned-submissions">REST API webhook subscriptions</a>, from Zapier, Make, or any other
    caller.
  </p>

  <p>
    Only for <code>localhost</code> and <code>*.localhost</code> during development. All other URLs must use HTTPS.
  </p>

  <p>
    Delete it from Form settings → Integrations. formbase stops sending requests immediately, and the integration's event history is deleted
    with it.
  </p>

<h2 id="next-steps">Next steps</h2>
<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Webhook reference](/developers/webhooks-reference) — Payload, headers, and signature verification
  - [Airtable](/integrations/airtable) — Push submissions into an Airtable base
  - [Linear](/integrations/linear) — Turn submissions into Linear issues
  - [Repeating groups](/building-forms/repeating-groups) — Let respondents add as many entries as they need
</div>


# Airtable

Push submissions into an Airtable base with typed field mapping.

## Airtable

Send every submission to an Airtable table as a new record. Map fields to typed columns, compose a rich-text body, and backfill past responses.

<h2 id="what-you-get">What you get</h2>

<ul>
  <li>One record per submission</li>
  <li>Field-level mapping with type compatibility checks</li>
  <li>Metadata mapping, including a link to the generated submission PDF</li>
  <li>Optional rich-text body field with headings, bold, lists, and piped answers</li>
  <li>Backfill existing submissions on connect (batches of 10)</li>
  <li>Auto-recovery when columns are renamed</li>
  <li>Test record to verify the connection before real submissions arrive</li>
</ul>

<h2 id="connect">Connect Airtable</h2>

<h2 id="column-types">Column types</h2>

<p>formbase auto-creates select options when the submitted value doesn't match an existing option.</p>

<p>
  Formula, rollup, count, lookup, linked record, collaborator, created/modified time, created/modified by, auto-number, button, and
  synced-source columns are read-only and cannot be mapped.
</p>

<h2 id="repeating-groups">Repeating groups</h2>

<p>
  A <a href="/building-forms/repeating-groups">repeating group</a> lets respondents add as many entries as they need — like a list of
  guests, each with a name and age. In the field picker you choose how to map it:
</p>

<ul>
  <li>
    <strong>The whole group</strong> maps to one field. Every entry shows as numbered, labeled rows, like "1. Full name: Jane Appleseed,
    Age: 34, 2. Full name: Marcus Lee, Age: 29".
  </li>
  <li>
    <strong>Each member field</strong> maps to its own field. The values from every entry join with <code> · </code>, so the Full name field
    reads "Jane Appleseed · Marcus Lee".
  </li>
</ul>

<p>
  Pick one, the other, or both. If you remove a member field from your form later, it still appears in the picker so past records keep their
  field — your mapping never breaks.
</p>

<h2 id="rich-text-body">Rich-text body</h2>

<p>
  If your table has a Rich text, Long text, or Single line text column, you can compose its content with the template editor. Type{' '}
  <strong>@</strong> to insert any form field value or metadata such as PDF Link — the editor shows them as mention chips that resolve at
  sync time. Rich text columns preserve formatting; other column types receive plain Markdown.
</p>

<p>
  Supported formatting: headings (H1-H4), <strong>bold</strong>, <em>italic</em>, ~~strikethrough~~, <code>inline code</code>, links, and
  ordered/unordered lists. Images and embeds are not supported.
</p>

<h2 id="backfill">Backfill existing submissions</h2>

<p>
  After connecting, you can backfill past responses. Records are written in batches of 10 with a short delay between batches to stay within
  Airtable's rate limits. Progress is shown in the integration panel.
</p>

<h2 id="test-event">Test event</h2>

<p>
  The Finalize step and the integration detail panel both have a <strong>Send test</strong> button that writes a placeholder record. Text
  columns get <code>[Question Title]</code> strings, numbers get <code>0</code>, emails get <code>test@example.com</code>, dates get the
  current timestamp, and selects use the first option from the question's choices (to avoid creating stray options). Delete the test record
  after verifying.
</p>

<h2 id="abandoned-responses">Abandoned response events</h2>

<p>
  If a respondent starts your form but doesn't finish, formbase can still sync the partial data. On the{' '}
  <strong>Abandoned submissions</strong> step, set <strong>Send abandoned event after</strong> to 12 hours, 1 day, 3 days, or 1 week. An
  hourly sweep then writes a record using the same field mapping as a completed submission. Map the "Submission status" metadata field to a
  single-select or text column to tell "Partial" records from "Completed" and "Updated" ones.
</p>

<p>
  Only drafts from a share link are swept, and <strong>Required fields</strong> narrows it further: the record is written only when at least
  one of the fields you pick was filled in.
</p>

> ℹ️ **Pro feature**
> <p>
>     Abandoned response delivery requires a Pro or Business plan. The core Airtable integration is available on all plans. See{' '}
>     <a href="/subscription-billing/plans-pricing">plans & pricing</a>.
>   </p>

<h2 id="schema-drift">Schema changes and auto-recovery</h2>

<p>
  If you rename a column in Airtable, formbase detects the change on the next submission and updates its mapping automatically. Columns are
  matched by their stable internal ID, so renames are transparent.
</p>

<p>
  If you delete a column, formbase drops it from the mapping and keeps syncing the remaining fields. If a column's type changes to something
  incompatible, the value fails to sync — re-open the integration and re-map that field.
</p>

<h2 id="troubleshooting">Troubleshooting</h2>

<ul>
  <li>
    <strong>Authorization expired</strong> — Airtable access tokens last about an hour and are refreshed automatically, both by a daily job
    and before each delivery. If the refresh itself fails — the grant was revoked, or the refresh token expired — the connection is marked
    expired and every integration on it stops. Press Reconnect.
  </li>
  <li>
    <strong>Column not visible</strong> — Confirm the OAuth grant includes the base and the column type is writable (see table above).
  </li>
  <li>
    <strong>Rate limit</strong> — Airtable enforces 5 requests/second per base. Backfills throttle automatically; live syncs retry with
    backoff.
  </li>
</ul>

<h2 id="faq">FAQ</h2>

  <p>
    Four Airtable scopes: read/write record data, read base schemas, and read your email (to identify the account). Uses OAuth with PKCE —
    no API keys to manage.
  </p>

  <p>Yes. Airtable connections are workspace-scoped. Any member can use any connected account when setting up an integration.</p>

  <p>Yes. Authorize additional accounts during new integration setups. Each form picks which account to use.</p>

  <p>
    The next submission fails with a "not found" error and the integration stops. The base and table are fixed when the integration is
    created, so delete this integration and create a new one against the new table.
  </p>

  <p>
    No. These are read-only in Airtable's API. See the full list of excluded column types in the <a href="#column-types">column types</a>{' '}
    section.
  </p>

  <p>
    Go to Form settings → Integrations → Airtable and delete the integration. To also revoke formbase's access, go to your Airtable account
    settings and remove the formbase OAuth app.
  </p>

<h2 id="next-steps">Next steps</h2>
<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Linear](/integrations/linear) — Turn submissions into Linear issues
  - [GitHub Issues](/integrations/github) — Turn submissions into GitHub issues
  - [Repeating groups](/building-forms/repeating-groups) — Let respondents add multiple entries
</div>


# Linear

Open a Linear issue from each submission.

## Linear

Turn submissions into Linear issues automatically. Each submission creates one issue in the team you choose, with a custom title, description, and project.

<h2 id="what-you-get">What you get</h2>

<ul>
  <li>One Linear issue per submission, attributed to the connecting user</li>
  <li>
    Title and description built from a template editor — type <strong>@</strong> to insert any form field value
  </li>
  <li>Description supports markdown (bold, lists, headings, code fences, block quotes, links)</li>
  <li>Optional default project applied to every issue</li>
  <li>Backfill existing submissions on demand (~1 issue per second)</li>
  <li>
    Test button that creates a real <code>[Test]</code>-prefixed issue so you can verify before going live
  </li>
</ul>

<h2 id="connect">Connect Linear</h2>

<h2 id="backfill">Backfill existing submissions</h2>

<p>
  When you first connect, you can export existing submissions as Linear issues. The backfill runs in the background at ~1 issue per second
  to stay within Linear's rate limits. Progress is logged every 10 submissions.
</p>

<h2 id="abandoned-responses">Abandoned response events</h2>

<p>
  When a respondent starts your form but doesn't finish, formbase can create a separate issue for the partial data. Set{' '}
  <strong>Send abandoned event after</strong> to 12 hours, 1 day, 3 days, or 1 week, then fill in the abandoned issue title and description
  — neither can be empty. Abandoned issues get a <code>partial-submission</code> label.
</p>

<p>
  An hourly sweep looks for idle drafts, so the issue appears at the first sweep after the window passes. Only drafts from a share link are
  swept, and <strong>Required fields</strong> narrows it further: the issue is created only when at least one of the fields you pick was
  filled in.
</p>

> ℹ️ **Pro feature**
> <p>
>     Abandoned response delivery requires a Pro or Business plan. The core Linear integration is available on all plans. See{' '}
>     <a href="/subscription-billing/plans-pricing">plans & pricing</a>.
>   </p>

<h2 id="token-refresh">Token refresh</h2>

<p>
  Linear access tokens refresh automatically before each dispatch when close to expiry. If refresh fails (token revoked, app uninstalled),
  formbase marks the integration as error and you receive a failure notification email. Reconnect from the workspace Integrations page to
  restore.
</p>

<h2 id="removing">Removing the integration</h2>

<ul>
  <li>
    <strong>From formbase</strong> — Form settings → Integrations → Linear → Delete. New submissions stop creating issues immediately.
  </li>
  <li>
    <strong>From Linear</strong> — Revoke the OAuth grant from your user settings. The next dispatch fails and the credential is marked
    expired.
  </li>
</ul>

<h2 id="limits">Limits and behavior</h2>

<ul>
  <li>One issue per submission — no comment-on-existing-issue mode</li>
  <li>Title is plain text, truncated with an ellipsis past 255 characters; description is markdown-only</li>
  <li>
    An empty title falls back to <code>New submission to &lt;form name&gt;</code>
  </li>
  <li>File upload answers render as the file name followed by its download link, so the file is reachable from the issue</li>
  <li>
    A <a href="/building-forms/repeating-groups">repeating group</a> field inserted with @ renders every entry joined by " · "
  </li>
</ul>

<h2 id="faq">FAQ</h2>

  <p>
    Three Linear OAuth scopes: <code>read</code> (list teams, projects, and labels),
    <code>write</code> (update issue metadata), and <code>issues:create</code> (create issues). The connection uses <code>actor=user</code>,
    so issues are attributed to you, not a bot.
  </p>

  <p>
    Yes. Open the integration detail sheet and change either field. Switching teams resets the project since projects are team-specific.
  </p>

  <p>Yes. Authorize different Linear accounts to add multiple credentials. Each form integration picks which one to use.</p>

  <p>
    Yes. Linear connections are workspace-scoped. Any workspace member can use any connected Linear account when setting up an integration.
  </p>

  <p>
    The integration auto-pauses and an email goes to the person who set it up. A revoked token, a lost permission, or a deleted team does
    not wait for five failures — it stops the integration immediately. Either way, fix the cause and press Resume.
  </p>

<h2 id="next-steps">Next steps</h2>
<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [GitHub Issues](/integrations/github) — Turn submissions into GitHub issues
  - [Health and monitoring](/integrations/health-monitoring) — Track integration errors and token refresh status
</div>


# GitHub Issues

Open a GitHub issue from each submission.

## GitHub Issues

Turn submissions into GitHub issues automatically. Each submission creates one issue in the repository you choose, with a custom title, body, and milestone.

<h2 id="what-you-get">What you get</h2>

<ul>
  <li>One GitHub issue per submission, attributed to the connecting user</li>
  <li>
    Title and body built from a template editor — type <strong>@</strong> to insert any form field value
  </li>
  <li>Body supports GitHub-flavored markdown (bold, lists, headings, code fences, links)</li>
  <li>Optional default milestone applied to every issue</li>
  <li>
    Test button that creates a real <code>[Test]</code>-prefixed issue so you can verify before going live
  </li>
</ul>

<h2 id="connect">Connect GitHub</h2>

<h2 id="abandoned-responses">Abandoned response events</h2>

<p>
  When a respondent starts your form but doesn't finish, formbase can create a separate issue for the partial data. Set{' '}
  <strong>Send abandoned event after</strong> to 12 hours, 1 day, 3 days, or 1 week, then fill in the abandoned issue title and body —
  neither can be empty. Abandoned issues get a <code>partial-submission</code> label, created on first use.
</p>

<p>
  An hourly sweep looks for idle drafts, so the issue appears at the first sweep after the window passes. Only drafts from a share link are
  swept, and <strong>Required fields</strong> narrows it further: the issue is created only when at least one of the fields you pick was
  filled in.
</p>

> ℹ️ **Pro feature**
> <p>
>     Abandoned response delivery requires a Pro or Business plan. The core GitHub Issues integration is available on all plans. See{' '}
>     <a href="/subscription-billing/plans-pricing">plans & pricing</a>.
>   </p>

<h2 id="tokens">Tokens</h2>

<p>
  GitHub OAuth App tokens <strong>do not expire</strong> — there is no refresh-token rotation. If you revoke the grant from your GitHub
  account's authorized OAuth Apps page, the next dispatch fails, formbase marks the credential as expired, and you receive a failure
  notification email. Reconnect from the workspace Integrations page to restore.
</p>

<h2 id="removing">Removing the integration</h2>

<ul>
  <li>
    <strong>From formbase</strong> — Form settings → Integrations → GitHub Issues → Delete. New submissions stop creating issues
    immediately.
  </li>
  <li>
    <strong>From GitHub</strong> — Revoke the OAuth grant from your account's authorized OAuth Apps page. formbase does not automatically
    revoke the token on its side.
  </li>
</ul>

<h2 id="limits">Limits and behavior</h2>

<ul>
  <li>One issue per submission — no comment-on-existing-issue mode</li>
  <li>No backfill — only new submissions create issues</li>
  <li>Title is plain text, truncated with an ellipsis past 256 characters; body is markdown-only</li>
  <li>
    An empty title falls back to <code>New submission to &lt;form name&gt;</code>
  </li>
  <li>File upload answers render as the file name followed by its download link, so the file is reachable from the issue</li>
  <li>
    Respondent answers are inserted into the issue body as-is — if an answer contains <code>@username</code>, GitHub notifies that user, and{' '}
    <code>#123</code> creates an issue reference
  </li>
</ul>

<h2 id="faq">FAQ</h2>

  <p>
    Three GitHub scopes: <code>repo</code> (create issues), <code>read:user</code> (identify your account), and <code>read:org</code> (list
    organization repos). This is a standard OAuth App — no GitHub App installation required.
  </p>

  <p>
    Yes. GitHub connections are workspace-scoped. Any workspace member can use any connected GitHub account when setting up an integration.
  </p>

  <p>
    Yes. Open the integration detail sheet and change either field. Switching repositories resets the milestone since milestones are
    repo-specific.
  </p>

  <p>
    The next submission fails with a "not found" error and the integration shows an error status. Edit the integration to pick a different
    repository.
  </p>

  <p>
    The integration auto-pauses and an email goes to the person who set it up. A revoked token, a lost permission, or a deleted repository
    does not wait for five failures — it stops the integration immediately. Either way, fix the cause and press Resume.
  </p>

<h2 id="next-steps">Next steps</h2>
<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Developer overview](/developers/overview) — REST API, webhooks, and MCP server
  - [Plans & pricing](/subscription-billing/plans-pricing) — Compare Free, Pro, and Business features
</div>


# Analytics streaming

Forward form analytics events to Google Analytics or Axiom, with optional respondent answers for Axiom.

> ℹ️ **Available on the Free plan.**


## Analytics streaming

Connect a workspace analytics destination once, then control event and answer forwarding for each form.

<h2 id="two-streams">Two separate streams</h2>

> ⚠️ **Answers never leave formbase by default**
> <p>
>     Connecting Axiom starts answer-free analytics events only. Full submissions stay off until you explicitly enable{' '}
>     <strong>Send respondent answers</strong> on a specific form. Google Analytics never receives answers.
>   </p>

<h2 id="event-contract">Analytics event contract</h2>

<p>Three fixed event names are forwarded:</p>

<p>
  Every event carries <code>form_id</code>, <code>form_name</code>, and <code>source</code> — <code>link</code> for a public{' '}
  <a href="/sharing-publishing/sharing-embedding">share link</a>, <code>request</code> for a <a href="/requests/overview">request</a> — so a
  dashboard can tell the two apart. When available it also carries country, device, browser, traffic source, UTM source, medium and
  campaign, referrer host, and, on <code>form_submit</code>, completion duration. Alongside them goes a pseudonymous visitor ID. Events
  never contain answer values, respondent email, or question content.
</p>

<h2 id="connect">Connect a destination</h2>

<p>
  Go to <strong>Workspace → Integrations → Analytics</strong>, then connect Google Analytics or Axiom. A destination belongs to the whole
  workspace. Event forwarding starts immediately for every form in that workspace.
</p>

<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Google Analytics (GA4)](/integrations/google-analytics) — Measurement Protocol setup and reporting
  - [Axiom](/integrations/axiom) — Event queries and optional answer streaming
</div>

<h2 id="per-form">Per-form controls</h2>

<p>
  Open <strong>Form settings → Integrations → Analytics streaming</strong>. Each destination provides:
</p>

<ul>
  <li>
    <strong>Forward analytics events</strong> — disable this destination for only this form.
  </li>
  <li>
    <strong>Send respondent answers</strong> — Axiom only; opt in to full submitted payloads.
  </li>
  <li>
    <strong>Send test event</strong> — verify credentials without waiting for live traffic.
  </li>
</ul>

<h2 id="reliability">Reliability</h2>

<p>
  Event forwarding is best-effort. Each destination request is given 3 seconds, failures are swallowed so they never block a respondent, and
  a failed event is not retried. Analytics are statistical; avoiding one queue row per form view keeps the cost bounded.
</p>

<p>
  Axiom answer streaming rides the submission delivery queue instead: 5 attempts with exponential backoff. After 20 consecutive answer
  failures, that destination stops being queued until a delivery succeeds or you save a new API token.
</p>

<h2 id="privacy">Privacy and consent</h2>

<p>
  Analytics events are pseudonymous, not anonymous. They carry a random visitor ID and coarse traffic/device metadata but no answers. You
  remain responsible for telling respondents about your third-party analytics, collecting consent where required, and configuring retention
  in GA4 or Axiom.
</p>

<p>
  Axiom answer streaming can contain personal or sensitive data. Enable it only for datasets you control and only when your privacy notice,
  lawful basis, and processing agreements cover that transfer.
</p>


# Google Analytics (GA4)

Forward form views, engagement, and submissions to your Google Analytics 4 property.

> ℹ️ **Available on the Free plan.**


## Google Analytics (GA4)

Send form_view, form_engaged, and form_submit events to your own GA4 property through Measurement Protocol.

<h2 id="before-you-start">What you need</h2>

<ul>
  <li>A GA4 property with a Web data stream</li>
  <li>
    Measurement ID, formatted like <code>G-XXXXXXXXXX</code>
  </li>
  <li>Measurement Protocol API secret for that stream</li>
</ul>

<h2 id="connect">Connect GA4</h2>

<h2 id="events">Events and parameters</h2>

<p>
  Every event carries <code>form_id</code>, <code>form_name</code>, and <code>source</code> (<code>link</code> or <code>request</code>).
  Depending on what is known about the visit, an event can also include:
</p>

<ul>
  <li>
    <code>country</code>, <code>device</code>, and <code>browser</code>
  </li>
  <li>
    <code>traffic_source</code>, <code>traffic_source_name</code>, <code>traffic_medium</code>, and <code>traffic_campaign</code>
  </li>
  <li>
    <code>referrer_host</code>
  </li>
  <li>
    <code>duration_seconds</code> on submit, when available
  </li>
</ul>

<p>
  formbase maps its pseudonymous visitor ID to GA4 <code>client_id</code>. When no ID is available, it sends <code>unknown</code>. Events
  never contain answers or respondent email.
</p>

<h2 id="reporting">Use events in GA4</h2>

<ul>
  <li>
    Check <strong>Reports → Realtime</strong> after sending a test or opening a live form.
  </li>
  <li>
    Use <strong>Reports → Engagement → Events</strong> after GA4 finishes processing events.
  </li>
  <li>Create an Exploration filtered to the three formbase event names.</li>
  <li>
    Register parameters such as <code>form_id</code>, <code>device</code>, or <code>traffic_source</code> as custom dimensions.
  </li>
  <li>
    Mark <code>form_submit</code> as a key event if submission completion is a conversion for your business.
  </li>
</ul>

> ⚠️ **Do not send answers to GA4**
> <p>
>     Google Measurement Protocol policies prohibit personally identifiable information. formbase never offers answer streaming for GA4. Use
>     Axiom when you need an opt-in full-submission destination.
>   </p>

<h2 id="troubleshooting">Troubleshooting</h2>

<h2 id="next-steps">Next steps</h2>

<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Analytics streaming](/integrations/analytics-streaming) — Defaults, privacy, and per-form controls
  - [Built-in analytics](/submissions-analytics/analytics-insights) — Use formbase analytics without external setup
</div>


# Axiom

Stream form analytics events and optional full respondent answers into an Axiom dataset.

> ℹ️ **Available on the Free plan.**


## Axiom

Query raw form events in APL, build dashboards and monitors, and optionally send full submitted answers.

<h2 id="connect">Connect Axiom</h2>

<p>
  The two region choices are <strong>US East 1 (AWS)</strong> and <strong>EU Central 1 (AWS)</strong>. Pick the one your Axiom organization
  lives in — formbase derives the ingest endpoint from it, and ingesting into the wrong region is rejected.
</p>

<h2 id="event-stream">Analytics events</h2>

<p>
  Connecting Axiom turns answer-free event forwarding on for every form in the workspace. Records use event names <code>form_view</code>,
  <code>form_engaged</code>, and <code>form_submit</code>, with the same parameters documented in{' '}
  <a href="/integrations/analytics-streaming#event-contract">Analytics streaming</a>.
</p>

<pre>
  <code>{`['your-dataset']
| where ['event'] in ('form_view', 'form_engaged', 'form_submit')
| summarize events=count() by ['event']`}</code>
</pre>

<p>
  Each record flattens the event parameters to top level, so <code>form_id</code>, <code>source</code>, <code>device</code> and the rest are
  ordinary columns you can filter and summarize on.
</p>

<p>
  Event forwarding is best-effort and not retried. Disable it for one form under{' '}
  <strong>Form settings → Integrations → Analytics streaming → Forward analytics events</strong>.
</p>

<h2 id="answers">Respondent answers</h2>

<p>
  Axiom can also receive one full record per completed submission. This stream is <strong>off by default</strong> and must be enabled
  separately for each form with <strong>Send respondent answers</strong>.
</p>

<p>
  An answer record is the same event envelope a <a href="/integrations/webhooks">custom webhook</a> receives, plus an Axiom{' '}
  <code>_time</code> field: the event id, type, <code>createdAt</code>, <code>apiVersion</code> and <code>test</code>, and under{' '}
  <code>data</code> the form, the submission (id, respondent email, submitted time, PDF link, language), an <code>answers</code> object
  keyed by <a href="/requests/field-keys">field key</a> and a <code>display</code> object with the same keys as readable text. Submissions
  that answered a request also carry a <code>request</code> block with your external id and metadata. The full shape is in the{' '}
  <a href="/developers/webhooks-reference#payload">webhook reference</a>.
</p>

<p>
  Answers are streamed when a submission is completed and again when a respondent edits it. Abandoned drafts never reach Axiom this way —
  answer streaming has no idle window.
</p>

> ⚠️ **Answer records contain personal data**
> <p>
>     A full submission can contain names, emails, free text, files, signatures, and other sensitive answers. Use a restricted dataset, keep
>     token scope narrow, and set retention/access rules before enabling this stream.
>   </p>

<h2 id="delivery">Answer delivery and failures</h2>

<p>
  Answer records ride the submission delivery queue: up to 5 attempts with exponential backoff. After 20 consecutive failures the
  destination stops being queued, so a bad token or a rejected dataset cannot generate a failing delivery for every new submission. Saving a
  new API token clears the error and resumes delivery.
</p>

<p>
  Event forwarding and answer streaming have independent switches. You can send answers without events, events without answers, both, or
  neither for each form.
</p>

<h2 id="troubleshooting">Troubleshooting</h2>

<h2 id="next-steps">Next steps</h2>

<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Analytics streaming](/integrations/analytics-streaming) — Shared schema, defaults, and privacy
  - [Integration health](/integrations/health-monitoring) — Understand retries and auto-pause
</div>


# Health and monitoring

How formbase monitors integrations, refreshes tokens, and surfaces errors.

## Health and monitoring

formbase keeps your integrations running behind the scenes — refreshing tokens before they expire, pausing when something breaks, and showing you exactly what happened so you can fix it fast.

<h2 id="proactive-token-refresh">Proactive token refresh</h2>

<p>
  A daily background job checks the OAuth connections in your workspace, including Cal.com. If a token expires within the next 24 hours,
  formbase refreshes it automatically — no action needed from you.
</p>

<p>
  This applies to Google, Slack, Linear, Airtable, and Cal.com. Notion and GitHub issue tokens that don't expire, so they're skipped.
  Discord tokens expire after about 7 days and are refreshed on demand right before each delivery rather than by the daily job.
</p>

<p>
  If a refresh fails — for example because the connection was revoked on the provider side — formbase marks the connection as expired and
  moves every integration that shares it into an error state. You'll see a warning on each affected integration and receive a notification
  email.
</p>

<h2 id="health-states">Health states</h2>

<p>
  Each integration card carries a status badge, and opening the integration shows a banner explaining what to do. The badge says{' '}
  <strong>Active</strong>, <strong>Paused</strong>, or <strong>Delivery error</strong>; the banner is more specific.
</p>

<p>
  Connection problems always take priority. If a token expired, reconnecting fixes every integration that shares that account at once — no
  need to address each one separately.
</p>

<h2 id="error-indicators">Error indicators</h2>

<p>When something goes wrong, formbase shows it in two places:</p>

<ul>
  <li>
    <strong>Integration card</strong> — the card shows a status badge and a colored border: orange for a connection problem, red for a
    delivery error. When the connection expired, a <strong>Reconnect</strong> button appears on the card itself.
  </li>
  <li>
    <strong>Detail panel</strong> — open the integration to see a banner naming what happened and how to fix it.
  </li>
</ul>

<h2 id="auto-pause">Auto-pause after repeated failures</h2>

<p>
  If an integration fails 5 times in a row, formbase pauses it so a broken destination cannot keep burning deliveries. An authentication,
  permission, or not-found error skips the counter and stops the integration immediately, because retrying cannot fix it.
</p>

<p>
  Either way, an email goes to the person who set the integration up — or to the workspace owner when that address is unavailable. It is
  sent at most once per integration per 24 hours, so one broken destination cannot flood your inbox.
</p>

<p>Fix the underlying issue, then press Resume. Delivery picks up from the next new submission.</p>

<h2 id="event-log">Event log</h2>

<p>
  Each integration lists its last 20 events under <strong>Recent events</strong> — deliveries, connection tests, and backfill exports, each
  marked as passed or failed. A failed entry shows the destination's own error message, so a Notion validation error reads exactly as Notion
  worded it. A failed backfill also gets a <strong>Retry</strong> button that restarts the export.
</p>

<h2 id="failed-deliveries">Failed deliveries and retries</h2>

<p>
  Every delivery gets up to 5 attempts, waiting at least 1, 2, 4, and 8 minutes between them, or longer when the destination asks for more
  time through a <code>Retry-After</code> header. formbase looks for due retries every 30 minutes, so each retry can come up to half an hour
  later, and the last attempt about two hours after the first.
</p>

<p>
  Submissions that use up all 5 attempts are counted in a banner at the top of the integration: "3 submissions failed to deliver".{' '}
  <strong>Retry all</strong> re-queues every one of them and reactivates the integration if it was paused or errored. The banner is hidden
  when nothing has failed.
</p>

> ℹ️ **Analytics destinations are separate**
> <p>
>     Everything on this page covers submission integrations. Analytics event forwarding is best-effort and never retried, and Axiom answer
>     streaming stops on its own after 20 consecutive failures. See <a href="/integrations/analytics-streaming">Analytics streaming</a>.
>   </p>

<h2 id="reconnecting">Reconnecting an expired credential</h2>

<p>
  When a connection expires, click <strong>Reconnect</strong> on the integration card or in the detail panel. This re-runs the OAuth flow
  with the same provider. Once authorized, the credential is restored and all integrations using that account resume automatically.
</p>

<p>You can also switch to a different connected account from the detail panel if the original account is no longer available.</p>

<h2 id="faq">FAQ</h2>

  <p>
    Once a day. Tokens expiring within 24 hours are refreshed automatically. If you suspect an issue sooner, open the integration detail
    panel — the health state updates in real time based on delivery results.
  </p>

  <p>
    No. Submissions are always saved in formbase regardless of integration status. Failed deliveries are queued and can be retried from the
    detail panel once the issue is resolved.
  </p>

  <p>
    formbase refreshes tokens proactively, so in most cases you won't notice. If the refresh fails, you receive an email notification
    immediately — typically before the token actually expires.
  </p>

  <p>
    The integration switches to the new account. Make sure the new account has access to the same destination (spreadsheet, database,
    channel). If the destination doesn't exist in the new account, you'll need to update the integration settings.
  </p>

<h2 id="next-steps">Next steps</h2>
<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Google Sheets](/integrations/google-sheets) — Auto-sync submissions to a spreadsheet
  - [Slack](/integrations/slack) — Send submissions to a Slack channel
  - [Webhooks](/integrations/webhooks) — POST signed JSON to any URL
  - [Notion](/integrations/notion) — Push submissions into a Notion database
</div>

