# Guides · REST API

> Stay within rate limits, scope API keys, and use smart tags and cell references.

HTML version: https://developers.sheetgo.com/api/guides/

[Developers](/index.md) › [REST API](/api.md) › Guides

REST API · guides

## Guides

Topics that apply across many endpoints. In the API's own docs: [Rate Limiting](https://api.sheetgo.com/rest/guides/rate-limiting) · [API Key Scopes](https://api.sheetgo.com/rest/guides/api-key-scopes) · [Smart Tags](https://api.sheetgo.com/rest/guides/smart-tags) · [Cell References](https://api.sheetgo.com/rest/guides/cell-references).

## Rate Limiting

To keep the platform fast and fair, the API limits how many requests each
account can make in a short window. If you exceed the limit, further requests are
temporarily rejected with **HTTP 429 (Too Many Requests)** until the window
resets. Rate limits protect you too — they stop a runaway script from exhausting
your account's capacity.

### How the limits work

- **Per account.** Limits apply to your Sheetgo **user account**, across *all* of
  your API keys combined. Creating extra API keys does **not** add capacity — the
  budget is shared by the account.
- **Two separate budgets:**
  - **Reads** — `GET` and `HEAD` requests (higher allowance).
  - **Writes** — `POST`, `PUT`, `PATCH`, `DELETE` requests (lower allowance).
- **Per-minute window.** Each budget refills on a rolling one-minute basis.

Current default limits (per account, per minute): **120** read, **30** write.
Do **not** hard-code these numbers — some accounts have higher allowances and
limits may change. Read the limit from the response headers instead.

### Response headers (on every response)

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Your current limit for that category (read or write). |
| `X-RateLimit-Remaining` | Requests left in the current window. |
| `X-RateLimit-Reset` | Seconds until the window resets. |

When `X-RateLimit-Remaining` approaches `0`, slow down **before** you get
blocked — the most effective way to avoid 429s.

### When you are throttled

A `429` response includes a `Retry-After` header and a JSON body:

```
{
  "error": "rate_limit_exceeded",
  "message": "Rate limit exceeded: 120 read requests per minute. Retry after 37 seconds.",
  "retry_after": 37,
  "limit": 120,
  "remaining": 0,
  "reset": 37,
  "scope": "read"
}
```

| Field | Meaning |
| --- | --- |
| `error` | Stable code: always `rate_limit_exceeded`. |
| `message` | Human-readable explanation. |
| `retry_after` | Seconds to wait before retrying (same as the `Retry-After` header). |
| `limit` / `remaining` / `reset` | Your limit, requests left (`0`), seconds to reset. |
| `scope` | Which budget you exhausted: `read` or `write`. |

### Consuming the API reliably

1. **Honor `Retry-After` on a 429** — wait at least that many seconds before retrying.
2. **Use exponential backoff with jitter** for other transient errors.
3. **Self-throttle** using `X-RateLimit-Remaining` — slow down proactively.
4. **Spread work out** with small delays rather than bursting.
5. **Cache and avoid redundant calls** — reads count even when data is unchanged.
6. **Treat 429 as normal** — catch it, wait, and retry transparently.

Reference retry loop:

```
for attempt in range(max_retries):
    resp = call_api(...)
    if resp.status_code != 429:
        return resp
    wait = int(resp.headers.get("Retry-After", 2 ** attempt))
    sleep(wait + random_jitter())
```

### Automated agents and LLM tools

The response **body** carries everything needed to recover (`retry_after`,
`scope`, and a plain-language `message`) — parse the body, not just the status
code. Honor `retry_after` rather than retrying in a tight loop. Most HTTP client
libraries already back off automatically on `429` + `Retry-After`; keep that on.

### Need higher limits?

If your legitimate workload consistently approaches the limits, contact **Sheetgo
support** to raise your account's capacity. A change takes effect automatically —
and because you read the limit from `X-RateLimit-Limit`, your integration needs no
code change.

## API Key Scopes

Sheetgo API keys let apps, scripts, and AI agents act on your Sheetgo account.
By default a key can do **everything you can** — read, edit, run, and delete any
workflow, automation, form, and more.

**Scopes** let you narrow that down. You can limit a key to certain **actions**
(view only, or edit, or delete) and to certain **items** (all of your workflows,
or only a specific few). This is useful when you hand a key to an **AI agent or
script** that should only read data, an **integration** that should touch only
one or two workflows, or a **partner** who should have limited access.

> **Scopes only limit the key — not you.** Restricting a key never changes what
> you can do when you log in to Sheetgo yourself; it only limits requests made
> with that key.

### The two things a scope controls

A scope combines **what actions** are allowed with **which items** they apply to.

#### Actions

