Skip to content

Releases and versions

This site describes the latest release of each package and API. It does not keep pages for older versions.

Package Registry
@ebitex/content-sdk npm
@ebitex/forms-sdk npm
@ebitex/content-mcp npm
@ebitex/visitors-sdk npm
@ebitex/data-sdk npm
@ebitex/starter-kit-react npm
Ebitex.Content.Delivery NuGet
Ebitex.Data.Delivery NuGet

Versions are MAJOR.MINOR.PATCH. Every package that has not reached 1.0, and a minor release adds: new exports, new options, and new optional members on the shapes the APIs return. Before 1.0 a minor release can also remove or change something, and each such change is listed under What changed. The patch number increases with every build we publish, so a new patch can carry a fix or a documentation change without any change to the API.

Pin a minor version (^0.20.0 in npm, 0.2.* in NuGet) to take fixes without surprises, and read the changes below before moving to a new minor.

The three APIs are versioned in their paths (/content/delivery/v1, /content/management/v1, /forms/v1/sdk). Within a version, changes only add: new endpoints, new optional parameters, and new members in responses. Your client should ignore response members it does not recognise.

Changes are recorded on this page from the site’s launch onward.

2026-10-29 — The starter kit and its renderer move to 1.0: presentations, with placement in Template settings

Section titled “2026-10-29 — The starter kit and its renderer move to 1.0: presentations, with placement in Template settings”

Starter kit 1.0.0 and @ebitex/starter-kit-react 1.0 are a breaking release.

  • The model. An experience’s hero and sections are presentation fields (a Template, its settings and a Component) instead of component fields. Every placement and appearance choice (width, background, appearance, anchor, span, layout, aspect, emphasis, tone, columns, the theme, and an accordion’s behaviour) moved from a contextual field on a Contract to the settings of that Contract’s Template, so the content Contracts hold only editorial content. Tabs and Accordion hold their own items (a label and a list of presentations). Every field id is now kebab-case (primary-action, meta-title).
  • The renderer. ebitexRenderers() returns a renderer for every kit Template, and you replace one or add a Template of your own by merging into <ContentProvider renderers>: renderers={{ ...ebitexRenderers(), 'ebitex-card-template': MyCard }}. A renderer is chosen by Template, never by Contract. A custom renderer receives { template, settings, component } (the Contract’s fields at component.content) in place of { content }. EbitexProvider keeps only link: its sections option and EbitexSectionProps are removed. Draw inside Frame to get a Template’s frame and to join the appearance cascade.
  • Installing. A kit install into an environment that already holds an earlier version is refused, with nothing written, because a new major version is never mixed into an older one. Your existing copy keeps working with the 0.x renderer. To use 1.0, install into a new environment or organization.

If you copied the source with starter-kit-eject, nothing changes for you: the copy is yours.

2026-10-28 — The starter kit renderer’s names now start with Ebitex

Section titled “2026-10-28 — The starter kit renderer’s names now start with Ebitex”

@ebitex/starter-kit-react 0.2 renames what it exports and the attributes it writes, so they carry the ebitex prefix the starter kit’s own Contracts already use. The package and the starter-kit-eject command keep their names. Upgrading means changing the names below and nothing else.

Before After
KitProvider EbitexProvider
KitMarkdown EbitexMarkdown
kitRenderers() ebitexRenderers()
KitOptions EbitexOptions
KitSectionProps EbitexSectionProps
KitExperience EbitexExperience
KitLink, KitContext, useKit() EbitexLink, EbitexContext, useEbitex()
--kit-* tokens --ebitex-*
data-kit-theme, data-kit-appearance, data-kit-unknown, data-kit-placeholder data-ebitex-theme, data-ebitex-appearance, data-ebitex-unknown, data-ebitex-placeholder

If you copied the source with starter-kit-eject, nothing changes for you: the copy is yours. If you redefine a token, or select on a data-kit-* attribute in your own CSS or tests, rename those too.

2026-10-27 — A React renderer for the starter kit

Section titled “2026-10-27 — A React renderer for the starter kit”

