Wisegrid API · v1
API Reference OpenAPI Get a key →
Per-object scoped keys Partial-success bulk writes Idempotent retries Cursor pagination

Wisegrid API

A clean, predictable REST API for your sheets, columns and rows. Scoped keys, idempotent writes, partial-success bulk operations, and 600 requests/min/key with no 10× attachment tax — it stays the same fast call on a 1M-cell sheet.

Getting Started

From zero to your first row in five minutes.

The Wisegrid API is a versioned REST API (v1) over your spreadsheet data: list the sheets a key can read, read columns and rows, and write rows — single or in bulk. Every response is JSON with a stable requestId, every error uses one envelope, and every write can be safely retried.

The one-liner: same Smartsheet-style API surface, but with self-documenting string error codes, per-object scoped keys, true partial-success bulk writes, first-class idempotency, and 2× the rate budget — on sheets twice as large.

1. Mint an API key

Sign in to Wisegrid, then go to Settings → API Keys and create a key. Choose a name and the scopes it should carry (which sheets or projects, read or write — see Authentication & Scopes). The secret looks like wg_live_… and is shown exactly once — copy it into your secret manager immediately.

The secret is shown once. Wisegrid stores only a hash of your key, so it can never re-display the secret. If you lose it, revoke the key and mint a new one.

2. Authenticate

Send the key as a Bearer token on the Authorization header. The base URL is https://wisegrid.co/api/v1.

Authorization: Bearer wg_live_xxx

3. Your first call — confirm the key works

GET /me echoes the key's id, its owner, its scopes, and your current rate-limit window. It's the fastest way to confirm a key is live.

curl https://wisegrid.co/api/v1/me \
  -H "Authorization: Bearer wg_live_xxx"

A successful response:

{
  "keyId": 42,
  "ownerId": 7,
  "scopes": [
    { "verb": "write", "scopeType": "sheet", "scopeId": 128 }
  ],
  "rateLimit": { "limit": 600, "remaining": 599, "reset": 1717536000 },
  "requestId": "req_3kF9aZ…"
}

4. List your sheets, then add a row

Pick a sheet id from GET /sheets, then add a row with :bulkAdd. Cells are keyed by column id (as a string). Pass an Idempotency-Key so a network retry never double-inserts.

# List the sheets this key can read
curl https://wisegrid.co/api/v1/sheets \
  -H "Authorization: Bearer wg_live_xxx"

# Add a row to sheet 128 (cells keyed by column id)
curl -X POST "https://wisegrid.co/api/v1/sheets/128/rows:bulkAdd" \
  -H "Authorization: Bearer wg_live_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 1f7c2b9a-uuid" \
  -d '{ "rows": [ { "cells": { "88": "Doing", "90": "2026-06-30" } } ] }'

5. Install an SDK (optional)

Official, fully-typed SDKs — generated from the same OpenAPI contract this page renders — handle auth headers, retries on 429, and pagination for you.

# No SDK — just curl + the Bearer header, as shown above.

Jump to the TypeScript or Python quickstart, or keep reading the core concepts below.


Authentication & Scopes

Per-object scoped keys that can never exceed their owner.

API keys

Every request authenticates with an API key sent as a Bearer token: Authorization: Bearer wg_live_xxx. A missing, malformed, revoked, or expired key returns 401 unauthorized — the response never reveals whether the token shape was recognized.

  • Format. Live keys are prefixed wg_live_ followed by high-entropy random characters. The full secret is shown only at creation; afterwards only the short prefix (e.g. wg_live_3kF9) is visible in Settings so you can tell keys apart.
  • Storage. Wisegrid stores only a SHA-256 hash of the key — the raw secret is never persisted and can't be re-fetched.
  • Expiry. Keys expire (default one year, or a date you set at creation). An expired key returns 401.

Per-object scopes

A key isn't all-or-nothing. Each key carries one or more grants, and a grant is a verb on a scope object:

FieldValuesMeaning
verbread · writeread grants viewer-level access; write grants editor-level (which includes read).
scopeTypeproject · folder · sheetThe kind of object the grant covers.
scopeIdintegerThe id of that object.

Grants flow down the hierarchy: a write grant on a project covers every folder and sheet inside it; a grant on a folder covers the sheets in it. So you can mint one narrowly-scoped key for a single sheet, or one broad key for a whole project.

Scopes intersect with the owner's live access

A key can never exceed its owner's current access. On every request, the effective permission on a target is:

