Grist Widgets
SDKGuide

Attachments & REST

When you need data the plugin API doesn't expose directly — attachment bytes, SQL queries, the document's REST endpoints — the SDK gives you a signed `fetch` that handles authentication for you.

When you need data the plugin API doesn't expose directly — attachment bytes, SQL queries, the document's REST endpoints — the SDK gives you a signed fetch that handles authentication for you.

Access tokens

Grist short-lived access tokens authorize REST calls against the current document. useGrist() exposes:

const token = await w.getAccessToken({ readOnly: true })
// { token: string, baseUrl: string }

Tokens are cached briefly (~45s) and single-flighted per readOnly flag. On 401, the cache clears and the call is retried once. You should rarely need to call getAccessToken directly — prefer w.fetchWithAuth(...) or w.uploadAttachment(...).

Widget tokens vs API keys

Grist documents two REST credentials:

CredentialUse caseHow to send it
Widget access tokenCustom widgets (getAccessToken)Query parameter ?auth=<token> on URLs under baseUrlGrist AccessTokenResult
API keyServer scripts, curl, integrationsAuthorization: Bearer <API-KEY>REST API usage

Never put an API key in widget code. The SDK attaches widget tokens as ?auth= for all browser REST calls (downloads and uploads).

REST fetch

const response = await w.fetchWithAuth("/api/docs/{docId}/sql", {
  method: "POST",
  body: JSON.stringify({ sql: "SELECT * FROM Tasks LIMIT 10" }),
  headers: { "Content-Type": "application/json" },
})
const data = await response.json()

path may be:

  • An absolute URL (https://...) — used as-is.
  • A path with leading / — resolved against the token's baseUrl.
  • A relative path — treated as /${path}.

For all methods (including POST uploads), the widget access token is appended as ?auth= (Grist’s documented widget pattern). The SDK does not send Authorization: Bearer with access tokens from the browser; use API keys only outside widgets.

Pass readOnly: true when calling read-only endpoints; the token is scoped lower and stays valid longer.

await w.fetchWithAuth("/api/docs/.../tables/Tasks/records", { readOnly: true })

Attachments

Grist attachment columns store an array of file refs. The SDK handles extracting ids and downloading bytes.

Upload (write access)

Requires requiredAccess: "full". Prefer the built-in helper:

const { ids, firstId } = await w.uploadAttachment(file)

This performs POST /attachments with multipart/form-data field upload, header X-Requested-With: XMLHttpRequest, and ?auth= from getAccessToken({ readOnly: false }). Grist returns a JSON array of ids, e.g. [42] — see parseGristAttachmentUploadResponseIds if you call fetchWithAuth yourself.

Uploading only stores the file on the document; you still link the id to a row:

const { firstId } = await w.uploadAttachment(file)

// New row: single attachment
const fields = w.mapBack({ Photos: gristAttachmentCellValue(firstId) })
await w.table.create({ fields })

// Existing row: append to the cell
const fields = w.mapBack({
  Photos: mergeGristAttachmentCellValue(row.Photos, [firstId]),
})
await w.table.update({ id: row.id, fields })

Cross-origin widgets (CORS)

If the widget is hosted on a different origin than Grist (e.g. localhost:5173, ngrok), the browser blocks REST unless the Grist server allows your widget origin and headers such as X-Requested-With. A failed preflight often shows as TypeError: Failed to fetch with no HTTP status. See grist-core#2040 for operator notes.

Signed URL only

const url = await w.getAttachmentUrl("123", { readOnly: true })
// Use the URL directly in <img src={url} />, <a download href={url} />, etc.

Blob (preferred for large files)

const { blob, contentType } = await w.fetchAttachmentBlob("123")
const objectUrl = URL.createObjectURL(blob)
// remember to URL.revokeObjectURL(objectUrl) when unmounting

Base64 (for embedding directly)

const { base64, contentType } = await w.fetchAttachmentBase64("123")
const dataUrl = `data:${contentType};base64,${base64}`

Use Blob for anything more than a few KB; Base64 is convenient for inline icons or PDF previews.

Working with cells

Attachment columns hold arrays of file refs. Use the helpers from the package root:

import {
  extractGristAttachmentIdsFromCell,
  extractGristAttachmentId,
  getAttachmentDownloadUrlForCell,
  fetchAttachmentBase64ForCell,
} from "grist-widget-sdk"

const ids = extractGristAttachmentIdsFromCell(row.Photos)
const firstUrl = await getAttachmentDownloadUrlForCell(docApi, row.Photos, 0)

docApi is w.fetchTable-class methods; you'd normally use this helper from a non-React context. Inside React, prefer w.getAttachmentUrl(id) after extracting ids.

Required access level

You need at least requiredAccess: "read table" for read-only tokens, and requiredAccess: "full" for read-write REST. Set this on <GristWidgetProvider> (or via w.configure({ requiredAccess: "full" }) at runtime).

Error handling

Both fetchWithAuth and attachment helpers throw on non-2xx responses with a generic message. For richer handling, inspect the Response yourself:

const response = await w.fetchWithAuth("/api/...", { readOnly: true })
if (!response.ok) {
  const body = await response.text()
  throw new Error(`Grist REST failed (${response.status}): ${body}`)
}

For the 401 retry path, the SDK's internal token cache is cleared automatically — your code keeps working after a brief renewal.

On this page