# Introduction

SpreadAPI is a free Google Apps Script that allows you to add REST API to any spreadsheet in Google Sheets in a few minutes. The API can be accessed over HTTPS from back-end as well as front-end apps.

![SpreadAPI architecture](/files/-Lv_Nnu-JhstuNP4FhKI)

SpreadAPI is a free alternative to tools like [sheetsu.com](https://sheetsu.com/), [sheetdb.io](https://sheetdb.io/) or [sheety.co](https://sheety.co/). With SpreadAPI you get just the same but for free. Moreover, you don't share the data with any third party because the code runs on your account as a Google Apps Script.

{% hint style="info" %}
SpreadAPI is maintained by the author of [Roombelt](https://roombelt.com) - a simple and reliable meeting room display system.\
\
If you want to support SpreadAPI consider trying [Roombelt](https://roombelt.com) in your company!
{% endhint %}

{% content-ref url="/pages/-LuOeXjz586TN9Anksv-" %}
[Setup](/setup)
{% endcontent-ref %}

{% content-ref url="/pages/-LuOlwEmHtUejmPyH64S" %}
[Usage](/usage)
{% endcontent-ref %}

{% content-ref url="/pages/-LuOebbvPCOzRESxMRIG" %}
[API Reference](/usage/usage)
{% endcontent-ref %}

{% content-ref url="/pages/-Lv0Oxz7Zf\_yNW5s9Fcu" %}
[Examples](/examples)
{% endcontent-ref %}


# Setup

After following this page you will have a free, fully-featured and secure spreadsheet API at your disposal.

### Prerequisites

The first row in each spreadsheet is treated as column names. E.g. let's take the following spreadsheet:

![](/files/-Lv0MzjmSDnPqh7D--d6)

The API for getting all rows would return the following result:

![](/files/-Lv0NydXCUhudSxbRafC)

### Installation

In your spreadsheet open *Extensions* -> *Apps Script*.

<figure><img src="/files/no4tvl98vLoCCMdLx8PI" alt=""><figcaption></figcaption></figure>

\
Copy the below script to the Google Script Editor and save it.

```javascript
/*
 * SpreadAPI 1.0, created by Mateusz Zieliński
 * Home page: https://spreadapi.com
 * Sponsored by: https://roombelt.com
 * License: Apache 2.0
 */


/* 
 * Configure authentication below 
 * Learn more at docs.spreadapi.com
 */

// Admin account that has read/write access to all sheets
User("admin", "PUT_STRONG_PASSWORD_HERE", ALL);

// User account that can add entries to the "transactions" sheet
// User("user", "Passw0rd!", { transactions: POST });

// User account that can add entries to the "transactions" sheet and read from "summary"
// User("user", "Passw0rd!", { transactions: POST, summary: GET });

// Anonymous account that has write access to a specified sheet
// User("anonymous", UNSAFE(""), { transactions: POST });

// Anonymous account that has read/write access to all sheets (NOT RECOMMENDED!)
// User("anonymous", UNSAFE(""), ALL);

// Anonymous account that has read access to all sheets (NOT RECOMMENDED!)
// User("anonymous", UNSAFE(""), GET);

/*
 * Copyright 2019 Mateusz Zieliński
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/**
 * @OnlyCurrentDoc
 */
 
function doPost(request){try{var requestData=JSON.parse(request.postData.contents)}catch(e){return httpResponse(error(400,"invalid_post_payload",{payload:request.postData.contents,type:request.postData.type}))}return Array.isArray(requestData)?httpResponse(requestData.map(handleRequest)):httpResponse(handleRequest(requestData))}function handleRequest(params){const ss=SpreadsheetApp.getActiveSpreadsheet(),sheetName=(params.sheet||"").toLowerCase(),_id=null==params.id?null:+params.id,method=(params.method||"GET").toUpperCase(),key=params.key||"";if(!hasAccess(key,sheetName,method))return error(401,"unauthorized",{});if(!isStrongKey(key))return error(401,"weak_key",{message:"Authentication key should be at least 8 characters long and contain at least one lower case, upper case, number and special character. Update your password or mark it as UNSAFE. Refer to the documentation for details."});const sheet=ss.getSheetByName(sheetName);if(!sheet)return error(404,"sheet_not_found",{sheet:sheetName});if(null!=_id&&_id<=1)return error(400,"row_index_invalid",{_id:_id});const payload=params.payload;switch(method){case"GET":return null!=_id?handleGetSingleRow(sheet,_id):handleGetMultipleRows(sheet,params);case"POST":return handlePost(sheet,payload);case"PUT":return handlePut(sheet,_id,payload);case"DELETE":return handleDelete(sheet,_id);default:return error(404,"unknown_method",{method:method})}}function handleGetSingleRow(sheet,_id){const lastColumn=sheet.getLastColumn(),headers=getHeaders(sheet),result=mapRowToObject(sheet.getRange(_id,1,1,lastColumn).getValues()[0],_id,headers);return result?data(200,result):error(404,"row_not_found",{_id:_id})}function handleGetMultipleRows(sheet,params){const lastColumn=sheet.getLastColumn(),headers=getHeaders(sheet),lastRow=sheet.getLastRow(),total=Math.max(lastRow-2+1,0),limit=null!=params.limit?+params.limit:total,isAsc="string"!=typeof params.order||"desc"!==params.order.toLowerCase();if(isNaN(limit)||limit<0)return error(404,"invalid_limit",{limit:limit});var firstRowInPage=isAsc?2:lastRow-limit+1;if(null!=params.start_id){const start_id=+params.start_id;if(start_id<2||start_id>lastRow)return error(404,"start_id_out_of_range",{start_id:start_id});firstRowInPage=start_id-(isAsc?0:limit-1)}const lastRowInPage=Math.min(firstRowInPage+limit-1,lastRow);if((firstRowInPage=Math.max(firstRowInPage,2))>lastRowInPage)return data(200,[]);const rows=sheet.getRange(firstRowInPage,1,lastRowInPage-firstRowInPage+1,lastColumn).getValues().map((function(item,index){return mapRowToObject(item,firstRowInPage+index,headers)}));isAsc||rows.reverse();var next=isAsc?lastRowInPage+1:firstRowInPage-1;return(next<2||next>lastRow)&&(next=void 0),data(200,rows.filter(isTruthy),{next:next})}function handlePost(sheet,payload){const row=mapObjectToRow(payload,getHeaders(sheet));return sheet.appendRow(row),data(201)}function handlePut(sheet,_id,payload){if(null==_id)return error(400,"row_id_missing",{});const row=mapObjectToRow(payload,getHeaders(sheet));return sheet.getRange(_id,1,1,row.length).setValues([row]),data(201)}function handleDelete(sheet,_id){return sheet.getRange("$"+_id+":$"+_id).setValue(""),data(204)}function httpResponse(data){return ContentService.createTextOutput(JSON.stringify(data)).setMimeType(ContentService.MimeType.JSON)}function error(status,code,details){return{status:status,error:{code:code,details:details}}}function data(status,data,params){params=params||{};const result={status:status,data:data};for(var key in params)params.hasOwnProperty(key)&&(result[key]=params[key]);return result}function getHeaders(sheet){const headers=sheet.getRange(1,1,1,sheet.getLastColumn()).getValues()[0];for(var i=headers.length-1;i>=0;i--)if(!isEmpty(headers[i]))return headers.slice(0,i+1);return[]}function isTruthy(x){return!!x}function isEmpty(item){return""===item||null==item}function find(array,predicate){if(Array.isArray(array))for(var i=0;i<array.length;i++)if(predicate(array[i]))return array[i]}function mapObjectToRow(object,headers){return headers.map((function(column){return isEmpty(column)||void 0===object[column]?"":object[column]}))}function mapRowToObject(row,_id,headers){if(row.every(isEmpty))return null;const result={_id:_id};for(var i=0;i<headers.length;i++)isEmpty(headers[i])||(result[headers[i]]=row[i]);return result}var users;function User(name,key,permissions){users||(users=[]),users.push({name:name,key:key,permissions:permissions})}function getUserWithKey(key){return find(users,(function(x){return x.key===key||"object"==typeof x&&x.key.__unsafe===key}))}function isStrongKey(key){const strongKeyRegex=new RegExp("^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[ -/:-@[-`{-~])(?=.{8,})"),user=getUserWithKey(key);return!!user&&(user.key.__unsafe===key||user.key.match(strongKeyRegex))}function getPermissions(user,spreadsheet){if(Array.isArray(user.permissions))return user.permissions;if("function"==typeof user.permissions)return user.permissions;const keys=Object.keys(user.permissions);for(var i=0;i<keys.length;i++)if(keys[i].toLowerCase()===spreadsheet.toLowerCase())return user.permissions[keys[i]];return user.permissions.ALL}function hasAccess(key,spreadsheet,method){const user=getUserWithKey(key);if(!user)return!1;const permission=getPermissions(user,spreadsheet);return!!permission&&!(permission!==ALL&&permission.toString()!==method&&!find(permission,(function(x){return x===ALL}))&&!find(permission,(function(x){return x.toString()===method})))}function GET(){}function POST(){}function PUT(){}function DELETE(){}function ALL(){}function UNSAFE(key){return{__unsafe:key}}GET.toString=function(){return"GET"},POST.toString=function(){return"POST"},PUT.toString=function(){return"PUT"},DELETE.toString=function(){return"DELETE"},ALL.toString=function(){return"*"};

```

{% hint style="info" %}
You can find a non-minified version of the above script at <https://github.com/ziolko/spreadapi>.
{% endhint %}

{% hint style="warning" %}
Always copy SpreadAPI script from [spreadapi.roombelt.com](https://spreadapi.roombelt.com/) or [github.com/ziolko/spreadapi](https://github.com/ziolko/spreadapi). Never copy the script from other pages to ensure it has not been malformed.
{% endhint %}

### Authentication

SpreadAPI uses a custom authentication mechanism. Directly in the script, you can define multiple *Users* with granular access levels. Each user has an *access key* used to authenticate him while accessing the API.

#### Access key

The *access key* should meet the following criteria:

* be at least 8 characters long
* contain at least one lower case (e.g. *a, b*, *c*)
* contain at least one upper case (e.g. *A*, *B,* C)
* contain at least one number (e.g. *1*, *2*, *3*)
* contain at least one special character (e.g. *#, ^* )

If you don't want to meet these criteria or need to leave the access key empty (e.g. to allow anonymous access) you can mark the password as *UNSAFE* as shown in the example below:

```javascript
// Anonymous account that has write access to a sheet "transactions"
User("anonymous", UNSAFE(""), { transactions: POST });
```

#### Scopes

Access can be granted for each sheet and operation (*GET*, *POST, PUT* and *DELETE)* separately. E.g. the following configuration allows anonymous user to add entries (but not read, delete or modify) to a sheet called *transactions* and read (but not add, modify or delete) from a sheet called *summary*.

```javascript
User("anonymous", UNSAFE(""), { transactions: POST, summary: GET });
```

You can allow more than one operation for a single sheet using the array syntax or all of them with an alias *ALL.*

```javascript
// Can add new entries and remove them from "transactions"
// and do all operations on "summary"
User("user1", "Passw0rd!1", { 
    transactions: [POST, DELETE], 
    summary: ALL
});

// Can read from all sheets, but can't modify
User("user2", "Passw0rd!2", { ALL: GET });

// Can do everything in all sheets
User("user3", "Passw0rd!3", ALL);
```

### Deployment

Once you have authentication configured you can deploy the API. To do that click *Deploy* -> *New deployment*

<figure><img src="/files/yJNT85ipbnQBDD2RkOZy" alt=""><figcaption></figcaption></figure>

On the *New deployment* popup select type *Web app* and choose the following options:

* *Execute as: Me,*
* *Who has access: Anyone*

<figure><img src="/files/6dAHRuWm80dGmAErie9X" alt=""><figcaption></figcaption></figure>

After clicking deploy you will be asked to grant the script authorization to your spreadsheet.&#x20;

{% hint style="info" %}
**Note:** You grant read/write access to your spreadsheet to the script you've just created. You **don't** grant access to your data to any third-party (not even to SpreadAPI author).
{% endhint %}

<figure><img src="/files/T1zQErsg0Q6cATcu3q8t" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
While granting access ensure that the only required permission is *View and manage spreadsheets that this application has been installed in* (as shown in the screenshot above).
{% endhint %}

### That's it!

After successfully deploying the script you should get its *URL*. You will use this URL to make requests to the spreadsheet API. See [usage](/usage) for details.

<figure><img src="/files/mn4OkjY2beHagdkKkxO9" alt=""><figcaption></figcaption></figure>


# Usage

### API design

SpreadAPI has to deal with limitations imposed by the Google Apps Script engine. There are two limitations that make running a *state-of-art* REST API impossible in this environment:

1. Each script can respond only on one hardcoded URL. It can't handle request comming at subpaths like */users* or */transactions/15*.
2. Only *GET* and *POST* methods are supported.

Due to the these limitations the SpreadAPI script handles only *POST* requests on a single URL. The actual HTTP method and resource path are provided in request body as shown in the example below:

```javascript
{
    "method": "GET",
    "sheet": "users"
}
```

Other parameters (like payload for *POST* and *PUT*  requests) are provided as additional fields in the request body:

```javascript
{
    "method": "PUT",
    "sheet": "users",
    "id": "2",
    "payload": {
        "firstname": "John",
        "lastname": "Smith"
    }
}

```

For more information on this topic visit [API Reference](/usage/usage) documentation page.

{% hint style="info" %}
For each request Google App Scripts first returns status 302 (redirect). To get result of your request you need to make a follow-up GET request (without any payload) to the  URL shared in the Location response headers. Some libraries do this automatically, but that's not always the case.
{% endhint %}


# API Reference

{% hint style="info" %}
SpreadAPI is maintained by the author of [Roombelt](https://roombelt.com) - simple and reliable meeting room display system.\
\
If you want to support SpreadAPI consider trying [Roombelt](https://roombelt.com) in your company!
{% endhint %}

{% hint style="success" %}
Check out [this live example ](https://jsfiddle.net/mkzL9ver/27/)to see how to use SpreadAPI from a browser application.
{% endhint %}

## Get multiple rows

<mark style="color:green;">`POST`</mark> `https://script.google.com/macros/s/SCRIPT_ID/exec/`

Return list of rows. Empty rows are skipped. Each row has an *\_id* field which is equal to row number in Google Sheets.

#### Request Body

| Name      | Type   | Description                                                                                             |
| --------- | ------ | ------------------------------------------------------------------------------------------------------- |
| method    | string | *GET*                                                                                                   |
| sheet     | string | Sheet name, e.g. *transactions* or *users*                                                              |
| key       | string | Authentication key. See page *Setup* -> *authentication* for details.                                   |
| order     | string | Order of results. Ascending by default. For descending use *DESC.*                                      |
| start\_id | number | ID of the first row to return. By default first row in the spreadsheet (or last row if *order = DESC*). |
| limit     | number | Maximum number of rows in response. By default all rows are returned.                                   |

{% tabs %}
{% tab title="200 " %}

```
```

{% endtab %}
{% endtabs %}

## Get single row

<mark style="color:green;">`POST`</mark> `https://script.google.com/macros/s/SCRIPT_ID/exec/`

#### Request Body

| Name   | Type   | Description                                                         |
| ------ | ------ | ------------------------------------------------------------------- |
| method | string | *GET*                                                               |
| sheet  | string | Sheet name, e.g. *transactions* or *users.*                         |
| id     | number | ID of row to get.                                                   |
| key    | string | Authentication key. See page *Setup -> authentication* for details. |

{% tabs %}
{% tab title="200 " %}

```
```

{% endtab %}
{% endtabs %}

## Insert row

<mark style="color:green;">`POST`</mark> `https://script.google.com/macros/s/SCRIPT_ID/exec/`

Adds a row to the end of sheet. To add multiple rows pass an array of objects e.g.:

```json
[ 
    { "method": "POST", "sheet": "users", "payload": { "Name": "Adam" } },
    { "method": "POST", "sheet": "users", "payload": { "Name": "John" } },
    { "method": "POST", "sheet": "users", "payload": { "Name": "Alex" } }
]
```

#### Request Body

| Name    | Type   | Description                                                           |
| ------- | ------ | --------------------------------------------------------------------- |
| method  | string | *POST*                                                                |
| sheet   | string | Sheet name, e.g. *transactions* or *users.*                           |
| key     | string | Authentication key. See page *Setup* -> *authentication* for details. |
| payload | object | Object with new row content.                                          |

{% tabs %}
{% tab title="200 " %}

```
```

{% endtab %}
{% endtabs %}

## Update row

<mark style="color:green;">`POST`</mark> `https://script.google.com/macros/s/SCRIPT_ID/exec/`

Update content of a single row. To update multiple rows pass an array of objects e.g.:

```json
[ 
    { "method": "PUT", "sheet": "users", "payload": { "id": 2, "Name": "Adam" } },
    { "method": "PUT", "sheet": "users", "payload": { "id": 10, "Name": "John" } },
    { "method": "PUT", "sheet": "users", "payload": { "id": 5, "Name": "Alex" } }
]
```

#### Request Body

| Name    | Type   | Description                                                           |
| ------- | ------ | --------------------------------------------------------------------- |
| method  | string | *PUT*                                                                 |
| sheet   | string | Sheet name, e.g. *transactions* or *sheets.*                          |
| id      | string | ID of row to update.                                                  |
| key     | string | Authentication key. See page *Setup* -> *authentication* for details. |
| payload | object | Object with the updated row content.                                  |

{% tabs %}
{% tab title="200 " %}

```
```

{% endtab %}
{% endtabs %}

## Remove row

<mark style="color:green;">`POST`</mark> `https://script.google.com/macros/s/SCRIPT_ID/exec/`

Clear a single row. Notice, that the row is not physically removed so that ids of the following rows don't change.

To remove multiple rows pass an array of objects e.g.:

```json
[ 
    { "method": "DELETE", "sheet": "users", "id": 2 },
    { "method": "DELETE", "sheet": "users", "id": 10 },
    { "method": "DELETE", "sheet": "users", "id": 5 }
]
```

#### Request Body

| Name   | Type   | Description                                                           |
| ------ | ------ | --------------------------------------------------------------------- |
| method | string | *DELETE*                                                              |
| sheet  | string | Sheet name, e.g. *transations* or *users*                             |
| id     | number | ID ofrow to remove                                                    |
| key    | string | Authentication key. See page *Setup* -> *authentication* for details. |

{% tabs %}
{% tab title="200 " %}

```
```

{% endtab %}
{% endtabs %}


# Examples

The idea to provide simple API to spreadsheets is very powerful. You can easily put data there and then analyze and report this data using all spreadsheet functionalities.

You can find a few use cases (with code!) for SpreadAPI below. If you have a nice use case to share let me know at <mateusz@roombelt.com>.

{% content-ref url="/pages/-Lv0P2Is26Eab2eIJCE0" %}
[Sign-up form](/examples/sign-up-form)
{% endcontent-ref %}

{% content-ref url="/pages/-Lv0PA8t8LEnYvtOK9Kj" %}
[404 errors reporting](/examples/404-reporting)
{% endcontent-ref %}


# Sign-up form

You can create newsletter sign-up in a few minutes with SpreadAPI. In this tutorial we will create a simple sign-up form with only an email field.

This example shows how to create a simple newsletter sign-up form in just a few minutes using SpreadAPI, allowing users to register their email with a single field.

![The sign-up form that we will create in this tutorial](/files/-Lv0VVWX34x5mzfwxVMy)

First, create a Google Sheet like the one below. It's just a single sheet called *emails* with a single column *email*.

![](/files/-Lv0W41-cNlUSDHtqca4)

Next, follow the [setup instructions](/setup) to configure API for your spreadsheet. While configuring authentication add the following line to the script so that everybody can add an entry to the `emails` sheet.

```javascript
User("anonymous", UNSAFE(""), { emails: POST });
```

Now you can now create your site with an HTML form. The code below is a good self-contained starting point, using Bulma CSS and jQuery. I added a nice confirmation message shown after signing up.

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/bulma.min.css"
    />
    <script
      src="https://code.jquery.com/jquery-3.7.1.min.js"
      integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
      crossorigin="anonymous"
    ></script>
    <title>Spreadapi Example Sign-up Form</title>
  </head>
  <body>
    <form id="newsletter-form">
      <div class="columns is-multiline">
        <div class="column is-6">
          <input
            class="input is-medium is-fullwidth"
            type="email"
            id="email"
            placeholder="Enter your Email"
          />
        </div>
        <div class="column is-6">
          <button
            class="button is-medium is-primary is-fullwidth is-clear"
            id="sign-up"
          >
            Sign up
          </button>
        </div>
      </div>
    </form>

    <!-- A dialog shown after successfully signing-up -->
    <div id="subscription-success-modal" class="modal">
      <div class="modal-background"></div>
      <div class="modal-content">
        <div class="box is-clearfix">
          <div>You've been added to the SpreadAPI subscription list!</div>
          <button
            class="button is-primary is-pulled-right"
            id="modal-ok-button"
          >
            OK
          </button>
        </div>
      </div>
      <button class="modal-close is-large" aria-label="close" />
    </div>

    <script>
      // This code requires jQuery

      var $form = $("#newsletter-form");
      var $email = $("#email");

      var $modal = $("#subscription-success-modal");
      var $signUp = $("#sign-up");

      var $modalOK = $("#modal-ok-button");

      $form.submit(function (e) {
        e.preventDefault();

        // Don't do anything if email field is empty
        var email = $email.val();
        if (!email) {
          return;
        }

        // Mark the "signup" button as "loading"
        $signUp.addClass("is-loading");

        // make the request
        $.post({
          // Replace the URL with the one specific for your script
          url: "https://script.google.com/macros/s/AKfycbzoEi2wm45iNgmtsLf6nzMIo4hxFpZvUKKFTXxJc1jEtN4W9gRo/exec",
          data: JSON.stringify({
            method: "POST",
            sheet: "emails",
            payload: { email },
          }),
        }).then(function () {
          // Remove the "loading" state from the "signup" button
          $signUp.removeClass("is-loading");

          // Show the popup saying that user has been added
          // to the subscription list
          $modal.addClass("is-active");
        });
      });

      // Once user clicks "OK" on the popup saying that he
      // has been added to the subscription list we want to
      // close the popup
      $modalOK.click(function () {
        $modal.removeClass("is-active");
      });
    </script>
  </body>
</html>
```

That's it. Now your users can sign up to the newsletter and you will add their emails on Google Sheets!


# 404 errors reporting

Imagine you own a company website. Over the time the structure of your page changes (redesign?) and some pages are either deleted or moved to another URLs.  Unfortunately it's so easy to forget configuring redirect for some of these pages and that leads to the 404 (not found) screens. If you knew what pages you're missing you could redirect them to the proper URLs in seconds.

You can easily monitor what pages are missing on your site by adding the following snippet to the 404 (not found) page HTML. The exact steps to edit this page source code depend on your web server or hosting provider but each of them should have this option.

```markup
<script>
var spreadApiURL = 'TODO'; // Read further to get the URL
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("post", spreadApiURL);
xmlHttp.send(JSON.stringify({
    method: "POST",
    sheet: "entries",
    payload: {
      time: new Date().toISOString(),
      url: document.location.href,
      referrer: document.referrer
    }
  })
);
</script>
```

The only thing missing in the above script is the value of variable `spreadApiURL`. To get this URL first create a spreadsheet with the following structure (single sheet called *entries* with columns *time, url* and *referrer*):

![](/files/-Lv0_obuqAm7kustVkE7)

Next, follow the [setup instructions](/setup) to configure API for your spreadsheet. While configuring authentication add the following line to the script so that everybody can add an entry to your sheet.

```javascript
User("anonymous", UNSAFE(""), { entries: POST });
```

Now simply put the Web App URL provided to you during the deployment step as a value of variable `spreadApiURL` and you are done!

Keep in mind that you can use all the power of Google Sheets to analyze your data! E.g. you can create an ordered list of URLs reported most frequently.


# Contact

If you have any questions, reach me at <mateusz@roombelt.com> or create an issue on [github](https://github.com/ziolko/spreadapi).


