# EntryBit Developer Documentation > The complete EntryBit API & OAuth documentation as a single document, for LLM ingestion. Canonical HTML lives under https://entrybit.net/docs/. Each page is also available as raw Markdown by appending `.md` to its URL. OpenAPI 3.1: https://entrybit.net/openapi.json # Get started ## Introduction Source: https://entrybit.net/docs/get-started/introduction/ The EntryBit API lets your app sign users in with EntryBit, issue and manage guest passes, and integrate your backend — over OAuth 2.0 / OpenID Connect and a Bearer-token REST API. The **EntryBit API** lets you build on the same platform that powers EntryBit's own apps: sign users in with EntryBit, read and create guest passes, and connect your backend or SDK. Everything is standard HTTPS + JSON, and everything self-describes at the OpenID Connect discovery document — so most OAuth libraries configure themselves from a single URL. - **Base URL:** `https://entrybit.net` - **Discovery:** `GET /.well-known/openid-configuration` - **Format:** JSON over HTTPS (TLS required); `Cache-Control: no-store` on every credential-bearing response. ## Three ways to authenticate Pick the model that matches who is acting: 1. **Sign in with EntryBit (OAuth 2.0 / OpenID Connect)** — for apps acting on behalf of a **signed-in user** (e.g. a resident mobile app). Authorization-code flow with **PKCE required**; you receive a short-lived access token (RS256 JWT, ~15 min) and a rotating refresh token. Call `/api/v1/*` with the access token. 2. **Organization API keys** — for **servers and SDKs** acting as the organization. Keys look like `eb_sk_…`, are created by an org admin in **Settings → API keys**, and are shown once. Send as `Authorization: Bearer eb_sk_…`. Call `/api/v1/org/*`. 3. **Discovery & JWKS** — public metadata (`/.well-known/openid-configuration`) and the signing keys (`/.well-known/jwks.json`) that let any resource server verify a token offline. See [Authentication](/docs/get-started/authentication/) for a side-by-side of when to use which. ## Where to start - **Building a mobile or web app that signs users in?** Start with the [Quickstart: Sign in with EntryBit](/docs/get-started/quickstart-react-native/), then the [OAuth 2.0 & OpenID Connect](/docs/oauth/overview/) reference. - **Building a server integration or SDK?** Read [API keys](/docs/api-keys/overview/) and the [Organization API](/docs/api-reference/organization/). - **Just want the endpoints?** Jump to the [API reference conventions](/docs/api-reference/conventions/). ## Conventions - **Errors** — protocol endpoints return RFC 6749 / 6750 error objects (`{ "error": … }` with a `WWW-Authenticate` challenge on `401`/`403`). Business endpoints return `{ "success": false, "error": … }`; quota exhaustion is `402`. Branch on the error **code**, never the prose — descriptions are deliberately generic. - **Rate limits** — per-IP and per-credential buckets; exceeding one returns `429`. Back off and retry. - **Security** — never place credentials in a URL; all endpoints require TLS. Refresh tokens **rotate** on every use — always persist the newest one. ## Machine-readable specs The entire surface is published as **OpenAPI 3.1** and as LLM-friendly text: - **OpenAPI:** [`/docs/api-reference/openapi/`](/docs/api-reference/openapi/) (download / view — point Swagger UI, Redoc, or an SDK generator at it). - **For LLMs:** [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt). Every doc page is also available as raw Markdown — append `.md` to its URL. ## Authentication Source: https://entrybit.net/docs/get-started/authentication/ OAuth 2.0 signs in users; API keys authenticate servers. Which credential to use, what each looks like, and the one rule that keeps your secret key safe. EntryBit has two ways to prove who is calling — and they are not interchangeable. Pick by asking **who is acting**: a *person* using your app, or a *server* acting as the whole organization. - **A signed-in user** → **OAuth 2.0 / OpenID Connect**. Your app never sees the password; it receives a short-lived access token on the user's behalf and calls [`/api/v1/*`](/docs/api-reference/passes/). - **A backend or SDK** → an **organization API key**. A long-lived secret that acts as the organization and calls [`/api/v1/org/*`](/docs/api-reference/organization/). ## The two credentials at a glance | | OAuth client | API key | |---|---|---| | Acts as | A **signed-in user** | The **organization** | | Looks like | `eb_9f1c2ab34cd56ef7` (`client_id`) | `eb_sk_…` (secret) | | Secret? | **No** — the `client_id` is public | **Yes** — a 256-bit secret | | Proof of identity | The user's login + **PKCE** | The key itself | | Where it lives | Shipped in the app / SPA | On your server, in a vault | | Created in | **Settings → Team → OAuth apps** | **Settings → API keys** | | Token you send | `Authorization: Bearer ` (a JWT) | `Authorization: Bearer eb_sk_…` | | Calls | `/api/oauth/*`, `/api/v1/*` | `/api/v1/org/*` | | Scopes | User-consent scopes (`passes:read`, …) | `org:*` scopes | ## The credential shapes Both identifiers start with `eb_`, and the difference is the one thing worth memorising: - **`eb_9f1c2ab34cd56ef7`** — an OAuth **`client_id`**. It has no `sk`. It is **public**: it ships inside your mobile app or SPA, and that is fine because a `client_id` alone can do nothing. The security comes from the user logging in plus **PKCE** (a per-request secret the app proves it holds). - **`eb_sk_…`** — an organization **API key**. The `sk` means *secret key*. It is 256 bits of CSPRNG output, it acts as the whole organization, and it is shown to you exactly **once**. ### The golden rule > **Never ship an `eb_sk_…` key inside an app, APK, SPA bundle, or any client anyone can download.** A secret key can be extracted from a shipped binary in minutes, and it grants organization-wide access. Client apps use an OAuth **`client_id`** (public, PKCE-protected); secret keys live only on servers you control. If a secret key was ever committed, embedded, or shared, **[revoke it](/docs/api-keys/create-and-manage/)** and switch the app to a `client_id`. ## Decision table | You are building… | Use | Start here | |---|---|---| | A mobile app that signs users in | OAuth 2.0 (public client + PKCE) | [Quickstart: Sign in with EntryBit](/docs/get-started/quickstart-react-native/) | | A single-page web app (SPA) | OAuth 2.0 (public client + PKCE) | [OAuth 2.0 & OpenID Connect](/docs/oauth/overview/) | | A web app with a backend that can keep a secret | OAuth 2.0 (confidential client) | [Register an app](/docs/oauth/register-an-app/) | | A server integration that acts for the whole org | API key | [API keys](/docs/api-keys/overview/) | | A cron job / data sync / internal tool | API key | [API keys](/docs/api-keys/overview/) | | A resource server that only verifies tokens | Discovery + JWKS | [Discovery & JWKS](/docs/oauth/discovery-jwks/) | ## What each model gets you **OAuth 2.0 / OpenID Connect** — the authorization-code flow with **PKCE (S256), required for every client**. The user signs in through the system browser (never a WebView), your app exchanges the resulting code for tokens, and you receive: - an **access token** — an RS256 JWT, ~15 minutes, sent as `Authorization: Bearer …`; - a **refresh token** — opaque, 60-day, **rotating** (when you request `offline_access`); - an **id_token** — who signed in and *how* (when you request `openid`). See [OAuth 2.0 & OpenID Connect](/docs/oauth/overview/) for the full flow, and [Scopes](/docs/oauth/scopes/) for what you can ask for. **Organization API keys** — a single secret that authenticates every request. An org admin mints it in **Settings → API keys** with least-privilege `org:*` scopes, an optional expiry, and an optional source-IP allowlist. Send it on every call and you act as the organization. See [API keys](/docs/api-keys/overview/). ## Next steps - New to the platform? Read the [Introduction](/docs/get-started/introduction/). - Signing users in? [Quickstart: Sign in with EntryBit](/docs/get-started/quickstart-react-native/). - Building server-side? [API keys](/docs/api-keys/overview/) then the [Organization API](/docs/api-reference/organization/). ## Quickstart: Sign in with EntryBit Source: https://entrybit.net/docs/get-started/quickstart-react-native/ Add Sign in with EntryBit to a React Native (Expo) app in minutes — register a public client, run the PKCE flow with expo-auth-session, exchange the code, and call the API. This is the short path to **"Sign in with EntryBit"** in a React Native app and calling the API on the user's behalf. It uses `expo-auth-session`, the authorization-code flow, and **PKCE (S256)** — the standard, secure pattern for native apps ([RFC 8252](https://www.rfc-editor.org/rfc/rfc8252)). - **Base URL:** `https://entrybit.net` (or your staging origin) - **Auth model:** authorization-code + PKCE via the **system browser** — never a WebView, never an in-app password screen. - **Everything self-describes:** `GET /.well-known/openid-configuration` For the full walkthrough with raw HTTP and the *why* behind each choice, the endpoint reference is the [OpenAPI spec](/docs/api-reference/openapi/). ## Step 0 — Register a public client (one-time) Register a **public OAuth client** in **Settings → Team → OAuth apps** — self-service, any org admin can do it. See [Register an app](/docs/oauth/register-an-app/) for the full field reference. - **Application type:** Public (mobile / SPA — no secret; PKCE is the proof) - **Redirect URI:** `entrybitresident://oauth/callback` - **Scopes:** `openid profile email offline_access passes:read passes:write invites:read` You get a **`client_id`** like `eb_9f1c…` immediately. That, plus the base URL, is everything the app needs. > **`client_id` is not an API key.** The `client_id` (`eb_…`, no `sk`) is public and belongs in the app. An **API key** (`eb_sk_…`, from Settings → API keys) is a **server secret** — never put it in an app; anyone can extract it from an APK or bundle. See [Authentication](/docs/get-started/authentication/). ## Step 1 — Install ```bash npx expo install expo-auth-session expo-crypto expo-secure-store expo-web-browser ``` Declare the custom scheme (the `entrybitresident://` part of your redirect URI) in `app.json`: ```json { "expo": { "scheme": "entrybitresident" } } ``` > OAuth with a custom scheme does **not** work in Expo Go — use a development build (`npx expo run:ios` / `run:android`, or an EAS dev client). Do not route OAuth through the old AuthSession proxy. ## Step 2 — Sign in Three small modules keep this testable: shared config, a secure token store, and the sign-in screen. All of the code below is complete — paste it in and change the `CLIENT_ID`. `lib/entrybit.ts` — one place for config and the discovery document: ```ts import * as AuthSession from 'expo-auth-session'; export const ISSUER = 'https://entrybit.net'; export const CLIENT_ID = 'eb_9f1c2ab34cd56ef7'; // from step 0 — public, not a secret export const redirectUri = AuthSession.makeRedirectUri({ scheme: 'entrybitresident', path: 'oauth/callback', }); export const SCOPES = [ 'openid', 'profile', 'email', 'offline_access', 'passes:read', 'passes:write', 'invites:read', ]; // The same document useAutoDiscovery() reads, cached for non-hook code. let discovery: AuthSession.DiscoveryDocument | undefined; export async function getDiscovery(): Promise { return (discovery ??= await AuthSession.fetchDiscoveryAsync(ISSUER)); } ``` `lib/tokenStore.ts` — the access token lives ~15 minutes, so it stays **in memory**; only the durable credentials touch the device keychain: ```ts import * as SecureStore from 'expo-secure-store'; import type { TokenResponse } from 'expo-auth-session'; const REFRESH_TOKEN_KEY = 'entrybit.refreshToken'; // the durable credential const ID_TOKEN_KEY = 'entrybit.idToken'; // kept only for logout (id_token_hint) let accessToken: string | null = null; let accessTokenExpiresAt = 0; // epoch ms export async function saveTokens(t: TokenResponse): Promise { accessToken = t.accessToken; accessTokenExpiresAt = (t.issuedAt + (t.expiresIn ?? 900)) * 1000; // Rotation rule: every refresh returns a NEW refresh token — always overwrite. if (t.refreshToken) await SecureStore.setItemAsync(REFRESH_TOKEN_KEY, t.refreshToken); if (t.idToken) await SecureStore.setItemAsync(ID_TOKEN_KEY, t.idToken); } export function getFreshAccessToken(): string | null { return Date.now() < accessTokenExpiresAt - 30_000 ? accessToken : null; // 30 s clock skew } export const getRefreshToken = () => SecureStore.getItemAsync(REFRESH_TOKEN_KEY); export const getIdToken = () => SecureStore.getItemAsync(ID_TOKEN_KEY); export async function clearTokens(): Promise { accessToken = null; accessTokenExpiresAt = 0; await Promise.all([ SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY), SecureStore.deleteItemAsync(ID_TOKEN_KEY), ]); } ``` > **Tokens belong in `expo-secure-store`** (Keychain / Android Keystore) or in memory — never AsyncStorage, never logs, never Redux state persisted to disk. `app/sign-in.tsx` — the screen. The hook opens the system browser; the effect exchanges the returned code for tokens: ```tsx import { useEffect, useRef, useState } from 'react'; import { Button, Text, View } from 'react-native'; import * as AuthSession from 'expo-auth-session'; import { CLIENT_ID, ISSUER, redirectUri, SCOPES } from '../lib/entrybit'; import { saveTokens } from '../lib/tokenStore'; export default function SignInScreen({ onSignedIn }: { onSignedIn: () => void }) { // Configures every endpoint from /.well-known/openid-configuration. const discovery = AuthSession.useAutoDiscovery(ISSUER); const [error, setError] = useState(null); const exchangedCode = useRef(null); const [request, response, promptAsync] = AuthSession.useAuthRequest( { clientId: CLIENT_ID, redirectUri, responseType: 'code', usePKCE: true, // S256 — the server rejects requests without it scopes: SCOPES, }, discovery, ); useEffect(() => { // 'cancel' / 'dismiss' mean the user backed out — nothing to do. if (!discovery || !request?.codeVerifier || response?.type !== 'success') return; // An authorization code is single-use — exchanging it twice revokes the // grant — so guard against effect re-runs before exchanging. const { code } = response.params; if (exchangedCode.current === code) return; exchangedCode.current = code; AuthSession.exchangeCodeAsync( { clientId: CLIENT_ID, code, redirectUri, extraParams: { code_verifier: request.codeVerifier }, }, discovery, ) .then(saveTokens) // { accessToken, refreshToken, idToken, expiresIn: 900 } .then(onSignedIn) .catch(() => setError('Sign-in failed. Please try again.')); }, [discovery, request, response, onSignedIn]); return (