@ebitex/starter-kit-react draws the starter kit’s sections, an experience’s theme, a section’s appearance and Assets media with Tailwind tokens, for use with @ebitex/content-sdk. kitRenderers() gives <ContentProvider> the kit’s one Template, <KitProvider sections={{ ... }}> replaces how one Contract is drawn (or draws a Contract of your own), link replaces how links navigate, and the --kit-* tokens in styles.css carry every colour. The package root has the plain functions that read a delivered category (keyOf, pick) with no React. To take the source instead, npx starter-kit-eject <directory> copies the components, tokens and vocabulary into your project without overwriting a file. See Start a client site from the starter kit.

2026-10-25 — Sandbox organizations are marked in every response

Section titled “2026-10-25 — Sandbox organizations are marked in every response”

An agency can build a client’s site in a sandbox organization before the client is a customer. Responses from the Delivery, Forms and Data APIs of a sandbox now carry X-Ebitex-Sandbox (development or cutover) and X-Robots-Tag: noindex; neither is sent for an ordinary organization or a sandbox that has gone live. @ebitex/content-sdk gains client.sandbox, an onSandbox option, readSandboxMode, sandboxRobots and a sandbox option on buildSitemapDocuments; @ebitex/forms-sdk gains client.sandbox and onSandbox; Ebitex.Content.Delivery reads the header into Sandbox. All of it is additive and absent for an organization that is not a sandbox. See Build against a sandbox.

2026-10-23 — Attribute a person to the campaign that brought them

Section titled “2026-10-23 — Attribute a person to the campaign that brought them”

POST /audience/management/v1/identify accepts an optional attribution ({ source, medium, campaign }, the utm_source, utm_medium and utm_campaign of the link the person followed, each at most 100 characters). Audience records it once, as the person’s first touch: a person with no Awareness pass gets one with those facts, an open pass with none is filled in, and anyone else is left as they are. The campaign text is matched to an existing campaign by alias and never registers one. The answer gains attribution: { status, campaignResolved }. Awareness gains a source fact, and the Attribution report groups people by source, medium and campaign.

@ebitex/visitors-sdk’s connectEmbeds takes an attribution option (a value or a function, defaulting to the page’s own utm_* tags) and hands it to the form frame with the token. @ebitex/forms-sdk’s <EbitexForm> takes the same attribution prop and sends it with the submission. Neither SDK stores anything. The public form submission accepts an optional attribution with the same members.

Section titled “2026-10-23 — The browser that follows a verification link is verified too”

When a verification link has a returnUrl, pressing the button on its page now also hands a one-time code to the page the person continues to. The “Continue” link carries it in the URL fragment, #ebitex-verify=<code>; it works once and expires after 10 minutes. @ebitex/content-sdk reads it, removes it from the address bar and calls the new POST /audience/delivery/v1/sign-in/handoff with the code and the browser’s own visitor token, so a link followed on a phone and continued from there unlocks the page on the phone. The route answers 202 with the same body whatever became of the code. The client gains audience.redeemHandoff(code) and an audience: { handoff: false } option, and the audience client gains redeemHandoff().

Section titled “2026-10-22 — A page unlocks when the emailed link is opened on another device”

@ebitex/content-sdk now looks again at a visitor’s audiences every few seconds while a sign-in link is pending, for up to 15 minutes and only while the tab is visible. A link opened on a phone therefore unlocks the page open on the desktop without a reload; before, only a link followed in the same browser did. useSignIn() does this by itself. It adds audience.watch() and the React hook useAudienceWatch(active) for a wait of your own, such as a link your own email sent. The page the emailed link opens now says the address is verified, rather than that a device is signed in.

2026-10-21 — One signed token for a visitor’s audiences

Section titled “2026-10-21 — One signed token for a visitor’s audiences”

Which audiences a visitor is in now reaches Content as one signed token, and the two earlier ways are removed. POST /audience/delivery/v1/resolve returns an access member for every visitor: a token (a1.…), its expiry and the audiences it grants, each tagged verified, declared or context. Send it in the X-Ebitex-Audiences header on GET /path, GET /nodes, GET /components, GET /components/{provider}/{key} and GET /streams/{externalId}; Content verifies it, and a response that depends on it is private, no-store. resolve also takes an optional audiences list (up to 64; more is 400 too_many_audiences), and GET /content/delivery/v1/audiences lists the audiences your published content uses.

Removed: audiences in ctx (a request that sends it answers 400 audiences_not_in_ctx), POST /audience/delivery/v1/claim and the X-Ebitex-Membership header. A variant on a stored-membership audience is now withheld from a visitor who does not hold the audience, even when your site chooses variants itself, and is left out of search; a variant on an audience worked out from context is public copy.

