Foldkit docs

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_
NameSecret keyPublishable key
Lives inYour server’s environmentYour page’s source
GrantsIts scopes, to whoever holds it, from anywhereThe anonymous tier, and only from an origin you verified
If it leaksRotate immediately. It is a full credential.Nothing an attacker could not do by loading your own page.
Shown to youOnce, 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.

ScopeLets a key
designs:readRead designs, their documents, versions and previews.
designs:writeCreate, rename, archive and delete designs.
exports:readList export jobs and fetch their download URLs.
exports:writeStart an export. This spends your export allowance.
assets:readRead uploaded images and their metadata.
assets:writeUpload images for use in a design.
quotes:readRead quote requests your customers submitted.
quotes:writeCreate quote requests.
templates:readRead the box-style catalog and the template gallery.
usage:readRead your own metered usage, for reconciling an invoice.
webhooks:manageCreate, update and delete webhook endpoints, and read the delivery log.
embed:createMint 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 localStorage or 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.

PermissionLets the end-user
designs:readOpen a design and see it. Without this the editor is blank.
designs:writeEdit and save. Omit it for a read-only preview embed.
exports:writeStart an export from inside the editor.
assets:writeUpload their own images.
quotes:writeSubmit 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 schemaVersion and 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.