{"openapi":"3.1.0","info":{"title":"EntryBit API","version":"1.0.0","summary":"Sign in with EntryBit (OAuth2/OIDC) + guest-pass API for apps, servers and SDKs.","description":"The public EntryBit API.\n\n## Three ways to authenticate\n\n1. **OAuth2 / OpenID Connect (user-delegated)** — for apps acting on behalf of a signed-in EntryBit user (e.g. the resident mobile app). Authorization-code flow with **PKCE (S256) required**; tokens from `POST /api/oauth/token`. Endpoints under `/api/v1/*` accept the resulting Bearer access token (RS256 JWT, ~15 min) and are additionally bound by the user's organization role.\n2. **Organization API keys (server-to-server)** — for backends and SDKs acting as the organization. Keys look like `eb_sk_…`, are created by an org admin in **Settings → API keys**, are shown exactly once, and carry least-privilege scopes. Send as `Authorization: Bearer eb_sk_…` (or `X-API-Key`). Endpoints under `/api/v1/org/*`.\n3. **Discovery** — everything self-describes at `/.well-known/openid-configuration`; token signatures verify against `/.well-known/jwks.json`.\n\n## Conventions\n\n- **Errors**: protocol endpoints return RFC 6749/6750 error objects (`{\"error\": …}` with a `WWW-Authenticate` challenge on 401/403). Business endpoints return `{\"success\": false, \"error\": …}` or `{\"code\", \"message\"}`; quota exhaustion is **402** with `dimension`/`limit`/`current` fields.\n- **Rate limits**: per-IP and per-credential buckets; exceeding one returns **429** — back off and retry.\n- **Security**: never place credentials in URLs; all endpoints require TLS; `Cache-Control: no-store` on every credential-bearing response. Refresh tokens rotate on every use — always persist the newest one.","contact":{"name":"EntryBit","url":"https://entrybit.net/contact/"}},"servers":[{"url":"https://entrybit.net","description":"Production"}],"tags":[{"name":"Discovery","description":"OIDC/OAuth self-description documents. Public, no auth."},{"name":"OAuth2","description":"The authorization server: authorize, token, introspection, revocation, userinfo, logout."},{"name":"Passes","description":"The signed-in user's guest passes (OAuth Bearer token, `passes:*` scopes)."},{"name":"Invitations","description":"Organization invitations addressed to the signed-in user (`invites:read`)."},{"name":"Organization","description":"Company-wide operations for servers and SDKs (organization API key, `org:*` scopes)."}],"paths":{"/.well-known/openid-configuration":{"get":{"tags":["Discovery"],"operationId":"getOidcConfiguration","summary":"OpenID Connect discovery document","description":"Every endpoint, scope and capability of the authorization server. OIDC libraries configure themselves from this single URL.","responses":{"200":{"description":"Discovery document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscoveryDocument"}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"tags":["Discovery"],"operationId":"getOauthServerMetadata","summary":"OAuth 2.0 authorization-server metadata (RFC 8414)","description":"Identical content to `/.well-known/openid-configuration`.","responses":{"200":{"description":"Metadata document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscoveryDocument"}}}}}}},"/.well-known/jwks.json":{"get":{"tags":["Discovery"],"operationId":"getJwks","summary":"JSON Web Key Set","description":"RS256 public keys for verifying access tokens and id_tokens offline.","responses":{"200":{"description":"JWK set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Jwks"}}}}}}},"/api/oauth/authorize":{"get":{"tags":["OAuth2"],"operationId":"authorize","summary":"Begin the authorization-code flow (browser navigation)","description":"Open in the user's browser (native apps: the SYSTEM browser per RFC 8252 — never a WebView). EntryBit runs login (password, passkey, Google, 2FA) and consent, then 302-redirects to your registered `redirect_uri` with `code`, `state` and `iss` (RFC 9207). **PKCE S256 is mandatory.** Protocol errors after client validation are returned to the redirect_uri as `error`/`error_description`; an invalid `client_id`/`redirect_uri` pair gets a direct 400 (never a redirect).","parameters":[{"name":"response_type","in":"query","required":true,"schema":{"type":"string","enum":["code"]}},{"name":"client_id","in":"query","required":true,"schema":{"type":"string"},"example":"eb_9f1c2ab34cd56ef7"},{"name":"redirect_uri","in":"query","required":true,"description":"Byte-exact match against the client's registered list.","schema":{"type":"string","format":"uri"}},{"name":"scope","in":"query","required":false,"description":"Space-delimited subset of the client's allowed scopes. Default: `openid profile email`.","schema":{"type":"string"},"example":"openid profile email offline_access passes:read"},{"name":"state","in":"query","required":false,"description":"Opaque CSRF value, echoed back verbatim. Strongly recommended.","schema":{"type":"string"}},{"name":"code_challenge","in":"query","required":true,"description":"BASE64URL(SHA-256(code_verifier)).","schema":{"type":"string"}},{"name":"code_challenge_method","in":"query","required":true,"schema":{"type":"string","enum":["S256"]}},{"name":"nonce","in":"query","required":false,"description":"Binds the id_token to this request. Required if you validate id_tokens client-side.","schema":{"type":"string"}},{"name":"prompt","in":"query","required":false,"description":"Space-set of `none`, `login`, `consent`. `login` forces a fresh sign-in; `none` fails fast (`login_required`/`consent_required`) instead of showing UI.","schema":{"type":"string"}},{"name":"max_age","in":"query","required":false,"description":"Maximum seconds since the user last authenticated; older sessions re-authenticate.","schema":{"type":"integer","minimum":0}}],"responses":{"302":{"description":"Redirect: to your `redirect_uri` with `code`+`state`+`iss` on success (or `error` params), or to the EntryBit login/consent UI mid-flow."},"400":{"description":"Invalid client_id or redirect_uri (rendered directly — never redirected).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/oauth/token":{"post":{"tags":["OAuth2"],"operationId":"token","summary":"Exchange a code, or refresh tokens","description":"Machine endpoint (CORS-open, no cookies). Public clients authenticate with PKCE; confidential clients add HTTP Basic or `client_secret_post`. **Refresh tokens rotate**: every refresh returns a NEW `refresh_token` — persist it; replaying an old one revokes the whole token family.","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"oneOf":[{"$ref":"#/components/schemas/AuthorizationCodeGrant"},{"$ref":"#/components/schemas/RefreshTokenGrant"}]},"examples":{"authorization_code":{"value":{"grant_type":"authorization_code","code":"SplxlOBeZQQYbYS6WxSbIA","redirect_uri":"entrybitresident://oauth/callback","client_id":"eb_9f1c2ab34cd56ef7","code_verifier":"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"}},"refresh_token":{"value":{"grant_type":"refresh_token","refresh_token":"8xLOxBtZp8","client_id":"eb_9f1c2ab34cd56ef7"}}}}}},"responses":{"200":{"description":"Tokens issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"Protocol error (`invalid_grant`, `invalid_request`, `invalid_scope`, `unsupported_grant_type`). Descriptions are deliberately generic.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"`invalid_client` — client authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/oauth/introspect":{"post":{"tags":["OAuth2"],"operationId":"introspect","summary":"Introspect a token (RFC 7662, confidential clients only)","description":"Resource-server lookup, bound to the calling client — a client can only introspect tokens issued to it. Anything invalid reads as the flat `{\"active\": false}`.","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string"},"client_id":{"type":"string"},"client_secret":{"type":"string"}}}}}},"responses":{"200":{"description":"Introspection result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntrospectionResponse"}}}},"401":{"description":"`invalid_client`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/oauth/revoke":{"post":{"tags":["OAuth2"],"operationId":"revoke","summary":"Revoke a refresh token (RFC 7009)","description":"Revokes the token and its whole rotation family when it belongs to the calling client. Always returns 200 — even for unknown tokens (no token scanning).","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string"},"client_id":{"type":"string"},"client_secret":{"type":"string"}}}}}},"responses":{"200":{"description":"Revoked (or token was unknown — indistinguishable by design)."},"401":{"description":"`invalid_client`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/oauth/userinfo":{"get":{"tags":["OAuth2"],"operationId":"getUserinfo","summary":"The signed-in user's identity claims (OIDC)","description":"Requires the `openid` scope; `email`/`profile` scopes gate the corresponding claims.","security":[{"OAuthAccessToken":[]},{"EntryBitOAuth":["openid"]}],"responses":{"200":{"description":"Scope-gated claims.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}},"401":{"description":"Missing/invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BearerError"}}}},"403":{"description":"`insufficient_scope` — token lacks `openid`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BearerError"}}}}}},"post":{"tags":["OAuth2"],"operationId":"postUserinfo","summary":"UserInfo via POST (OIDC Core §5.3.1)","description":"Identical to GET.","security":[{"OAuthAccessToken":[]},{"EntryBitOAuth":["openid"]}],"responses":{"200":{"description":"Scope-gated claims.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}},"401":{"description":"Missing/invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BearerError"}}}},"403":{"description":"`insufficient_scope`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BearerError"}}}}}}},"/api/oauth/logout":{"get":{"tags":["OAuth2"],"operationId":"endSession","summary":"RP-initiated logout (OIDC end_session_endpoint)","description":"Open in the browser to end the user's EntryBit web session. The session is terminated ONLY when `id_token_hint` verifies and matches the signed-in user (forced-logout-CSRF defense); the browser is then redirected to `post_logout_redirect_uri` ONLY if it exactly matches the client's registered post-logout list — otherwise to the EntryBit home page.","parameters":[{"name":"id_token_hint","in":"query","required":false,"description":"The id_token previously issued to the client (may be expired; signature must verify).","schema":{"type":"string"}},{"name":"post_logout_redirect_uri","in":"query","required":false,"schema":{"type":"string","format":"uri"}},{"name":"state","in":"query","required":false,"description":"Echoed to the post-logout redirect.","schema":{"type":"string"}},{"name":"client_id","in":"query","required":false,"description":"Optional; must match the hint's audience when present.","schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect to the registered post-logout URI (with `state`) or the EntryBit home page."},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}},"post":{"tags":["OAuth2"],"operationId":"endSessionPost","summary":"RP-initiated logout via form POST","description":"Same contract as GET with the parameters in an `application/x-www-form-urlencoded` body.","requestBody":{"required":false,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"id_token_hint":{"type":"string"},"post_logout_redirect_uri":{"type":"string","format":"uri"},"state":{"type":"string"},"client_id":{"type":"string"}}}}}},"responses":{"302":{"description":"Redirect (see GET)."},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/v1/passes":{"get":{"tags":["Passes"],"operationId":"listMyPasses","summary":"List the user's guest passes","description":"Keyset-paginated list of passes the user created, newest first. The first page carries the monthly invite-allowance `usage` block. Requires the user to still hold the organization's self-invite role.","security":[{"OAuthAccessToken":[]},{"EntryBitOAuth":["passes:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":30}},{"name":"cursor","in":"query","description":"Opaque `next_cursor` from the previous page.","schema":{"type":"string"}},{"name":"search","in":"query","description":"Case-insensitive match on guest name / email / phone.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of passes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PassPage"}}}},"401":{"$ref":"#/components/responses/InvalidToken"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Passes"],"operationId":"createMyPasses","summary":"Create guest pass(es)","description":"Creates 1–10 passes with distinct QR credentials, scoped to a facility's doors, and delivers ONE email/SMS carrying a single pass link. Counts against the user's monthly invite allowance (all-or-nothing for the batch).","security":[{"OAuthAccessToken":[]},{"EntryBitOAuth":["passes:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PassCreateRequest"}}}},"responses":{"201":{"description":"Pass(es) created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PassCreateResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/InvalidToken"},"402":{"$ref":"#/components/responses/QuotaExceeded"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"Facility not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppError"}}}},"409":{"description":"Facility deactivated or suspended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppError"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/passes/{public_id}":{"get":{"tags":["Passes"],"operationId":"getMyPass","summary":"Get one of the user's passes","security":[{"OAuthAccessToken":[]},{"EntryBitOAuth":["passes:read"]}],"parameters":[{"$ref":"#/components/parameters/PassPublicId"}],"responses":{"200":{"description":"The pass.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"pass":{"$ref":"#/components/schemas/Pass"}}}}}},"401":{"$ref":"#/components/responses/InvalidToken"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Passes"],"operationId":"revokeMyPass","summary":"Revoke one of the user's active passes","description":"Cancels the pass AND deactivates its QR credential on the door hardware. Only passes the user created, and only while still active (`expected`/`registered`).","security":[{"OAuthAccessToken":[]},{"EntryBitOAuth":["passes:write"]}],"parameters":[{"$ref":"#/components/parameters/PassPublicId"}],"responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeResponse"}}}},"401":{"$ref":"#/components/responses/InvalidToken"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The pass is no longer active.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppError"}}}}}}},"/api/v1/invites":{"get":{"tags":["Invitations"],"operationId":"listMyInvites","summary":"Pending organization invitations addressed to the user","security":[{"OAuthAccessToken":[]},{"EntryBitOAuth":["invites:read"]}],"responses":{"200":{"description":"Pending invitations.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"invites":{"type":"array","items":{"$ref":"#/components/schemas/OrgInvite"}}}}}}},"401":{"$ref":"#/components/responses/InvalidToken"},"403":{"$ref":"#/components/responses/InsufficientScope"}}}},"/api/v1/org/passes":{"get":{"tags":["Organization"],"operationId":"orgListPasses","summary":"List ALL passes in the organization","description":"Company-wide, keyset-paginated. Rows include `created_by` (the inviting user's id) for attribution.","security":[{"OrgApiKey":[]},{"OrgApiKeyHeader":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":30}},{"name":"cursor","in":"query","schema":{"type":"string"}},{"name":"search","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of passes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PassPage"}}}},"401":{"$ref":"#/components/responses/InvalidApiKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Organization"],"operationId":"orgCreatePass","summary":"Create guest pass(es) as the organization","description":"Same body and behavior as the user-delegated create; attribution and invite quota go to the admin who minted the key. Requires the `org:passes:write` scope.","security":[{"OrgApiKey":[]},{"OrgApiKeyHeader":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PassCreateRequest"}}}},"responses":{"201":{"description":"Pass(es) created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PassCreateResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/InvalidApiKey"},"402":{"$ref":"#/components/responses/QuotaExceeded"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"Facility not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppError"}}}},"409":{"description":"Facility deactivated or suspended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppError"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/org/passes/{public_id}":{"delete":{"tags":["Organization"],"operationId":"orgRevokePass","summary":"Revoke ANY active pass in the organization","description":"Cancels the pass and deactivates its QR credential — regardless of which user created it (still tenant-scoped to the key's organization).","security":[{"OrgApiKey":[]},{"OrgApiKeyHeader":[]}],"parameters":[{"$ref":"#/components/parameters/PassPublicId"}],"responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeResponse"}}}},"401":{"$ref":"#/components/responses/InvalidApiKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The pass is no longer active.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppError"}}}}}}},"/api/v1/org/facilities":{"get":{"tags":["Organization"],"operationId":"orgListFacilities","summary":"List the organization's facilities","description":"The facility ids an SDK needs to create passes.","security":[{"OrgApiKey":[]},{"OrgApiKeyHeader":[]}],"responses":{"200":{"description":"Facilities.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"facilities":{"type":"array","items":{"$ref":"#/components/schemas/Facility"}}}}}}},"401":{"$ref":"#/components/responses/InvalidApiKey"},"403":{"$ref":"#/components/responses/InsufficientScope"}}}}},"components":{"securitySchemes":{"EntryBitOAuth":{"type":"oauth2","description":"Authorization-code flow. PKCE (S256) is REQUIRED for every client; public clients (mobile/SPA) have no secret.","flows":{"authorizationCode":{"authorizationUrl":"https://entrybit.net/api/oauth/authorize","tokenUrl":"https://entrybit.net/api/oauth/token","refreshUrl":"https://entrybit.net/api/oauth/token","scopes":{"openid":"Verify your identity","profile":"Your name and profile photo","email":"Your email address","offline_access":"Stay signed in (issue a refresh token)","passes:read":"View your guest passes","passes:write":"Create and manage your guest passes","invites:read":"View invitations addressed to you"}}}},"OAuthAccessToken":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"An EntryBit access token (RS256 JWT, `typ: at+jwt`) from POST /api/oauth/token. Verifiable offline against /.well-known/jwks.json."},"OrgApiKey":{"type":"http","scheme":"bearer","bearerFormat":"eb_sk_…","description":"An organization API key (created in Settings → API keys, shown once). 256-bit secret; optional expiry and source-IP allowlist; least-privilege org:* scopes."},"OrgApiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Alternative header for the organization API key."}},"parameters":{"PassPublicId":{"name":"public_id","in":"path","required":true,"description":"The pass's public identifier (`gst_…`).","schema":{"type":"string"}}},"responses":{"InvalidToken":{"description":"Missing, malformed, expired or revoked access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BearerError"}}}},"InvalidApiKey":{"description":"Missing, unknown, revoked, expired or IP-restricted API key (indistinguishable by design).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BearerError"}}}},"InsufficientScope":{"description":"The credential lacks a required scope (the `WWW-Authenticate` challenge names it), or the acting user's organization role was revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BearerError"}}}},"RateLimited":{"description":"Too many requests — back off and retry.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"temporarily_unavailable"}}}}}},"NotFound":{"description":"No such resource in the caller's scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppError"}}}},"ValidationError":{"description":"The request body failed validation (the `message`/`error` says which field).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppError"}}}},"QuotaExceeded":{"description":"The monthly invite allowance can't fit the batch (all-or-nothing).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuotaError"}}}}},"schemas":{"DiscoveryDocument":{"type":"object","description":"OIDC Discovery / RFC 8414 metadata (key fields shown; consult the live document).","properties":{"issuer":{"type":"string","format":"uri"},"authorization_endpoint":{"type":"string","format":"uri"},"token_endpoint":{"type":"string","format":"uri"},"userinfo_endpoint":{"type":"string","format":"uri"},"end_session_endpoint":{"type":"string","format":"uri"},"introspection_endpoint":{"type":"string","format":"uri"},"revocation_endpoint":{"type":"string","format":"uri"},"jwks_uri":{"type":"string","format":"uri"},"scopes_supported":{"type":"array","items":{"type":"string"}},"response_types_supported":{"type":"array","items":{"type":"string"}},"grant_types_supported":{"type":"array","items":{"type":"string"}},"code_challenge_methods_supported":{"type":"array","items":{"type":"string"}},"token_endpoint_auth_methods_supported":{"type":"array","items":{"type":"string"}},"id_token_signing_alg_values_supported":{"type":"array","items":{"type":"string"}},"subject_types_supported":{"type":"array","items":{"type":"string"}},"claims_supported":{"type":"array","items":{"type":"string"}},"authorization_response_iss_parameter_supported":{"type":"boolean"}},"required":["issuer","authorization_endpoint","token_endpoint","jwks_uri"]},"Jwks":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"object","properties":{"kty":{"type":"string"},"use":{"type":"string"},"alg":{"type":"string"},"kid":{"type":"string"},"n":{"type":"string"},"e":{"type":"string"}}}}},"required":["keys"]},"AuthorizationCodeGrant":{"type":"object","title":"authorization_code","required":["grant_type","code","redirect_uri","code_verifier","client_id"],"properties":{"grant_type":{"type":"string","const":"authorization_code"},"code":{"type":"string","description":"The one-time code from the redirect (60 s TTL, single use)."},"redirect_uri":{"type":"string","format":"uri","description":"Must equal the authorize request's redirect_uri byte-for-byte."},"code_verifier":{"type":"string","minLength":43,"maxLength":128,"description":"The PKCE verifier whose S256 hash was sent to /authorize."},"client_id":{"type":"string"},"client_secret":{"type":"string","description":"Confidential clients only (or use HTTP Basic)."}}},"RefreshTokenGrant":{"type":"object","title":"refresh_token","required":["grant_type","refresh_token","client_id"],"properties":{"grant_type":{"type":"string","const":"refresh_token"},"refresh_token":{"type":"string","description":"The CURRENT refresh token (they rotate on every use)."},"client_id":{"type":"string"},"client_secret":{"type":"string","description":"Confidential clients only (or use HTTP Basic)."},"scope":{"type":"string","description":"Optional NARROWING of the granted scope (never widening)."}}},"TokenResponse":{"type":"object","required":["access_token","token_type","expires_in","scope"],"properties":{"access_token":{"type":"string","description":"RS256 JWT (`typ: at+jwt`), ~15 minutes. Send as `Authorization: Bearer …`."},"token_type":{"type":"string","const":"Bearer"},"expires_in":{"type":"integer","example":900},"scope":{"type":"string"},"refresh_token":{"type":"string","description":"Present when `offline_access` was granted. Rotates on every refresh — always persist the newest."},"id_token":{"type":"string","description":"OIDC id_token (present with `openid`): `sub`, identity claims, `nonce`, `auth_time`, `amr` (how the user authenticated — e.g. [\"swk\",\"user\"] for a passkey), `at_hash`."}}},"IntrospectionResponse":{"type":"object","required":["active"],"properties":{"active":{"type":"boolean"},"sub":{"type":"string"},"scope":{"type":"string"},"client_id":{"type":"string"},"token_type":{"type":"string"},"iss":{"type":"string"},"aud":{"type":"string"},"exp":{"type":"integer"},"iat":{"type":"integer"}}},"UserInfo":{"type":"object","required":["sub"],"properties":{"sub":{"type":"string","description":"Stable EntryBit user identifier."},"email":{"type":"string","format":"email","description":"With the `email` scope."},"email_verified":{"type":"boolean"},"name":{"type":"string","description":"With the `profile` scope."},"picture":{"type":"string","description":"With the `profile` scope."}}},"Pass":{"type":"object","description":"A guest pass. PII fields are decrypted server-side for authorized callers only.","properties":{"public_id":{"type":"string","example":"gst_9f1c2ab34cd56ef7"},"first_name":{"type":"string"},"last_name":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"status":{"type":"string","enum":["expected","registered","checked_in","checked_out","cancelled"]},"created_at":{"type":"string"},"arrival_date":{"type":"string","example":"2026-07-12"},"arrival_time":{"type":["string","null"],"example":"14:30"},"facility_id":{"type":["integer","null"]},"facility_name":{"type":["string","null"]},"created_by":{"type":"integer","description":"Inviting user's id (organization listings only)."}}},"PassPage":{"type":"object","required":["success","items","has_more"],"properties":{"success":{"type":"boolean"},"items":{"type":"array","items":{"$ref":"#/components/schemas/Pass"}},"total":{"type":["integer","null"],"description":"Unreliable while `search` is set."},"next_cursor":{"type":["string","null"],"description":"Pass back as `cursor` for the next page."},"has_more":{"type":"boolean"},"usage":{"$ref":"#/components/schemas/AllowanceUsage"}}},"AllowanceUsage":{"type":"object","description":"Monthly invite allowance (first page only, user-delegated listing).","properties":{"used":{"type":"integer"},"limit":{"type":"integer"},"remaining":{"type":"integer"}}},"PassCreateRequest":{"type":"object","required":["first_name","arrival_date","facility_id"],"description":"At least one delivery channel (`email` or `phone`) is required so the guest can receive their pass.","properties":{"first_name":{"type":"string","maxLength":100},"last_name":{"type":"string","maxLength":100},"email":{"type":"string","format":"email","description":"Delivery channel — the pass email."},"phone":{"type":"string","description":"Delivery channel — the pass SMS. 7–15 digits."},"arrival_date":{"type":"string","description":"`YYYY-MM-DD` (or `YYYY-MM-DDTHH:MM` — the time part seeds arrival_time).","example":"2026-07-12"},"arrival_time":{"type":"string","description":"`HH:MM`; access starts here (default: start of the arrival day).","example":"14:30"},"access_until":{"type":"string","description":"Optional pass expiry (default: end of the arrival day). Must not precede arrival_date.","example":"2026-07-14T23:59"},"facility_id":{"type":"integer","description":"Which facility's entry doors the QR opens. Must be active."},"quantity":{"type":"integer","minimum":1,"maximum":10,"default":1,"description":"Create N distinct passes in one call — one email/SMS with a single batch link."},"country":{"type":"string"},"vehicle_number":{"type":"string"},"notes":{"type":"string"},"invited_by":{"type":"string","description":"Display name shown to the guest."}}},"PassCreateResponse":{"type":"object","required":["success","id","public_id","quantity"],"properties":{"success":{"type":"boolean"},"id":{"type":"integer","description":"First guest's internal id (backward compatibility)."},"public_id":{"type":"string","description":"First pass's public id."},"qr_value":{"type":["string","null"],"description":"First pass's QR value (also in the delivered email)."},"qr_values":{"type":"array","items":{"type":"string"},"description":"Every issued QR value, in creation order."},"quantity":{"type":"integer"},"qr_sent":{"type":"boolean","description":"Whether the pass email was delivered."},"sms_sent":{"type":"boolean","description":"Whether the pass SMS was delivered."},"pass_link":{"type":["string","null"],"description":"The single shareable pass page URL (shows all QRs in the batch)."},"message":{"type":"string"}}},"RevokeResponse":{"type":"object","required":["success","public_id","revoked"],"properties":{"success":{"type":"boolean"},"public_id":{"type":"string"},"revoked":{"type":"boolean"}}},"OrgInvite":{"type":"object","properties":{"id":{"type":"integer"},"role":{"type":["string","null"]},"status":{"type":"string","const":"pending"},"created_at":{"type":["string","null"],"format":"date-time"},"organization":{"type":["string","null"],"description":"The inviting organization's display name."}}},"Facility":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"description":{"type":["string","null"]},"is_active":{"type":"boolean"},"suspended":{"type":"boolean","description":"Suspended facilities pause new invites."}}},"OAuthError":{"type":"object","required":["error"],"description":"RFC 6749 error object. Descriptions are deliberately generic (no token-state oracle) — branch on `error`, never on the text.","properties":{"error":{"type":"string","enum":["invalid_request","invalid_client","invalid_grant","unauthorized_client","unsupported_grant_type","unsupported_response_type","invalid_scope","access_denied","server_error","temporarily_unavailable","login_required","consent_required","interaction_required"]},"error_description":{"type":"string"}}},"BearerError":{"type":"object","required":["error"],"description":"RFC 6750 challenge body; the `WWW-Authenticate` response header carries the same code (and the missing scope on 403).","properties":{"error":{"type":"string","enum":["invalid_token","insufficient_scope"]}}},"AppError":{"type":"object","description":"Business-error envelope (one of two equivalent shapes).","properties":{"success":{"type":"boolean","const":false},"error":{"type":"string"},"code":{"type":"string"},"message":{"type":"string"}}},"QuotaError":{"type":"object","description":"Monthly allowance exhausted (HTTP 402).","properties":{"code":{"type":"string","const":"CAPACITY_EXCEEDED"},"message":{"type":"string"},"dimension":{"type":"string","const":"guest_invites"},"limit":{"type":"integer"},"current":{"type":"integer"},"requested":{"type":"integer"}}}}}}