# the security boundary, evaluated live on every call
effective_role = min( owner_live_role(target), key_ceiling(verb) )
  • Live, not snapshotted. If the owner loses access to a sheet, every key they minted instantly loses it too — there's no cached grant to go stale.
  • Mint-time guard. You can only grant a scope you currently have at least that level of access to. Trying to over-grant returns 422 validation_failed identifying the offending scopeId.
  • Live revocation. Revoking a key takes effect on the very next request — the key returns 401 immediately.
Read vs. write posture. A target entirely outside a key's reach returns an opaque 404 not_found (so the API never confirms an object exists outside your scope). A target you can read but a write you're not scoped for returns 403 forbidden.

Storing & rotating the secret

  • Treat wg_live_… like a password: keep it in a secret manager / server-side env var, never in client-side code or a public repo.
  • Rotate by minting a new key, deploying it, then revoking the old one — zero-downtime. Revocation is immediate and irreversible.
  • Use several narrow keys (one per integration) rather than one broad key, so revoking one never breaks the others.

Rate Limits

600 requests per minute, per key — 2× the typical budget, with no attachment tax.

Each API key gets 600 requests per minute. There is no extra throttle on attachment or cell-history operations — the exact calls a migration leans on aren't penalized.

Headers on every response

HeaderMeaning
X-RateLimit-LimitThe ceiling for this key (600).
X-RateLimit-RemainingRequests left in the current window for this key.
X-RateLimit-ResetUnix epoch (seconds) when a slot frees up.
Retry-After(429 only) Whole seconds to wait before retrying.

Handling 429

Over the limit, the API returns 429 rate_limit_exceeded with a Retry-After header (also echoed as detail.retry_after_seconds). Wait that many seconds, then retry. Both official SDKs do this automatically (maxRetries / max_retries, honoring Retry-After).

# 429 response body
{
  "errorCode": "rate_limit_exceeded",
  "message": "Rate limit exceeded for this key.",
  "detail": { "retry_after_seconds": 12 },
  "requestId": "req_…"
}
# Honor the Retry-After header before retrying.

Errors

One envelope, stable string codes, a requestId on every response.

Every 4xx/5xx response uses the same shape:

{
  "errorCode": "validation_failed",   // a stable string slug you can switch on
  "message":   "A cell value failed type validation.",
  "detail":    { "column_id": 90, "reason": "not_a_date" },  // optional, code-specific
  "requestId": "req_3kF9aZ…"            // also in the X-Request-Id header
}

Switch on errorCode, not message — the slug is the stable contract; messages may be reworded. The requestId is on success and error alike (and in X-Request-Id), so quote it in a support ticket.

The frozen error-code set

These 11 slugs are the complete v1 set (additive only — never removed or redefined):

errorCodeHTTPWhen
unauthorized401Missing, malformed, revoked, or expired key.
forbidden403Readable target, but the key lacks write scope.
not_found404Object doesn't exist, or is outside the key's reach (opaque — no existence oracle).
validation_failed422A cell value failed type validation, or a malformed cursor / over-grant.
version_conflict409The row changed since the version you sent (carries currentVersion + current).
capacity_exceeded409A tier / per-sheet capacity limit was reached.
rate_limit_exceeded429Over 600 req/min for this key (see Retry-After).
idempotency_key_reused409An Idempotency-Key was reused with a different request body.
idempotency_in_progress409A request with the same Idempotency-Key is still being processed.
payload_too_large413The request body exceeded the allowed size.
internal_error500An unexpected server error (still enveloped — nothing leaks a non-enveloped body).
Inside a bulk write, per-row failures carry this same { errorCode, message, detail? } envelope on each item — the request as a whole still returns 200. See Bulk Writes.

Pagination

Opaque cursors, stable on a moving table — no offsets, no totals.

List endpoints (/sheets and /sheets/{id}/rows) are keyset (cursor) paginated. Pass limit (default 100, max 500) and an opaque cursor. Each page returns a nextCursor; pass it back to get the next page. nextCursor is null on the last page.

  • The cursor is an opaque token — pass it back verbatim, don't parse it.
  • No COUNT(*) / totals are exposed — keyset paging stays fast and stable even as rows are added mid-iteration (no skips or duplicates).
  • An over-max limit is clamped to 500 (not an error). A malformed cursor returns 422 validation_failed.
# First page
curl "https://wisegrid.co/api/v1/sheets/128/rows?limit=500" \
  -H "Authorization: Bearer wg_live_xxx"

# Response: { "data": [ … ], "nextCursor": "c_eyJ…", "requestId": "req_…" }

# Next page — pass nextCursor back
curl "https://wisegrid.co/api/v1/sheets/128/rows?limit=500&cursor=c_eyJ…" \
  -H "Authorization: Bearer wg_live_xxx"