@ebitex/content-sdk does the whole flow when you pass audience: { apiKey } to createContentClient: it asks which audiences your content uses, asks Audience about exactly those, keeps the token in memory, renews it, sends it, and refreshes when a Forms submission, a followed sign-in link or a stale token says the audiences changed. It gains forVisitor() for a server request, onChange, refresh(), useAudienceChange() and useSignIn(), and loses withAudiences, audience.claim(), ContextBag.audiences and the membership option. @ebitex/forms-sdk announces a completed submission to the page. Ebitex.Content.Delivery gains AudienceClient.GetAccessAsync and AudienceToken on PathOptions, NodesOptions, ComponentOptions and StreamQueryOptions, and loses ClaimAsync, ContentContext.Audiences and Membership. See Serve pages only to members and Personalize content.

2026-10-19 — Clients for the Data Delivery API

Section titled “2026-10-19 — Clients for the Data Delivery API”

Two new packages read Data from your code. @ebitex/data-sdk works in a browser with a browser-safe key or on a server, with no runtime dependencies: it lists the models a key can read, then lists, filters, sorts, counts and pages records, follows references, and reads records by key. Ebitex.Data.Delivery does the same from a .NET server. Both send a cursor back with exactly the query that produced it, report an error with the API’s code and request id, and refuse early what the API would refuse. See Read Data records from JavaScript and Read Data records from a .NET server.

2026-10-19 — Interactions and events are two things, and page views can be counted

Section titled “2026-10-19 — Interactions and events are two things, and page views can be counted”

Audience now keeps two kinds of record about what a visitor does. An interaction is a meaningful fact that appears on a person’s timeline. An event is a high-volume signal, such as a page viewed, that is kept briefly and counted by audiences.

This renames the call that recorded an interaction. POST /audience/delivery/v1/events is now POST /audience/delivery/v1/interaction, the eventId member is interactionId, the daily-limit code event_cap_reached is interaction_cap_reached, and in @ebitex/visitors-sdk track() is interaction() with { interactionId } in its options. The old route and method are removed. Behaviour is otherwise unchanged.

POST /audience/delivery/v1/event records an event for a recognised, linked person: today content.viewed, with the page’s category ids and their ancestors. It takes one event or up to 20, and never slows your page. The SDK has event() and pageViewed(), and the drop-in script can record a view on load when you add data-ebitex-page-views. An audience can then count “viewed a page in this category at least N times in the last D days”. Each page the Content Delivery API resolves now carries a classification (the ids of its categories with their ancestors) when it has any; @ebitex/content-sdk and Ebitex.Content.Delivery carry it through. See Interactions and events.

2026-10-18 — Forms can share a page’s visitor

Section titled “2026-10-18 — Forms can share a page’s visitor”

@ebitex/forms-sdk gains sharedVisitorSession, which turns the visitor object a page already has (the one createVisitors returns in @ebitex/visitors-sdk) into a session for <EbitexForm visitor={…}>. The form then sends the page’s token and consent signals, hands what its responses say back to the page’s visitor, and forgets the device everywhere. A form without one keeps its own session, exactly as before, and @ebitex/forms-sdk still has no dependency on the other package. @ebitex/visitors-sdk gains consentSignals() and adopt() for this. See Share one visitor with the rest of your site.

2026-10-18 — Record what a recognised visitor did

Section titled “2026-10-18 — Record what a recognised visitor did”

POST /audience/delivery/v1/events puts a named event on the timeline of the person a device is recognised as, for example pricing-viewed. It is not a page-view tracker: an event is stored only when the device holds a recognition token that has not been forgotten, consent is granted on that device, and the device is already linked to a person. For anyone else the answer is 202 with recorded: false and the reason, and nothing is stored. Properties may hold only visit properties your organization has declared (Settings, Visit properties in Audience), typed as declared; each device may record 200 events in any 24 hours; an eventId makes a retry safe. @ebitex/visitors-sdk gains track(), and VisitorsApiError gains an errors member naming each rejected property. See Record what a visitor did.

Section titled “2026-10-18 — Recognise visitors with their consent, without forms”

