SDK changelog
Every release of @capacms/sdk, newest first.
The SDK reference covers the current release. Install it with pnpm add @capacms/sdk@next.
1.0.0-next.8 (2026-10-02)
- 1.0.0-next.8. Preview on a live site, in draft mode on its production
domain.
preview()takes the legacy key a site already holds (pk_,sk_or unprefixed), as the API does: Capa checks the token with any key that reads the project, and refuses a token made for another project. It used to throw aTypeErrorfor a legacy key before any request. Draft reads through the SDK still take acap_key only, and the legacy-key warning says so. - Next.js:
createPreviewRouteandexitPreviewRoutetake Next'scookies. Given it, the draft cookie is re-setHttpOnly; Secure; SameSite=None; Partitioned; Path=/; Max-Age=3600(maxAgechanges the hour), so Safari 26.2 and later send it inside the editor's frame and it no longer lasts until the browser closes. Exit and a bad link delete it partitioned, which is the only deletion that reaches it.redirectis optional now: left out, each route answers with its own 307, markedX-Robots-Tag: noindex, nofollow,Referrer-Policy: no-referrerandCache-Control: private, no-store, and the preview route's carries the editor'sframe-ancestors. Givenredirect, a route calls it as before. - Next.js:
capaHeaders({ adminOrigins })returns rules fornext.config'sheaders()that sendX-Robots-Tag: noindex, nofollowandContent-Security-Policy: frame-ancestors 'self' https://app.capacms.comon a request carrying the draft cookie or acapa-preview,capa-editorcapa-viewquery, and on nothing else, so a visitor's response is unchanged. Also exported:draftHeaders,frameAncestors(it throws for a value that is not a bare origin),frameDraftCookie,clearDraftCookie,CAPA_ADMIN_ORIGIN,DRAFT_ROBOTS_TAG,DRAFT_COOKIE_MAX_AGE,PREVIEW_PARAMandVIEW_PARAM. - Next.js:
capaMiddlewaremarks every edit-mode response, and every request carryingcapa-editorcapa-view,X-Robots-Tag: noindex, nofollow, andresolveEditRequestreturns that value asrobotsTag. - Docs: "Add preview to a live site without changing it", the steps for a
site that keeps its own Capa reads, and why
editMode()andgetCapaClient()make a static page dynamic.
1.0.0-next.7 (2026-09-29)
- 1.0.0-next.7. Browsers: the
/api/and legacy clients called the platformfetchas a method of their config, which a browser refuses ("Failed to execute 'fetch' on 'Window': Illegal invocation"), so every read from a browser failed unlessfetchwas passed in. The default fetch is now called throughglobalThison each request, which also picks up a fetch a framework patches in later. nextis no longer a peer dependency. No range matches every Next canary, so a site onnext@16.3.0-canary.39could notnpm installthe SDK without--legacy-peer-deps.@capacms/sdk/nextjs/overlaystill importsnext, which only a Next site loads.- Docs: the edit mark is dropped wherever an entry is serialized to the
browser (Pages Router props, SvelteKit and Remix loaders, Nuxt payload);
pass the edit flag to
capaAttrsthere. Found testing preview on nine stacks. - Next.js: a GraphQL read that gives neither
tagsnorrevalidate, throughgetCapaClient().graphql(), its builder orgraphql(), is no longer kept in Next's data cache. It is sent asentries.listandentries.getsend theirs, with nocacheand nonext, so a publish shows on the next render whether the page reads by REST or by GraphQL. It used to be kept undercapa:graphqluntil a webhook revalidated it, so a site with no webhook route kept serving the old answer by GraphQL while REST showed the new one. A read that givestagsor arevalidateis kept as before. To keep a read with no tags as it was kept, passrevalidate: false, which keeps it undercapa:graphqluntil the next publish.tags: []now counts as no tags.
1.0.0-next.6 (2026-09-28)
-
1.0.0-next.6. GraphQL, on
@capacms/sdk/nextand@capacms/sdk/nextjs, and two commands,capa-codegen --graphqlandcapa persist. Additive: every call that existed behaves as before.graphqlis an optional peer dependency that only the two commands load. The package's types now need TypeScript 5.0 or later, withstricton or off.Calls and errors.
client.graphql(document, variables?, options?)runs a query against/api/graphqland resolves with{ data, errors, extensions, cacheTags }once the API has run it, even whenerrorsis not empty: a root field that failed isnulland the others keep their data. A request the API refuses as a whole,errorsand nodata, throwsCapaErrorwhatever its status: a 4xx, or the 200 that GraphQL over HTTP sends onapplication/json, thrown withstatus400. 401, 402, 403, 404, 405 and 429 keep their own.CapaErrorcarries every error of the refusal ingraphqlErrors. Each error is aCapaGraphQLError, a plain object:message,locations,pathandextensionsas the spec writes them, withcode,hint,docs,paramandtypelifted out, soResponse.json(result)and a client component's props keep every field.isCapaGraphQLErrorchecks one by its shape. Where GraphQL is switched off the call throws "This Capa deployment does not serve GraphQL." with a hint to read over REST meanwhile.extensions.cost(requestedQueryCost,actualQueryCost, andbudget, whosecountedis what the 5,000-entry limit checks) andextensions.deprecations({ coordinate, reason }for each deprecated member a document used) are typed.Sending. A read is a GET whenever its URL fits the API's 8,192-byte limit, so a production key's read is cached by the CDN and the API with no option set, and a POST when it does not; no document is refused for its length.
method: "POST"always sends a POST. A host that serves GraphQL by POST only (the admin host) answers a GET as a path it does not serve; the read is repeated as a POST, and that host is read by POST for five minutes. A 429 (over the API's read limits: 4 running per project, 3 of them per client, and 64 waiting per client or 256 per key, with/api/entriesreads counted in the same limits) is sent again after itsRetry-Afterplus up to 250 ms, up to 3 times;retries: 0throws it, and a thrown 429 carriesretryAfterin seconds, on REST calls too.createClienttakes the legacy key a site holds for every read,pk_,sk_or unprefixed as older tenants were minted, and warns once per process without printing any of it;preview()and draft reads (draftClient,CAPA_DRAFT_KEY) take acap_key only.Persisted queries.
persisted: truesends the document's sha256 as a cacheable GET (a POST when the variables are too long for a URL), and onPersistedQueryNotFoundone POST with the document. The API stores a document only for a development key; a hash a host declined is remembered for five minutes and sent by POST meanwhile.capa persistregisters a project's documents at build time, with the development key inCAPA_DRAFT_KEY, onCAPA_API_URL(CAPA_ADMIN_URLfor a self-hosted stack whose read host stores nothing), and refuses a production key before it sends anything. Every document is pinned (Capa-Persist: pin), so documents registered in the Explorer or a preview never evict it, and one stored without its pin fails the run. A refusal is reported with its code.--releaseand--envname the build its pins belong to (Capa-Persist: pin; release=<id>; env=<env>), read from Vercel's and Netlify's build variables when not given, so the API keeps the latest production releases' documents before a preview's.Typed documents.
capa-codegen --graphqlchecks the project's.graphqlfiles and its#graphql,/* capa */and gql`` literals against the key's schema, and writes one module: the schema's types,CapaQuery, aTypedDocumentper operation with<Name>Models, the models it reads, beside it, a<Name>Fragmentper fragment andcapaTreeLayout.tagsFor({ namespace: <Name>Models })tags a Next.js read with every model its query reads, relations and filters through them included, so no model is left out by hand, and withcapa:media, whichrevalidateFromWebhookrevalidates when a file in the media library is edited.client.graphql(doc, vars)then infers data and variables with no cast, a literal is typed by its own text, and a document's required variables are required in the call. A literal codegen has not read yet does not compile (RunCapaCodegen). A fragment in a literal of its own, spread with${FRAGMENT}into a query keptas const, works as in Hydrogen. A field, argument or value a laterCapa-Versionphases out is@deprecated, and each use is printed with its file, line and reason.--watch,--check,--save-schemaand--schemaare supported. Both commands readCAPA_API_URLandCAPA_KEY(CAPA_BASE_URLandCAPA_API_KEYstill work) from the shell or the project's.envfiles, asnext devdoes.REST types.
capa-codegenwithout--graphqltakes acap_key: it writes the model interfaces the/v2schema gives a legacy key from the key's GraphQL schema, keyed by namespace, with no tenant id, and from a saved schema with--schema. Every field is optional there, an enum is astringand a field GraphQL leaves out isunknown, since GraphQL does not say more; noCAPA_SCHEMA_CHECKSUMis written. Whichever key reads it, the module now parses for any namespace: a field that is not an identifier is quoted ("am/pm_indicator"?: string;), a model whose PascalCase name is not one is named as GraphQL names it (2024_eventsis_2024Events, its references andSelectandAttrsaliases too), and an enum's values are escaped. A read'sfieldsis typed as the API returns it:entries.list<Articles, typeof select>types exactly the fields the select names, each always there andnullwhen it was never filled, an expanded relation as the entry (withfields) or{ id, model, missing: true }, one it does not expand as{ id, model }, a relation list as{ items, pageInfo }and media as{ id, url, alt, type, width, height }(EntryFields). A select the compiler cannot read, a string or one typedSelect<T>, types every field and each relation as any of the three. A flat read's relations are references,includedholds partial entries, andinflatereturns the tree read's type. BREAKING for code that compiled against the stored shape (fields.author.name,fields.coauthors[0]), which readundefinedor threw at run time. A relation may be named alone in a typed select, as a reference.The typed builder.
client.graphql.query(selection)builds the document from an object and, withcreateClient<CapaQuery>(), types the result from exactly what was selected. A misspelled field, argument, filter field or operator, an argument of the wrong type, a sort value outside the enum and a missing required argument (articlewithoutargs: { id }) do not compile, and the compiler's error names the key and where it was written (SelectionError<"titel is not a field of articles.nodes">).NodeOf<CapaQuery, typeof selection, "articles">is one entry of a read, for a component's props, andQueryResultthe whole of itsdata. An alias reads a field again in the same request:{ latest: { __aliasFor: "articles", args, nodes } }is sent aslatest: articles(...), and typed and checked asarticles. ADateinargsis sent as its ISO text.selectionToDocumentreturns the text a selection sends. It takes nopersisted, and throws aTypeErrorfor it before any request: its document is printed when it runs, socapa persistnever stored it, and it is already a cacheable GET.Paging, tags and descriptions. The builders page back as the API does: REST's
beforewith alimitis sent aslastwithbefore, andbefore=endaslastalone, the end of the list. A model's filter takes_tags, as REST'swheretakes$tags, with the operators the key's schema declares for it.graphqlSchema(), the builders and codegen read a field'sCapa field ...tag from the last line of its description, after the field's label, which is where the API writes it.REST's shape.
toTree(data, selection, layout)turns a builder result into REST'sshape=treedata for the same read, typed from the selection: system keys besidefields, each field under its namespace, media values in REST's public shape and order (id,url,alt,type,width,height), and each entry'smodelfrom the layout. A relation list withoutpageInfoselected is{ items }, and a missing item of one is left out, where REST keeps a{ id, model, missing: true }slot. The layout is thecapaTreeLayoutconstant codegen writes, so a server reads no schema for it, or a schema read withclient.graphqlSchema(). A root alias converts as the field it names; an alias below a root is refused, since REST reads each field once.selectToSelectionwrites a REST read as a builder selection, andgraphqlToSelecttakes a selection back to REST as it takes a tool spec.The tool spec.
buildGraphQLQuery,graphqlToSelectandselectToGraphQLmove between the spec the Explorer and@capacms/mcpshare, GraphQL text and the equal REST request, written exactly as the API writes it inextensions.capa.rest: the filter aswhereJSON, and a system key a field shadows as$tags,$createdAtorauthor.$id. They nest at most 4 relations below the root entry, which is the API's 5 levels of entries, and send each filter value as the type its filter input declares, sohas: "true"on a list of true/false values goes astrue. They check a filter's names throughand,or,notand one hop, refuse a system field GraphQL does not filter (_version) and a hop through a relation the key cannot read without naming the hidden model, and refuse a key a spec does not take (frist) with the keys it does.selectToGraphQLselects what REST returns: all six media fields, and for*or no select the system keys, every field and each relation list as a connection of ids.inflateandSelect<T>read$tags,$createdAtand the other$names as system keys (SystemKey). The schema summary lists the models GraphQL leaves out inrestOnly, and the builder refuses one by naming its REST read, never as an unknown model with another model suggested. A read of one entry pages a relation list from a cursor:afteron the relation's spec,after:in a select,args.afterin a selection. A list read refuses it, as the API does.sorttakes one value or a list, at the root too. A spec written as REST writes a read (author.name,author(name),*, a sort of-publishedAt) throws with what to write instead.Next.js.
graphql()on@capacms/sdk/nextjsreadsCAPA_API_URL,CAPA_KEYandCAPA_API_VERSION(a setting inconfigwins), works out draft and edit mode from{ draftMode, headers }asgetCapaClientdoes, and keeps a published read that answered with noerrorsin Next's data cache, throughunstable_cache, under itstags: until one of them is revalidated, or forrevalidateseconds. A read with notagsis taggedcapa:graphql(GRAPHQL_TAG), whichrevalidateFromWebhookrevalidates on every content and media event;tagsFor({ namespace })tags a read by the models it reads. Drafts are read uncached, by GET. It is not persisted by default.draftClient,getCapaClientandgetPublishedClienttakeCapaQuerylikecreateClient, andgetCapaClientkeeps its GraphQL reads, a document or the typed builder, in Next's data cache the same way, under thetagsandrevalidateeach call gives.The package root.
createClientfrom@capacms/sdk, the legacy/v2/apiclient, refuses acap_key with aTypeErrorthat names@capacms/sdk/next, before it asks for a tenant id or sends anything, where every read failed as a 401 "Invalid API key". ItsCapaErrormessage joins the body with a colon. The README opens with which import is which and links the API reference, andhomepageishttps://docs.capacms.com/api.Edit mode. A GraphQL read in edit mode is marked like a REST read: each object that selected
idandmodelis an entry, andcapaAttrs(node, field)andfieldAttrs(node)tag a field GraphQL renamed by its namespace. To know which fields those are, the client reads the key's type and field names once a minute, beside the page's read rather than after it; a document that never saysmodelreads none.toTreekeeps the mark, andmarkGraphQLEntriesis exported. With next.5'sFieldAttrs<T>:fieldAttrsalso takes a GraphQL node and, for a REST entry typed withModel, still returns exactlyFieldAttrs<Model>, so the<Model>Attrsaliases codegen writes keep working.TaggableField<E>is exported beside it.
1.0.0-next.5 (not published; its changes shipped in next.6)
- 1.0.0-next.5.
<CapaOverlay adminOrigins={[...]} />from the new entry point@capacms/sdk/nextjs/overlay: the live preview overlay as one Next.js client component, refreshing withrouter.refresh()on save (M6).reactandnextare optional peer dependencies, needed only for that entry. TheFieldAttrs<T>type is exported from@capacms/sdk/next, andcapa-codegenwrites a<Model>Attrsalias beside each<Model>Select, soconst a: ArticleAttrs = fieldAttrs(entry)catches a wrong field name at compile time (M5).
1.0.0-next.4 (2026-09-24)
- 1.0.0-next.4. Edit mode. BREAKING for sites that call
capaAttrs(entry, field)with no third argument: it now tags only entries read in edit mode, so a published page ships nodata-capa-attributes. Create the client witheditMode: true(work it out witheditMode({ draftMode, headers })from@capacms/sdk/nextjs) and every entry it reads, related entries included, is marked. Sites that pass a flag keep working unchanged. New in@capacms/sdk/nextjs:editMode,resolveEditRequestfor middleware (checks acapa-edittoken with Capa, strips a forgedx-capa-edit, returns theprivate, no-storeCache-Control to set), and the constantsEDIT_PARAM,EDIT_HEADER,DRAFT_COOKIE,EDIT_CACHE_CONTROL. New on the client: apathoption (config and per call) sent asCapa-PathbesideCapa-Page, so Capa can list the concrete URLs an entry appears on.markEditEntries,isEditEntryandCAPA_EDITare exported from@capacms/sdk/next. Five-minute integration (M6) in@capacms/sdk/nextjs:getCapaClient(keys and edit mode from env),getPublishedClient,createPreviewRoute,exitPreviewRouteandcapaMiddleware(preview links, the Published view, edit mode andno-storein one line). TypedfieldAttrs(entry).titleon@capacms/sdk/next(M5): a wrong field name is a compile error. Flat responses:entries.listandentries.gettakeshape: "flat", which sends?shape=flatand returns every relation as a{ id, model }reference with each expanded entry once inincluded, typed by the select. The result carries the select it sent.inflate(result)turns it back into the tree result, deep-equal to ashape=treeread of the same request; it returns copies and stops where the select stops, so cycles end. New types:FlatPage,FlatSingle,FlatListOptions,FlatGetOptions,Included,ExpandedTargets,ResponseShape,FlatResponse. Additive: a read withoutshapesends the same URL and returns the same result as before.
1.0.0-next.3 (2026-09-23)
- 1.0.0-next.3. Reads made from a layout are no longer charged to
/.routeOfreturned the route a file sits at, soapp/layout.tsxcame out as/and every Site singleton or nav read in a root layout was recorded as a read of the home page, on every page of the site.routeOfnow returns"(layout)"(exported asLAYOUT_PAGEfrom@capacms/sdk/nextand@capacms/sdk/nextjs) for an app-routerlayout.*ortemplate.*, and a read whose page is"(layout)"sends noCapa-Page, even when the client was created with apage. The return type is stillstring, so layout code that already passesrouteOf(import.meta.url)needs no change: upgrading fixes it. No API change: the API already records nothing for a read without the header. In the pages router,pages/layout.tsxis now the page/layoutrather than/.
1.0.0-next.2 (2026-09-23)
- 1.0.0-next.2. The overlay reports
visible { entryId, field }, the tagged element at the centre of the viewport, while the page scrolls: at most every 150ms and only when it changes (the topmost visible element at the very top of a page, the bottommost at the very bottom). The Capa editor's "Follow the page" scrolls the form to match. Additive and still protocolv: 1, so an older admin ignores it and an older overlay simply never sends it.pickCentredandscrollEdge, the pure choice behind it, andvisibleMessageare exported for tests.
1.0.0-next.1 (2026-09-23)
- 1.0.0-next.1. Live preview.
capaAttrs(entry, field, enabled)on@capacms/sdk/nexttags an element with the entry and field it renders, typed sofieldis one of the entry's data keys. The new entry point@capacms/sdk/overlayexportsstartOverlay({ adminOrigins, onRefresh }), which, inside the Capa editor's preview frame, outlines the field being edited, reports clicks on tagged elements back to the editor and re-renders the draft after a save. It does nothing outside a frame and has no dependencies.acceptMessageis exported for tests. See "Live preview" in the README. routeOf(import.meta.url)now decodes the file URL. A realimport.meta.urlpercent-encodes brackets, so a dynamic route such asapp/blog/[slug]/page.tsxcame out as/blog/%5Bslug%5Dand every read with thatpagethrew aTypeError.- The package is published as
@capacms/sdk. Thecapascope on npm was already taken, so the org iscapacms; entry points are@capacms/sdk(legacy/v2client),@capacms/sdk/nextand@capacms/sdk/nextjs. Nothing else about the package changed. Earlier notes below that named@capa/sdkwere written before the first publish and mean this package. - Added
pagetoCapaNextConfigand to every call's options, sending theCapa-Pageheader so Capa can report which of a site's pages read which entries. Telemetry only: it does not change a response, a cache key or anETag. A malformed value throws aTypeError, because the API ignores a header it cannot store and a typo should surface where it is written. - Added
client.pages.list()andclient.pages.get(page)for the page list and one page's detail.list({ entry })narrows to the pages that read one entry and addsentryReadsto each row;getreturnsnullfor an unknown page. - Added
client.preview(token), which verifies a preview token minted by the Capa admin and returns the claim, ornullwhen the token is invalid or expired. Every other failure throws. - Added
schemaChecksumtoCapaNextConfig, sending theCapa-Schemaheader on every call so Capa can tell a site built against the current models from one built against an older set. Telemetry only, likepage: it changes no response, cache key orETag. A malformed value throws aTypeError, because a stamp the API drops in silence looks exactly like a site that is up to date. capa-codegennow writesexport const CAPA_SCHEMA_CHECKSUMbeside the checksum comment it already wrote, so the value can be imported and handed tocreateClient. That constant is the only new byte in the generated file.client.pages.get(page)now carriesinsights, the suggestions Capa draws from that page's own reads (overfetch,fanout,cache,drift), each with the numbers behind it and a copyable rewrite where there is one, and everyqueries[]row carriesselection, the parsed Selection IR of itsselect, orselectionErrorwhen it no longer parses.- Every
queries[]row onclient.pages.get(page)also carriesurl, the request that group most often made, so a caller can print the real call with its filters, sort and limit instead of reconstructing one fromselect. It isnullonce the group has been folded into the daily rollup, which keeps counts rather than requests. The detail itself gainslastReadAt. client.pages.list()now carriesmeta.insights, the tenant-wideunusedrows: entries no page has read in 30 days and nobody has edited in 90.- Added
routeOf(file),preview(token, client)andpagesFor(client)to@capacms/sdk/nextjs.routeOfturns a Next route file into a page string, dropping route groups, parallel slots, leaf file names and extensions, and throws rather than guessing for files Next does not route.
1.0.0-next.0 (2026-09-22)
- Added
@capacms/sdk/next, the dependency-free/api/read client forcap_keys, typed selects, cursor iteration, structured/api/errors, and surrogate cache tags. - Added
@capacms/sdk/nextjshelpers for Next fetch caching, surrogate tag construction, webhook revalidation, and server-only draft client selection. - Kept the legacy
/v2/apiclient as the root export. - Added relation-aware
capa-codegenoutput while preserving byte-identical output for schemas without relations.