Idempotency

Retry any write safely — the same key replays one result, never a double write.

Every write endpoint accepts an optional Idempotency-Key header (use a UUID per logical operation). If a request with that key already completed, the API replays the stored response and does not write again — so a dropped connection or an automatic retry can't create duplicate rows.

  • Replay window. A retried request with the same key replays the stored response within 24 hours.
  • Same key, different body409 idempotency_key_reused (you reused a key for a different operation).
  • Original still in flight409 idempotency_in_progress (wait and retry).
  • Scope. Keys are namespaced per API key — two different API keys may use the same idempotency string independently.
curl -X POST "https://wisegrid.co/api/v1/sheets/128/rows:bulkAdd" \
  -H "Authorization: Bearer wg_live_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 1f7c2b9a-3e6d-4c11-9a2f-uuid" \
  -d '{ "rows": [ { "cells": { "88": "Doing" } } ] }'
# Re-send the exact same request with the same key → the SAME result, no second row.

Bulk Writes & Partial Success

One bad row never sinks the batch — every row gets its own result.

POST …/rows:bulkAdd and POST …/rows:bulkUpdate accept up to 2000 rows and return HTTP 200 even on partial failure. Each input row gets a result, in order:

{
  "results": [
    { "index": 0, "status": "succeeded", "row": { "id": 5012, "version": 1, "cells": { … } } },
    { "index": 1, "status": "failed",
      "error": { "errorCode": "validation_failed", "message": "A cell value failed type validation.",
                "detail": { "column_id": 90, "reason": "not_a_date" } } }
  ],
  "succeededCount": 1,
  "failedCount": 1,
  "requestId": "req_…"
}
  • Each result carries index (its position in your request) and statussucceeded (with the written row) or failed (with an error envelope).
  • Per-row failures use the same error slugs: validation_failed, capacity_exceeded, version_conflict, not_found, forbidden.
  • Read the counts. Inspect succeededCount / failedCount and retry only the failed rows — don't assume 200 means everything wrote.

bulkUpdate uses version-CAS

Each update item carries the row id and the version you last read. If the row changed since, that item fails version_conflict with the current version and current row, so you can diff and retry — while the other rows still apply.

curl -X POST "https://wisegrid.co/api/v1/sheets/128/rows:bulkUpdate" \
  -H "Authorization: Bearer wg_live_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-uuid" \
  -d '{
    "rows": [
      { "id": 5012, "version": 3, "cells": { "88": "Done" } },
      { "id": 5013, "version": 1, "cells": { "88": "Blocked" } }
    ]
  }'

Endpoints at a glance

Nine endpoints. Base URL https://wisegrid.co/api/v1.

GET/meIdentify the key — owner, scopes, rate-limit window.
GET/sheetsList sheets this key can read (cursor-paged).
GET/sheets/{id}A sheet + its columns + first page of rows.
GET/sheets/{id}/columnsThe sheet's columns.
GET/sheets/{id}/rowsThe sheet's rows (cursor-paged).
POST/sheets/{id}/rows:bulkAddAdd up to 2000 rows (partial-success).
POST/sheets/{id}/rows:bulkUpdateUpdate up to 2000 rows (partial-success, version-CAS).
PATCH/sheets/{id}/rows/{rowId}Update one row's cells (version-CAS).
DELETE/sheets/{id}/rows/{rowId}Delete a row and its subtree.

Full schemas, parameters and response shapes are in the interactive reference below.


API Reference

The complete, interactive contract — rendered live from the OpenAPI spec.

This reference is generated directly from /api/v1/openapi.json — the same contract the SDKs are generated from — so it can never drift from what's deployed. Prefer raw Swagger UI? It's also at /api/v1/docs.


TypeScript SDK — @wisegrid/api

A first-class, fully-typed client with a typed error union.

Real .d.ts types on every resource, generated from the live OpenAPI contract. Every method returns { data, error, response }data is fully typed; error carries the stable { errorCode, message, detail? } envelope.

npm install @wisegrid/api

Methods: getMe(), listSheets(), getSheet(), listColumns(), listRows(), bulkAddRows(), bulkUpdateRows(), patchRow(), deleteRow().


Python SDK — wisegrid

A fully-typed client with httpx under the hood.

Each method returns the generated typed model (e.g. SheetListV1, RowV1, BulkWriteResponseV1) — or the typed ApiErrorV1 on a documented error.

pip install wisegrid

Methods: get_me(), list_sheets(), get_sheet(), list_columns(), list_rows(), bulk_add_rows(), bulk_update_rows(), patch_row(), delete_row().