A site that has no forms can now recognise a returning visitor with their consent, and let them forget their device. POST /audience/delivery/v1/visitors/session answers with the token to use and the consent state (your consent tool, Global Privacy Control, Do Not Track, then the organization’s default decide); POST /audience/delivery/v1/visitors/forget ends a device’s token and what it unlocked, and always answers the same whatever the token. Both take an Audience delivery key, restricted to your site’s origins when used from a browser. The new package @ebitex/visitors-sdk wraps them: it keeps the token in a first-party cookie only while recognition is granted, ships a script for pages with no build step, and has a React layer. See Recognise visitors with their consent.

2026-10-17 — Restrict pages to an audience

Section titled “2026-10-17 — Restrict pages to an audience”

Editors can restrict a site, section or page to an audience, and the Content Delivery API enforces it. POST /audience/delivery/v1/claim returns a signed, short-lived membership claim for a visitor; send it in the new X-Ebitex-Membership header on GET /path and GET /nodes. A restricted page requested without a claim that admits the visitor answers 403 members_only with the door to offer and the unlock page to show, and is left out of sitemaps, include=paths listings and /nodes. Every response that depends on a claim is private, no-store. POST /audience/delivery/v1/sign-in emails a one-time sign-in link to a member of a verified-access audience, with the same answer for every address. @ebitex/content-sdk gains audience.claim(), audience.startSignIn(), a membership option on resolveLocation, getNavigation and resolvePath, and membersOnlyOf(error); Ebitex.Content.Delivery gains AudienceClient.ClaimAsync and StartSignInAsync, Membership on PathOptions and NodesOptions, and ContentDeliveryException.MembersOnly. See Serve pages only to members.

2026-10-15 — Audience Management API: a plan limit on people

Section titled “2026-10-15 — Audience Management API: a plan limit on people”

POST /audience/management/v1/individuals and POST /identify can now answer 403 quota_exceeded when the organization has reached its plan’s limit on people and the call would create a new person. Nothing already held changes. See Plan limits.

2026-10-13 — Render an email from your own site, or let ebitex render it

Section titled “2026-10-13 — Render an email from your own site, or let ebitex render it”

Audience can send an email whose content is a page in one of your Content sites. Choose the hosted renderer on the channel and ebitex renders the page with the channel’s delivery key, or choose your own renderer and serve the signed POST /render/v1 request, answering with a subject, HTML and text. @ebitex/content-sdk gains a render entry point for building a renderer. Audience never sends a renderer anything about a recipient. See Render an email from your own site.

Audiences are now defined in the Audience app and resolved by your site. POST /audience/delivery/v1/resolve answers which audiences a visitor is in, with ids, using an Audience delivery key; /audience/management/v1/audiences defines them from a script. Send the ids as ctx.audiences: a variant that names an audience is shown when its id is in that list. The Content Management API’s audiences routes, the audience kind in promotion and export, and the audiences section of a content model are gone, and an unresolved (client-resolve) response now carries each audience condition as { "audience": "<id>" } instead of the audience’s rule. @ebitex/content-sdk gains createAudienceClient and withAudiences; Ebitex.Content.Delivery gains AudienceClient. See Audiences.

2026-10-12 — Audience Management API: read and write the people your organization knows

Section titled “2026-10-12 — Audience Management API: read and write the people your organization knows”

A new API, at /audience/management/v1, lets your own server work with the individuals in Audience: create and update people, add and remove identities, record channel permissions and interactions, forget a person, and read the organization’s custom fields. POST /identify is the one call for the moment you learn who a visitor is: it finds or creates the person, merges on a verified identity, records an event, and, with the device’s consent signals forwarded, links the visitor’s browser. It takes an Audience management key created under Settings, API keys in the Audience app, with up to three scopes: audience.management.read, audience.management.write and audience.management.erase. See Knowing your audience.

Section titled “2026-10-12 — Forms: details held by a connected CRM are shown only after the visitor confirms an emailed link”

On a form’s public page, a returning visitor’s details from a connected CRM are no longer filled in just because their browser is recognized. The form offers to email a one-time link to the address; confirming it fills the details in and is remembered on that browser for a period the form sets (30 days by default). Until then the page prefills only what the visitor themselves submitted to that form. The form can still be submitted at any time: the offer never blocks it. The render response carries a new verification member for this, and an organization can turn the requirement off per form. @ebitex/forms-sdk is unchanged, since it never returned prefilled values.

Section titled “2026-10-12 — Forms: visitors are recognized only with consent; @ebitex/forms-sdk stops storing an id”

