Skip to content

createClient

createClient<S, T>(options): object

Create the browser client for one site.

import { createClient } from '@edgekitcloud/client-browser';
import type { EdgeKitDecisions } from './edgekit/generated/decisions';
import type { EdgeKitTraits } from './edgekit/generated/identity';
const ek = createClient<EdgeKitDecisions['site_example'], EdgeKitTraits>({ siteId: 'site_example' });

S is the site’s schema and T the tenant’s traits, both written by edgekit types pull. T is optional and defaults to an untyped record. It is a separate argument because a trait applies to every site of the tenant, while a feature belongs to one site.

The client holds no credential. The tenant accepts its requests from the origins registered on the site, and the session cookie is the only way it identifies a user.

S extends SiteSchema

T extends TraitValues = TraitValues

BrowserClientOptions<S>

newsletters: object

The newsletter lists this site offers, and where the user stands on each.

The user is whoever the session cookie names. With no session, as through an absolute baseUrl that does not carry one, subscriptions() answers unknown for every list. Throws EdgeKitConfigError on 400, 401, 403 or 404; a 404 usually means a list id from an outdated generated newsletters.ts, so run edgekit types pull again. A 429 is failed, so a sign-up form can tell the user to wait.

lists(options?): Promise<CallResult<NewsletterListsResponseBody>>

Every list this site has deployed, without the user’s standing. A disabled list is not deployed, so it never appears and there is no flag to filter on. Before the first deploy the answer is an empty set, the same as a site with no newsletters.

NewsletterOptions = {}

Promise<CallResult<NewsletterListsResponseBody>>

replace(input, options?): Promise<CallResult<NewsletterSubscriptionsResponseBody>>

Sets the user’s lists. A full replacement, not a patch: every list this site deploys that subscribed omits becomes unsubscribed. The response is the full resulting set, so you can see what was turned off.

A list that is pending stays pending whether named or omitted. 200 and 202 both arrive as ok; read the returned statuses to see which lists await confirmation.

With only an email and no session, the call adds and never removes. Omitted lists are left as they are, and the response is what an address never seen before would get: named lists pending or subscribed, the rest unknown. Unsubscribing needs a signed-in user.

SubscriptionsInput

NewsletterOptions = {}

Promise<CallResult<NewsletterSubscriptionsResponseBody>>

subscriptions(options?): Promise<CallResult<NewsletterSubscriptionsResponseBody>>

Where the user stands on every deployed list: the one call a preference centre needs. A list the user never touched answers unknown, so there is nothing to join against lists(). A signed-out user gets unknown for every list rather than a refusal, so the same form renders either way.

NewsletterOptions = {}

Promise<CallResult<NewsletterSubscriptionsResponseBody>>

get id(): object

EdgeKit ID: sign-in, the session, and the user’s own details.

Built on first access, so a page that only calls decide sets up no session store, no cross-tab sync and no session request.

Nothing here throws on an HTTP status: a refusal (a wrong code, a sign-in method this site does not offer) is failed, for the sign-in screen to render.

changeIdentifier: object

confirm: (input) => Promise<CallResult<OkResult>>

Completes the change with the code sent to newEmail. On success every session of the user ends, this one included: send the user back through sign-in.

string

string

Promise<CallResult<OkResult>>

request: (newEmail) => Promise<CallResult<OkResult>>

Starts changing the signed-in user’s email address: sends a code to newEmail. Only at an edgekit_only site. In dual and external_only mode your identity provider owns the address, and this answers failed with a 404.

Answers success even when another user holds the address, so it cannot be used to test which addresses exist; confirm reports the conflict.

string

Promise<CallResult<OkResult>>

config: () => Promise<CallResult<IdConfig>>

The site’s public sign-in settings: the sign-in methods it offers, code and link lifetimes, and registration traits. Sent without cookies and never cached by this client.

Promise<CallResult<IdConfig>>

consumeMagicLink: (token) => Promise<CallResult<SignInResult>>

Redeems a magic-link token, from a page you host.

A token works once. Mail scanners open emailed links, so call this from a step the user takes, such as a button on that page, rather than on page load; otherwise a scanner can use up the token and the user is told a link they never opened has expired.

string

Promise<CallResult<SignInResult>>

exchangeSession: () => Promise<CallResult<SignInResult>>

Exchanges your identity provider’s credential for an EdgeKit session. Only at a dual site; in any other mode it answers failed with a 404.

Promise<CallResult<SignInResult>>

getSession: () => Promise<CallResult<{ session: SessionRef | null; user: IdUser | null; }>>

Reads the session once, without the session store. Signed out is ok with {user: null, session: null}.

Promise<CallResult<{ session: SessionRef | null; user: IdUser | null; }>>

getUserDetails: () => Promise<CallResult<UserDetails<T>>>

The signed-in user’s record and the traits they set, to pre-fill a preferences form. No session payload carries traits; this is the call that reads them.

It does not touch the session store, and it does not replace getSession: for a session whose user no longer exists it answers failed with a 404, where getSession answers signed out.