| Action | What it allows |
| --- | --- |
| **View** (read) | See and list items — read a workflow, list automations, view a form or schedule. |
| **Edit** (write) | Create, change, and **run** items — edit a workflow, **trigger a run**, set a schedule, submit a form. |
| **Delete** | Remove items — archive/delete a workflow, delete a form, remove a trigger. |
| **Full** | All of the above. |

> **Running a workflow counts as "Edit", not "View."** A View-only key cannot
> start runs — give it Edit if it needs to.

#### Items

For each action you can choose **all items** of that type (all workflows, all
automations, …) or **only specific items** you pick. You can also apply a scope
to **everything at once** (all resource types) — for example, "View everything."

**Item types you can scope:** workflows, automations, forms, triggers
(schedules), webhooks, Outlook, and API destinations.

### Setting scopes in your Account

1. Go to **Account → Services and account → Sheetgo API**.
2. **Create a new key**, or open an existing key to **edit** it.
3. Choose the access level:
   - **Full access** *(default)* — the key can do everything.
   - **All items, limited actions** — e.g. *View only* across everything.
   - **Custom** — pick actions per item type, and optionally restrict each to
     specific items.
4. **Save.** When creating a key, copy it **immediately** — it is shown only
   once. When editing, changes take effect right away; **you don't need to
   regenerate the key** — the same key keeps working with its new limits.

### Examples

- **Read-only monitoring key** — *View → All items.* Lists and reads everything;
  cannot edit, run, or delete.
- **Run/edit only two workflows** — *Workflows → Edit → "Monthly Report",
  "Sales Sync".* Can run and edit those two, nothing else.
- **Read everything, edit one** — *View → All items* **plus** *Workflows → Edit
  → "Client Import".* A common "safe agent" setup.
- **Partner access to one workflow** — *Workflows → Full → "Partner Feed".* View,
  edit, run, and delete that one workflow only.
- **No restriction (default)** — *Full access.*

### Good to know

- **Restricting to specific items means the key can't create new ones.** Limiting
  *Edit* to a specific list lets the key change those items but **not create
  brand-new ones**. Give *Edit → All items* to allow creation.
- **Forms and schedules follow their workflow.** A form or trigger is reachable
  only if the key can also **view its workflow**.
- **Lists respect the scope.** A key limited to specific workflows sees only those
  when it lists workflows.
- **Keep keys secret.** A key is shown only once; if it leaks, delete it and
  create a new one. Scopes reduce the damage a leaked key can do but don't replace
  keeping it safe.
- **Changing a scope doesn't rotate the key** — editing limits keeps the same key
  string working.

## Smart Tags

A **smart tag** is a `{{placeholder}}` that Sheetgo replaces with a value from
your data while an automation runs. They are used by the `document`
destination — both inside the Google Docs template and in the destination's
`file_name`.

### How a tag resolves

A tag matches a **column header** in the data reaching the destination, and is
replaced with that row's value. One document is generated per row.

| Data header | Tag to write | Result |
| --- | --- | --- |
| `Customer` | `{{Customer}}` | `Acme Ltd` |
| `Invoice No` | `{{Invoice No}}` | `2026-0042` |

Header names are matched as they appear in your data, spaces included. There is
no mapping to configure: name the tag after the column.

### Where tags can be used

- **In the template document** (`document` destination) — anywhere in the body,
  including headers, footers and tables.
- **In `file_name`** (`document`) — so each generated document gets a distinct
  name, e.g. `"Invoice {{Invoice No}} - {{Customer}}"`.
- **In `subject` and `body`** (`share`, `mail_merge`) — the email is rendered
  per row, so each recipient sees their own values.
- **As a recipient** (`share`, `mail_merge`) — `to`, `cc`, `bcc` and `reply_to`
  accept a `"{{Header}}"` string instead of addresses, which reads the address
  from that column. The reference must be the whole value: `"{{Email}}"` works,
  `["{{Email}}"]` and `"cc: {{Email}}"` do not.

### When a tag has no matching column

`unused_fields` decides:

- `error` (default) — the run fails, so a template/data mismatch is visible.
- `ignore` — the tag is replaced with an empty string and the run continues.

### Reserved tags

| Tag | Meaning |
| --- | --- |
| `{{!group-start}}` … `{{!group-end}}` | Repeat the enclosed block once per row within a group, instead of generating one document per row. |
| `{{image:Column}}` | Insert the image at the URL held in `Column`. `img:`, `picture:` and `pic:` are accepted as equivalents. |

### PDF settings

The `pdf` destination has no smart tags — generated PDFs take their name from
the file they were converted from. Its `paper_size`, `page_orientation`,
`scale` and `merge_files` fields control the export. Note that `paper_size`
values for ISO sizes are **uppercase**: `A3`, `A4`, `A5`, `B4`, `B5`.

