Skip to content

Sessions and sign-out

A session is one signed-in browser or app. A web page carries it as a cookie; a native app carries it as a token. Both are the same server-side record, and ending it ends both.

Every response that signs a user in answers 200 with the user and the session:

{
"user": {
"id": "01926f3a-8b4c-7d2e-9f10-2a3b4c5d6e7f",
"email": "ada@example.com",
"emailVerified": true,
"name": "Ada Lovelace",
"firstName": "Ada",
"lastName": "Lovelace",
"image": null
},
"session": {
"id": "01926f3a-8b4d-7a11-8c22-3d4e5f6a7b8c",
"kind": "edgekit",
"expiresAt": "2026-08-30T09:00:00.000Z"
}
}

The body never carries a token. The response sets:

  • The session cookie, on the host the request reached, or on your organization’s cookie domain when EdgeKit has set one. It is HttpOnly, so page scripts cannot read it, SameSite=Lax, and Secure over HTTPS.
  • An access cookie for this site, which decide reads to know who the user is and what they hold.

Every EdgeKit cookie name starts with ek.. Over HTTPS, the session cookies’ names also carry __Secure- in front.

GET /v1/id/get-session answers who is signed in. With no session it answers 200 with { "user": null, "session": null }, never 401. It is never cached.

const response = await fetch('/edgekit/v1/id/get-session', {
credentials: 'include',
headers: { 'x-edgekit-site-id': 'site_example' },
});
const { user, session } = await response.json();
if (user === null) {
// signed out
}

session.kind is edgekit for a session EdgeKit holds. At an external_only site it is external and id is null, because the user was read from your login’s credential and EdgeKit holds no session; see Choose who signs users in. Treat an unknown kind as signed in.

Reading the session also refreshes the site’s access cookie, so a grant the user just received reaches decide on the next page view.

In the browser client, id.session is the current state and id.subscribe calls you on every change. The state is pending, anonymous, authenticated or unavailable; see SessionState. The client reads the session the first time you touch id.session, not on every window focus, and a sign-out in one tab reaches the others.

A session lasts 30 days from when it was last extended. Reading it with GET /v1/id/get-session extends it to 30 days from that read, at most once a day. A user who comes back at least once a month stays signed in.

POST /v1/id/sign-out ends the session the request carries. It deletes the server-side record, clears the session cookie and every site’s access cookie the request carries, and answers 200 even when no session was sent, so a retry is safe.

Terminal window
curl -X POST https://your-tenant-host/v1/id/sign-out \
-H 'x-edgekit-site-id: site_example' \
-H 'Authorization: Bearer <session-token>'
{ "success": true, "logoutUrl": null }

logoutUrl is null except at a dual site, where it is your own login’s sign-out page. Send the browser there too: EdgeKit’s sign-out does not sign the user out of your login. See Move users off your existing login.

Sign-out ends one session. The user’s other browsers and apps stay signed in. Every session a user holds ends when they change their email address; see Let users manage their account.

How quickly an ended session stops working

Section titled “How quickly an ended session stops working”

Routes that change a user’s data check the session on every request, so an ended session is refused there at once: /v1/id/user-details, /v1/id/change-identifier/* and /v1/org/*. Elsewhere it lingers in the browser that held it:

  • GET /v1/id/get-session can keep answering with the user for up to 15 minutes.
  • Decide reads the user from the site’s access cookie, and keeps doing so for up to an hour after that cookie was last refreshed.

Sign-out clears both cookies in the browser that signs out, so neither delay applies there.

A native app has no cookie jar, so it carries the session as a token:

  1. Sign the user in as a page would, but send no Origin header. A request with no Origin is refused when its Sec-Fetch-Site header is anything but same-origin, so a native client sends neither header.
  2. Read the session token from the set-auth-token response header, and keep it in the platform’s secure storage.
  3. Send it as Authorization: Bearer <session-token> on later requests, together with x-edgekit-site-id.
Terminal window
curl -i -X POST https://your-tenant-host/v1/id/sign-in/email-otp \
-H 'x-edgekit-site-id: site_example' \
-H 'content-type: application/json' \
-d '{"email": "ada@example.com", "otp": "482913"}'
# read the set-auth-token response header
curl https://your-tenant-host/v1/id/get-session \
-H 'x-edgekit-site-id: site_example' \
-H 'Authorization: Bearer <session-token>'

Cross-origin JavaScript cannot read set-auth-token or send Authorization to EdgeKit. A page calling /edgekit/ on its own domain is same-origin, so it can read the header when your forwarder passes it through. Web pages use the cookie, and the browser client never reads the token.

A user session is the only credential on these routes

Section titled “A user session is the only credential on these routes”

The routes that act on the signed-in user accept a user session and nothing else. A request that carries an API key in x-api-key is refused with 401 unauthorized, This route requires a user session., and a request with no session gets 401 unauthorized, No user session.. An API key is for your servers’ routes, such as /v1/id/upstream/*.

A session works on every site its cookie reaches. By default the cookie belongs to the one host the user signed in on. When EdgeKit sets your organization’s cookie domain, such as example.com, the cookie reaches every host under it, so one sign-in covers www.example.com and shop.example.com. A deploy is refused when the cookie domain does not cover every active site.

A site the cookie does not reach, such as one on example.net, gets its own sign-in, and the same address signs in to the same user. See Call the tenant host directly.