Values come back as stored. A list item your organization has since disabled is still returned but missing from traits(): render it as unset.

Promise<CallResult<UserDetails<T>>>

signIn: object

emailOtp: (email) => Promise<CallResult<OkResult>>

Emails a one-time sign-in code to email. Redeem it with verifyEmailOtp.

string

Promise<CallResult<OkResult>>

google: (input) => Promise<CallResult<SignInResult>>

Signs in with a Google ID token your page already holds.

This package does not load Google Identity Services. Your page loads it, renders Google’s button, and passes the credential from its callback as idToken. Read the Google client id from the google entry of config().methods (clientId) rather than building it into your page.

There is no redirect flow: this is an ordinary request, answering the same {user, session} as verifyEmailOtp. Pass nonce only when you asked Google for one, with the same value.

string

string

Promise<CallResult<SignInResult>>

magicLink: (input) => Promise<CallResult<OkResult>>

Emails a sign-in link to email. It answers before the user opens the link; the page the link opens calls consumeMagicLink.

string

string

string

Promise<CallResult<OkResult>>

verifyEmailOtp: (input) => Promise<CallResult<SignInResult>>

Signs in with the emailed code, and registers the user on their first sign-in. firstName, lastName and userTraits are the registration fields. This is the only sign-in call that accepts traits; a trait sent through the magic-link flow is dropped without an error.

userTraits is T whole, not a patch: registration is where a required trait is answered, so each required trait is a required key once you pass the object. Omitting userTraits entirely still registers the user, with no traits checked, so your sign-up form must ask the questions.

string

string

string

string

T

Promise<CallResult<SignInResult>>

signOut: () => Promise<CallResult<SignOutResult>>

Signs the user out, in every tab of this browser. At a dual site, logoutUrl is your identity provider’s sign-out page: send the browser there too, or the user stays signed in to your own system.

Promise<CallResult<SignOutResult>>

traits: () => Promise<CallResult<{ traits: IdTraitDescriptor[]; }>>

Every trait the tenant collects: the questions a sign-up form asks and a preferences screen edits, with each list trait’s enabled items and their labels.

Render forms from this, not from config().methods[].registrationTraits. That is [] for magic-link, and methods is empty at a site that offers no EdgeKit sign-in, such as an external_only site, which still collects traits.

A trait appears here as soon as it is saved, before any deploy. Sent without cookies and never cached by this client.

Promise<CallResult<{ traits: IdTraitDescriptor[]; }>>

updateUserDetails: (input) => Promise<CallResult<UserDetails<T>>>

Updates the signed-in user’s own name and traits, and answers with the result, as getUserDetails does. null clears firstName or lastName; name cannot be cleared. Traits merge into the stored ones: see TraitPatch.

UserDetailsInput<T>

Promise<CallResult<UserDetails<T>>>

get session(): SessionState

The current session state, without subscribing. The first read sets up the session store and requests the session, so it starts as pending.

SessionState

subscribe(listener): () => void

Calls listener at once with the current session state, then on every change. Returns a function that unsubscribes.

SessionListener

() => void

get org(): object

User organizations: groups of users sharing seats bought together, with their members and invitations. A user organization is not your EdgeKit organization.

Built on first access, like id, and like id it never throws on an HTTP status: a refusal, including a seat limit, is failed.

Nothing here creates or deletes a user organization. One exists because seats were bought.

acceptInvitation: (invitationId) => Promise<CallResult<OrgMember>>

string

Promise<CallResult<OrgMember>>

cancelInvitation: (invitationId) => Promise<CallResult<OkResult>>

string

Promise<CallResult<OkResult>>

getActiveMember: () => Promise<CallResult<OrgMember>>

Promise<CallResult<OrgMember>>

getInvitation: (id) => Promise<CallResult<OrgInvitation>>

Requires a session. The answer has no user organization name or slug and no inviter email, so an accept screen cannot name the inviter from this call alone.

string

Promise<CallResult<OrgInvitation>>

inviteMember: (input) => Promise<CallResult<OrgInvitation>>

A seat or invitation limit answers failed with code forbidden, the same as a permission refusal. No code tells them apart yet, so do not show an “out of seats” message based on it.

string

string

boolean

"admin" | "member"

Promise<CallResult<OrgInvitation>>

leave: (organizationId) => Promise<CallResult<OkResult>>

string

Promise<CallResult<OkResult>>

list: () => Promise<CallResult<{ organizations: Organization[]; }>>

Promise<CallResult<{ organizations: Organization[]; }>>

listInvitations: (query?) => Promise<CallResult<{ invitations: OrgInvitation[]; }>>

string

Promise<CallResult<{ invitations: OrgInvitation[]; }>>

listMembers: (query?) => Promise<CallResult<{ members: OrgMember[]; total: number; }>>

organizationId defaults to the session’s active user organization, which is unset until setActive is called, so pass it explicitly.

number

number

string

Promise<CallResult<{ members: OrgMember[]; total: number; }>>

listSeatRefusals: (organizationId) => Promise<CallResult<{ refusals: OrgSeatRefusal[]; }>>