## Cell References

A **cell reference** is a `{{Tab!Ref}}` placeholder you put inside an `api`
source's request. When the automation runs, Sheetgo reads that cell from the
automation's Google Sheets destination and substitutes the value before the
request goes out — so the request changes as your sheet changes, with no API
call needed.

```
GET https://api.example.com/events?since={{Log!A:last|date|fmt:iso_dt}}
```

### Which `{{ }}` am I looking at?

Three unrelated syntaxes share the double-brace shape. They are not
interchangeable.

| Syntax | Example | Where it works |
| --- | --- | --- |
| **Cell reference** | `{{Sheet1!A1|date|fmt:iso_d}}` | `api` source — `endpoint`, `headers`, `body` |
| **Smart tag** | `{{Customer}}` | `document`, `share`, `mail_merge` destinations |
| **Body template** | `{{DATA}}`, `{{for row in ROWS}}` | `webhook` destination `body` |

A cell reference always contains a `!`. A smart tag is a bare column name. The
body template uses the reserved names `DATA` and `ROWS` and the block tags
`{{for}}`, `{{if}}` and `{{except-last}}`.

### Grammar

```
{{ <tab>!<ref> [|<filter>]* }}
```

- **`<tab>`** — a tab in the destination spreadsheet. Quote it with single
  quotes if it contains a space, a special character, or starts with a digit:
  `{{'My Sheet'!A1}}`, `{{'2026 Sales'!A1}}`. Double an apostrophe inside a
  quoted name: `{{'O''Brien'!C3}}`. When in doubt, quote it — the quoted form
  always works.
- **`<ref>`** — a single cell (`A1`, `B7`, `AB42`) or `A:last`, the last
  non-empty cell in column A. Case-insensitive. Ranges (`A1:B5`), whole columns
  (`A:A`), whole rows, named ranges and formulas are **not** supported.
- **`|<filter>`** — optional, chainable, applied left to right.

### Filters

**Render mode** — must be the *first* filter if used. Default is `F`.

| Filter | Value you get | Cell displaying `$1,234.50` |
| --- | --- | --- |
| `F` *(default)* | the formatted display text | `"$1,234.50"` |
| `U` | the raw underlying value | `1234.5` |

`F` and `U` after another filter are invalid, and the placeholder is left as
literal text.

**String** — `trim` (strip whitespace), `upper`, `lower`.

**Numeric** — `int`, `float`. Both **fail the run** on a non-numeric value.

**Date input** — promote the value to a date, always doing work:

| Filter | Behaviour |
| --- | --- |
| `date` | auto-detect, US-leaning on ambiguous input |
| `date_us` | month first — `01/02/2026` is 2 January |
| `date_eu` | day first — `01/02/2026` is 1 February |
| `date_iso` | strict ISO 8601 only |

Accepts strings, Sheets serial numbers, and existing dates. On its own a date
filter renders ISO datetime; chain a `fmt:*` to change that.

**Date output** — `fmt:*`, a **no-op unless a date input filter came first**:

`fmt:day`, `fmt:month`, `fmt:year`, `fmt:timestamp` (Unix seconds),
`fmt:timestamp_ms` (Unix milliseconds), `fmt:serial` (Sheets serial),
`fmt:iso_dt`, `fmt:iso_d`, `fmt:us_dt`, `fmt:us_d`, `fmt:eu_dt`, `fmt:eu_d`.
`_dt` includes the time, `_d` is date only.

### Where they go

| Location | Substitution |
| --- | --- |
| `endpoint` | URL-encoded per component. Keep placeholders out of the scheme and host. |
| `headers` | verbatim, with CR/LF stripped |
| `body` | **verbatim, no escaping** — you are responsible for valid JSON. `"{{A1}}"` yields a quoted string; `{{A1|U}}` unquoted yields a bare JSON value. |

### When something goes wrong

| Situation | Behaviour |
| --- | --- |
| Invalid syntax, unknown filter, unbalanced braces | treated as **literal text** — your API receives the raw `{{...}}` |
| Tab does not exist | run **fails**, naming the placeholder and its location |
| Empty cell, out-of-bounds cell, `:last` on an empty column | substituted as `""`, warning logged, run continues |
| Numeric or date conversion fails | run **fails** |
| Network failure reading the sheet | retried 3× with backoff, then the run fails |

### ⚠️ Requires a Google Sheets destination

Substitution reads from the automation's destination, so it only happens when
that destination is a Google Sheets file. With any other destination the
placeholders are sent to your API as literal text, with no warning.

### ⚠️ Never resolves alongside a `webhook` destination

A `webhook` destination must be an automation's only destination, so such an
automation has no Sheets destination to read from. Cell references in it will
always be sent literally. Pair cell references with a `file` destination
instead.
