Mint from your server
Your server mints the credential that authorizes an embed, and the API key that mints it never leaves your server. The SpreadSpace SDKs for TypeScript, Python and C# carry both mints, the session revoke and the webhook receiver. A backend in any other language calls the same endpoints directly, shown here as cURL.
Every mint on this page reads the embed-minting key, the key that holds
embed:write, from SPREADSPACE_EMBED_API_KEY. The Backend key from the
Quick start stays in SPREADSPACE_API_KEY, and
the receiver under Verify webhooks is the one sample on
this page that uses it.
Server-side use only. An API key must never appear in browser code: the
browser receives a signed_url and nothing else.
Install
Construct the client
Pass the embed key explicitly. A client that is given no key reads
SPREADSPACE_API_KEY. A key from the Backend preset does not hold
embed:write, so a mint with it answers 403 insufficient_scope. In
TypeScript and C#, an unset SPREADSPACE_EMBED_API_KEY is that case.
Mint an iframe handle
POST /api/embed/iframe-urls mints a single-use handle for one loan and one
surface and returns it inside a signed_url. Return that URL to your page:
<SpreadSpaceEmbed> from @spreadspace/react loads it, and
so does a plain iframe src. The handle is exchanged for the session token
inside the frame, so the token never reaches your page. The loan_id is the
loan intake provisioned for the application, as
Board loans from your system
shows, or one you created.
The response:
Each call mints a fresh handle, and a handle is exchanged once: a second load
of the same URL fails inside the frame. Mint at the moment the page asks, and
never cache or reuse a signed_url.
Mint an embed session
POST /api/embed/sessions returns the token itself, embed_token, a
loan-scoped bearer for a client of your own that calls the API without the
iframe. The token is returned once, and no endpoint retrieves it again. The
request takes loan_id (required), scopes, expires_in_seconds (default
3600, at most 86400), external_user_id and display_name.
The response carries embed_token, session_id, expires_at (when the token
expires), loan_id, borrower_id, scopes and persistence, plus
external_user_id and display_name when the mint carried them.
Scopes
Name scopes on every mint. Read scopes are granted by mint authority: a key
that holds embed:write may name documents:read, extractions:read and
spreads:read whether or not it holds them. A write scope is granted only
when the request names it and only if the minting key holds it. No other
scope is mintable: any other named scope, or a wildcard, fails the mint with
400 embed_scope_not_allowed, and a named write the key does not hold fails
with 400 embed_scope_exceeds_key.
To give an analyst write access, name the writes beside the reads. This body mints a session that may finalize spreads and upload documents, from a key that holds both writes, as the dashboard’s Embed preset does:
In the SDKs the same list goes in scopes. A session with none of the writes
renders read-only and offers no Finalize. The token never carries
embed:write, so a leaked token cannot mint further sessions or widen its own
scope, and every scope it holds stays bound to the one loan.
Lifetimes
A value above its maximum is rejected with 400 invalid_request. The handle
dies at its first exchange, and the token’s lifetime runs from that exchange.
When the token nears expiry, or the API rejects it, the frame asks the host
page for a fresh handle. <SpreadSpaceEmbed> answers by calling your mint
endpoint again, so your server sees one more mint. The
Embed integration guide walks through the
refresh.
Per-user state
Both mints accept an optional external_user_id: an opaque, stable identifier
for the end user you are embedding for, up to 256 characters after trimming.
Use a durable id from your own system rather than an email address, which can
change. Derive it from your backend’s own session, never from the browser
request, so one user cannot open another’s saved state. Supply it and the
embed keeps that user’s state on the server, keyed to your organization and
the id you send, so ids only need to be unique within your own system. Saved
boards and view settings, and the memo template library, follow the user
across sessions and devices. Without it each session starts from defaults and
saves nothing.
The session response echoes external_user_id back, trimmed. On the iframe
path the id is stored on the handle and copied onto the session at exchange,
so both routes yield the same identity-carrying session. Send the same id for
the same person on every mint. A blank external_user_id is rejected.
display_name is optional, up to 120 characters. It is the name the workspace
shows for that user on the spreads they save and finalize, on their comments
and in live presence. It is echoed back, never written to a log and never
placed in the token. It requires external_user_id in the same request, and
sending one alone is rejected.
Every session response also carries persistence, which says which mode you
got: user when the mint included an external_user_id, session when it
did not. A session value means the embed renders and works normally but
saves nothing for the viewer. Assert on it in your integration tests, so a
dropped external_user_id fails your build instead of quietly discarding a
day of your analyst’s work. The handle response carries no persistence; to
assert it, mint a session.
Revoke a session
DELETE /api/embed/sessions/{sessionId} revokes a session minted with
POST /api/embed/sessions. The key that minted the session revokes it, the
call needs embed:write, and it answers 204. The revoked token is refused
from then on.
A session created by a handle exchange has no id your server ever sees, so
there is nothing to revoke per session on the iframe path. The control point
there is the key: removing embed:write from it severs every token it minted
and every handle still waiting to be exchanged, and the key’s other traffic
keeps flowing.
Verify webhooks
When the analyst finalizes inside the frame, spread.finalized reaches your
backend as a webhook. The SDK’s receiver verifies the
SpreadSpace-Signature header, drops duplicates, reads the snapshot the event
names, and hands back the status and body to answer with. It reads that
snapshot with spreads:read, which the Embed preset does not hold, so build it
from a client that uses the Backend key in SPREADSPACE_API_KEY.
The signature covers the exact bytes received. Give the receiver the raw
request body, never a parsed and re-serialized one. In Express,
express.raw({ type: 'application/json' }) on the route makes req.body those
bytes; Express does not set a raw body on the request by itself.
To verify without the receiver, call verifyAndParseWebhook (TypeScript),
verify_and_parse_webhook (Python) or WebhooksResource.VerifyAndParse (C#)
with the same raw body, the header value and the endpoint’s signing secret.
Each raises a signature error when verification fails. The
Webhooks page has the signature scheme, the receiver’s
options and secret rotation.
Errors
Each SDK raises a typed error that carries the wire type and the request id.
Match on the class, then read the type.
The full error vocabulary is on the Errors page.