---
title: "Google Sheets"
description: "Add a row to a Google Sheet for every submission, using a Google Cloud service account."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.getquickforms.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Google Sheets

The Google Sheets [destination](https://docs.getquickforms.com/destinations/) adds a row to a spreadsheet for every submission. It signs in with a Google Cloud service account, so there is no Google sign-in flow: you give quickforms the service account's JSON key and share the sheet with it.

## Set up

1. In Google Cloud, enable the Google Sheets API for a project.
2. Create a service account in that project and add a JSON key to it. Google downloads the key as a `.json` file.
3. Open the spreadsheet and share it with the service account's `client_email` (from the key file) as an editor.
4. Copy the spreadsheet ID from the sheet's URL: `docs.google.com/spreadsheets/d/<id>/edit`.
5. Add a Google Sheets destination to the form, on its **Destinations** tab in the [dashboard](https://docs.getquickforms.com/dashboard/) or with the [API](#with-the-api), and paste in the key and the ID.

## Settings

| Setting | Type | Required | Description |
| --- | --- | --- | --- |
| `service_account` | Secret | Yes | The whole JSON key file. It must include `client_email` and `private_key`. |
| `spreadsheet_id` | Text | Yes | The long ID in the sheet's URL. |
| `sheet` | Text | No | The sheet (tab) to add rows to. Defaults to `Sheet1`. |

Like every secret setting, the key is encrypted at rest, never shown again in full, and kept when you update the destination without sending it. See [config and secrets](https://docs.getquickforms.com/destinations/#config-and-secrets).

## Columns

The [mapping](https://docs.getquickforms.com/destinations/#mapping) is the row: one entry per column, in order. The keys are names for you to recognise the columns by. Only the values are written, so add a header row to the sheet yourself if you want one.

When you create the destination without a mapping, the columns are:

1. `Submitted at`: `{{_submitted_at}}`
2. One column per form field, named after the field's label (or its name), with its placeholder.
3. `Submission ID`: `{{_submission_id}}`

For a form with name, email and message fields, that's:

```json title="Default mapping"
{
  "Submitted at": "{{_submitted_at}}",
  "Name": "{{name}}",
  "Email": "{{email}}",
  "Message": "{{message}}",
  "Submission ID": "{{_submission_id}}"
}
```

Change, reorder or add columns by saving a new mapping. Any [placeholder](https://docs.getquickforms.com/destinations/#placeholders) works, and a file field's placeholder becomes its [download link](https://docs.getquickforms.com/file-uploads/#download-links).

## What a row looks like

A submission to the form above adds a row like this:

| Submitted at | Name | Email | Message | Submission ID |
| --- | --- | --- | --- | --- |
| `2026-11-06T10:24:00+00:00` | Ada Lovelace | ada@example.com | Interested in a demo | `01jca9qf3tz5b8m2k7w4r6x0hn` |

The values are illustrative.

Values are sent the way a person would type them (`USER_ENTERED`), so Google Sheets reads numbers and dates as usual. A value starting with `=`, `+`, `-` or `@` gets a `'` in front, so it is stored as text and not run as a formula.

## How it sends

Each submission is one request: a `POST` to the Sheets API `values.append` endpoint for the sheet's `A1` range, inserting a new row.

- At send time, quickforms signs a token request with the service account's private key and swaps it for an access token with the spreadsheets scope. The token is cached for 50 minutes.
- [Previews](https://docs.getquickforms.com/destinations/#preview) and [sandbox](https://docs.getquickforms.com/sandbox/) submissions never ask Google for a token. They show the request with the `Authorization` header as `[redacted]`.
- Deliveries are retried and logged like every other [destination](https://docs.getquickforms.com/destinations/#retries).

## With the API

```bash
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/destinations \
  -H "Authorization: Bearer $QUICKFORMS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "type": "google_sheets",
    "name": "Leads sheet",
    "config": {
      "service_account": "{\"client_email\": \"quickforms@your-project.iam.gserviceaccount.com\", \"private_key\": \"-----BEGIN PRIVATE KEY-----\\n...\", ...}",
      "spreadsheet_id": "SPREADSHEET_ID",
      "sheet": "Leads"
    }
  }'
```

`service_account` is the key file's contents as a string. The values are illustrative.

## Troubleshooting

| Error in the delivery log | What to check |
| --- | --- |
| "The service account key must be the JSON file from Google Cloud, with client_email and private_key." | Paste the whole JSON key file into `service_account`. |
| "Could not sign with the service account private key." | The `private_key` in the key is not a valid private key. |
| "The service account private_key must be the PEM key from the JSON file." | The `private_key` is not PEM content. |
| "Google did not return an access token: ..." | Google refused the service account. Check the key is current and the account still exists. |
| "Google Sheets responded with HTTP 403" or `404` | Check the sheet is shared with the `client_email`, the Sheets API is enabled, and the spreadsheet ID and sheet name are right. These are not retried. |

Source: https://docs.getquickforms.com/google-sheets/index.mdx