The visitor on a public form is now identified by an opaque token the API issues, not a UUID you generate, and recognizing a respondent across visits needs their consent. The submit and render requests take an optional consent member (cmp, gpc, dnt, cmpString); the responses carry visitor and consent (including the wording to show). A new forget operation lets a respondent clear their browser. Unsigned UUIDs are still accepted until 2027-01-12.

In @ebitex/forms-sdk, the token is held in memory and never written to localStorage, sessionStorage, a cookie or IndexedDB by the package. Pass a storage adapter to keep it between visits; it is called to save only while recognition is granted. getOrCreateVisitorId, which wrote to localStorage, is removed from @ebitex/forms-sdk/react. See Consent and returning visitors.

2026-09-22 — Content Management API: a stale pin is named; content_find pages searches

Section titled “2026-09-22 — Content Management API: a stale pin is named; content_find pages searches”

A PUT /content/management/v1/components/{id} whose document fails validation against the component’s pinned contract version, when that pin is behind the contract’s latest, now says so. The 400 validation_failed keeps its errors and adds pinnedContractVersion, latestContractVersion, validatesAgainstLatest and a message naming repinToLatest: true. A failure against a pin that is already the latest is unchanged.

The offset-paged listings (GET /components, /contracts, /templates) now document that they are not stable while the environment is being written to, and that GET /items is the way to enumerate one. Their behaviour has not changed.

@ebitex/content-mcp: content_find with search or externalId now returns a nextCursor when more results exist. Previously it always returned null, so an agent never saw past the first page of a search.

2026-09-22 — Content Management API: bundles and /items apply the key’s role

Section titled “2026-09-22 — Content Management API: bundles and /items apply the key’s role”

A management key acts as its role, and POST /import, POST /export and GET /items now enforce that as the per-item endpoints always have. Importing or exporting a contract, template, adapter, stream, audience or category needs the role’s manage permission for that kind (403 permission_denied). A component or page needs view to be exported and edit to be imported, both where it is and wherever the bundle files it (403 access_denied or 404). An import checks only the items it selects. GET /items leaves out components, folders and pages the role cannot view.

This can refuse a request that used to succeed. A key bound to a role without these permissions could import and export anything in its environment. If an import is now refused, mint the key against a role that holds the permission the refusal names.

2026-09-21 — Content Management API: imports respect plan limits

Section titled “2026-09-21 — Content Management API: imports respect plan limits”

POST /content/management/v1/import now refuses an import that would take the organization past its plan’s limit on components, pages or categories. Nothing is written, and the job fails with error: "quota_exceeded" and a quotas list naming each exceeded limit with its limit, current count and how many the import was adding. Previously such an import succeeded, and the organization found out later when creating anything by hand was refused.

Only the items an import creates count, so re-applying a model that updates existing content is never refused, even over a limit. POST /import-plan warns ahead of time: warnings gains one line starting quota: per limit the plan’s new items would pass. Nothing else about either endpoint has changed.

2026-09-16 — Content Management API: importing a bundle is now a job

Section titled “2026-09-16 — Content Management API: importing a bundle is now a job”

Breaking. POST /content/management/v1/import answers 202 { id, status } instead of 200 with the per-item results. The import runs as a background job: poll GET /operations/{id} until status is succeeded or failed, and read the same per-item outcomes from the finished run’s result.

The reason is a ceiling rather than a preference. A blocking import was capped by the timeout of whatever proxy sits in front of the API, however fast the server was — a 110-item bundle already failed at roughly 125 seconds with no HTTP status at all, and a full site’s content model is larger than that. Publishing has worked this way since this API launched, so the polling shape is not a new concept, and GET /operations/{id} is the endpoint you already use for it.

What has not changed: every refusal you can act on still comes back immediately, from the same call, with the same status and body — a blocked plan, an unconfirmed overwrite, an omitted new item, a malformed bundle. What has changed is that a bundle carrying an invalid document fails the job rather than the request: the run’s error carries the same list naming every invalid item and its failing fields. Nothing is written either way, since an import is one transaction.

Also in this release, GET /operations/{id} returns error as a JSON object, matching result beside it. It was previously the raw stored text, so it arrived as JSON encoded inside a JSON string.

@ebitex/content-mcp is updated for this in 0.4.97: content_transfer with apply returns the operation to poll rather than a finished result, and says so in its description. If you read its result, update before upgrading — the key is now operation, and the import is not finished when the tool returns.