Owners and admins only; anyone else gets 404.

string

Promise<CallResult<{ refusals: OrgSeatRefusal[]; }>>

listUserInvitations: () => Promise<CallResult<{ invitations: OrgInvitation[]; }>>

Promise<CallResult<{ invitations: OrgInvitation[]; }>>

rejectInvitation: (invitationId) => Promise<CallResult<OkResult>>

string

Promise<CallResult<OkResult>>

removeMember: (input) => Promise<CallResult<OkResult>>

string

string

Promise<CallResult<OkResult>>

setActive: (organizationId) => Promise<CallResult<{ organization: Organization | null; }>>

Sets the session’s active user organization. organizationId is required; null clears it.

string | null

Promise<CallResult<{ organization: Organization | null; }>>

update: (input) => Promise<CallResult<Organization>>

Renames a user organization or sets its logo. logo: null clears it.

string | null

string

string

Promise<CallResult<Organization>>

updateMemberRole: (input) => Promise<CallResult<OrgMember>>

owner is not assignable, so an admin cannot hand the user organization away.

string

string

"admin" | "member"

Promise<CallResult<OrgMember>>

checkout(input, request?): Promise<CallResult<CheckoutResponseBody>>

Buy an offer, with the offerToken a decision just returned and a card token from Recurly.js on your page. offerToken is an opaque string, so a page can check out without deciding first.

  • ok: the purchase reached the charge. Read data.status: granted, charged or pending. Money may have moved in every case, so never render one as a failure; treat a status you do not know as pending.
  • failed with a code to branch on: a 409 (an expired or unknown offer token, a closed offer, an expired card token, no active billing provider) or a 422 (card declined). After an expired offer token, decide again for a fresh one.
  • failed with code three_d_secure_required: run the Recurly.js 3-D Secure challenge on details.threeDSecureActionTokenId, then call checkout again with the same tokens plus threeDSecureActionResultTokenId.
  • unavailable: a 5xx, a network failure or an abort.
  • Throws EdgeKitConfigError on 400, 401, 403 or 404.

Nothing is retried for you, and timeoutMs does not apply. Your page loads Recurly.js, so its Content Security Policy must allow it: script-src https://js.recurly.com, plus connect-src and frame-src for api.recurly.com, or api.eu.recurly.com when your public key starts with fra-.

CheckoutRequestBody

CheckoutOptions = {}

Promise<CallResult<CheckoutResponseBody>>

createGiftLink(context?, request?): Promise<CallResult<CreateGiftLinkResponseBody>>

Create a gift link for this page, from the signed-in user. context is the page’s facts, as on decide, at the same unverified tier. The link is bound to the value of the fact your gift links are keyed on; when a recipient opens it, the site’s rules still decide whether the page opens. Append the returned token to the share URL as ?_gt=.

A link exists only when token does. A refusal is ok with status: 'denied' and no token; treat any other status without a token the same way. Signed out is failed with a 401; a page whose facts lack the key fact is failed with a 422. Throws EdgeKitConfigError on 400, 403 or 404, including when the deployed site has no gift link feature.

Not idempotent: every call counts against the user’s gift quota and creates a new link.

Record<string, unknown>

GiftLinkOptions = {}

Promise<CallResult<CreateGiftLinkResponseBody>>

decide<F>(features, request?): Promise<DecideResults<S, F>>

Ask which outcome each feature shows on this page. Pass one feature name or an array of up to 32. The result has an entry for every name, with status decided, unknown (the deployed site has no such feature) or unavailable (no answer).

A failed decision never throws, because this runs while the page renders. A timeout, a network failure, a 429 or a 5xx makes every feature unavailable; a 429 carries retryAfter, and nothing is retried for you. It throws EdgeKitConfigError only for a setup mistake: more than 32 features, or a 400, 401, 403 or 404 (for example an unknown site or an unregistered origin).

The user is whoever the session cookie names, or an anonymous device. Two parameters on the page URL are forwarded on every call: ?_gt=, a gift link token, and ?edgekitPreview=, a preview token kept for the tab until ?edgekitPreview=off. A token the tenant cannot verify is ignored.

F extends string

F | readonly F[]

BrowserDecideOptions = {}

Promise<DecideResults<S, F>>

forwardUrl(feature): string

The URL for a journey feature. Use it as an <a href> or with location.assign, never with fetch: the URL answers 302 to the destination, so fetching it fetches the destination page. Builds a string and sends no request. The site id is in the query string, since a navigation sends no custom header.

S["journeys"]

string

grants(request?): Promise<CallResult<GrantsResponseBody>>

What the signed-in user holds: every grant that reaches this site, their own and their user organizations’, newest term first. A purchase through checkout, a comp, a gift and an import all arrive as the same grant shape; label them in your UI however your site does.

Requires a session. Without one it answers failed with code unauthorized, never a throw, including through an absolute baseUrl.

Expired, suspended and revoked grants are included, each with its state; filter on state for the live ones.

Throws EdgeKitConfigError on 400, 403 or 404.

GrantsOptions = {}

Promise<CallResult<GrantsResponseBody>>