Concepts
Five words that mean something specific here. Worth ten minutes before you write any code.
Tenant
A tenant is you. It owns your branding, your catalog, your keys, your origins, your designs and your webhooks, and it is the boundary everything else is scoped by — every row we store carries a tenant, and every query names one.
You will never see another tenant’s data, and the way that is arranged is worth knowing because it changes what an error means: a design id belonging to someone else does not come back as 403 Forbidden. It comes back as 404, identical to an id that never existed. There is no request you can make that distinguishes the two.
Keys
Two classes, and they are a different security model rather than a different style of key.
sk_ | pk_ | |
|---|---|---|
| Name | Secret key | Publishable key |
| Lives in | Your server’s environment | Your page’s source |
| Grants | Its scopes, to whoever holds it, from anywhere | The anonymous tier, and only from an origin you verified |
| If it leaks | Rotate immediately. It is a full credential. | Nothing an attacker could not do by loading your own page. |
| Shown to you | Once, at creation. We store only a hash. | Whenever you ask — it is not a secret. |
Both come in a live and a test environment, encoded in the prefix — sk_live_, pk_test_ — so you can see at a glance which one you pasted. That is the single most common integration mistake there is.
Scopes
A secret key grants exactly the scopes it lists and nothing else. A route that needs a scope your key does not hold answers 403 insufficient_scope — so adding a route can never silently widen a key you already issued.
| Scope | Lets a key |
|---|---|
designs:read | Read designs, their documents, versions and previews. |
designs:write | Create, rename, archive and delete designs. |
exports:read | List export jobs and fetch their download URLs. |
exports:write | Start an export. This spends your export allowance. |
assets:read | Read uploaded images and their metadata. |
assets:write | Upload images for use in a design. |
quotes:read | Read quote requests your customers submitted. |
quotes:write | Create quote requests. |
templates:read | Read the box-style catalog and the template gallery. |
usage:read | Read your own metered usage, for reconciling an invoice. |
webhooks:manage | Create, update and delete webhook endpoints, and read the delivery log. |
embed:create | Mint embed session tokens. The one every integration needs. |
A publishable key holds no scopes at all, by construction: its privilege is fixed at the anonymous tier, and a scope list on it would be a claim nothing honours.
Embed sessions
A session is how the editor in the iframe knows who it is serving. Your backend mints one with your secret key, hands the resulting es_… token to your own page, and the page passes it to the SDK. The iframe exchanges it, once, for a short-lived access token that lives in memory only.
const session = await foldkit.embedSessions.create({
externalUserId: customer.id, // your id for them, opaque to us
designId: order.designId, // optional: resume an existing design
permissions: ['designs:read', 'designs:write', 'exports:write'],
})- The
es_token is single-use and short-lived. A stolen one is worth one exchange inside its window. - The access token lasts 15 minutes, never touches
localStorageor a cookie, and dies with the frame. - There is no refresh token. Renewal goes back to the party that was allowed to mint in the first place — your backend — which is what bounds a stolen token’s usefulness to its own lifetime.
Permissions
A session carries a permission set, decided by your backend when it mints. The editor hides what a session cannot do rather than showing a disabled control, and the API refuses it independently — so a page that calls a command it lacks permission for gets not_permitted rather than a silent no-op.
A permission is not a scope, and the two vocabularies are separate on purpose. A scope answers “what is this partner’s backend allowed to ask us for”; a permission answers “what may this one end-user, in this one iframe, for the next fifteen minutes, do”. You mint a session with a subset of what your key could do — a read-only preview on a product page, a full editor in the configurator — and the iframe cannot widen it, because the permissions are claims inside a token we signed.
| Permission | Lets the end-user |
|---|---|
designs:read | Open a design and see it. Without this the editor is blank. |
designs:write | Edit and save. Omit it for a read-only preview embed. |
exports:write | Start an export from inside the editor. |
assets:write | Upload their own images. |
quotes:write | Submit a quote request. |
Passing a designId when you mint pins the session to it: the resulting token cannot reach any other design, whatever its permissions say.
Designs and documents
Two things share the word “design” in most conversations, and separating them explains most of the API.
- The design is the row: an id, a name, a style, an owner, a version history. This is what you store against an order line.
- The document is what is inside it: box parameters, artwork on each surface, font references. It carries a
schemaVersionand is migrated forward when we evolve it — which is why you should treat it as opaque and read it through the API rather than storing a copy.
The dieline is a third thing and is never stored: it is recomputed from the box parameters every time. That is what guarantees the flat canvas, the 3D preview and the print file cannot disagree with each other.
Everything is in millimetres, in flat dieline space — origin top-left, x right, y down. If you ever compute a coordinate against one of our exports, that is the frame it is in.