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:
| Credential | Use case | How to send it |
|---|---|---|
| Widget access token | Custom widgets (getAccessToken) | Query parameter ?auth=<token> on URLs under baseUrl — Grist AccessTokenResult |
| API key | Server scripts, curl, integrations | Authorization: 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'sbaseUrl. - 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 unmountingBase64 (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.
Widget options
Every Grist Custom Widget can persist a JSON object alongside the section configuration. This is how you store user preferences — view modes, filters, theme tweaks, custom column rules, AI prompts,...
Widget linking & theme
Two Grist sections can be linked. A "selector" widget drives the cursor or row selection of another linked widget. Two methods cover this: