# Install Presence on the web This is the shortest complete public integration path for the default hosted Presence. The browser needs no Presence package and never receives your API key. Machine-readable operation and schema definitions: https://presence.bohita.com/openapi.json ## Before you start: browser-only static apps The self-serve path requires a trusted place to hold the API key and create short-lived Sessions. This can be an existing same-origin BFF, a serverless function, a traditional server, a trusted local coding-agent process, or a desktop host. It does not require deploying a conventional server or public website. Framework choice is not the boundary. A Vite or other static frontend can mount Presence when it calls one of those trusted processes. A browser-only production SPA with no BFF or trusted process cannot safely use the secret API key. Bohita's tenant BFF is available to applications deployed on Bohita infrastructure; it is not automatically mounted on an arbitrary customer domain. A browser-safe, origin-scoped bootstrap credential is required for a purely static external deployment. That credential mode is not self-serve today. Do not put `BOHITA_API_KEY` in frontend code or a `VITE_*` variable to work around this limitation. [Contact Bohita](mailto:hello@bohita.com) before choosing this topology. For local evaluation, a trusted local process can hold the key while the static page is served over HTTP. Do not double-click the HTML file or use a `file://` URL; run `python -m http.server 8000` (or `python3 -m http.server 8000`) and open `http://localhost:8000`. ## 1. Create an API key and buy minutes Create or manage an API key at: https://presence.bohita.com/account/api-keys Buy occupied-time credits at: https://presence.bohita.com/#pricing Creating a key establishes customer and application identity. It grants no usage and cannot access the free demo. Paid Session creation reserves Presence credits and returns HTTP `402` when the balance is insufficient. Existing keys work immediately after credits are purchased; no rotation is required. The key starts with `bohita_sk_live_`. Keep it in one of the trusted processes described above. Never put it in browser JavaScript, source control, a URL, analytics, or a browser-exposed environment variable such as `VITE_*`. Current controls require a verified email, allow up to 10 active keys per customer, and allow 120 requests per minute per key. Principals and individual keys can be revoked. Configure the trusted process with: ```bash export BOHITA_API_BASE_URL=https://api.bohita.com export BOHITA_API_KEY=bohita_sk_live_... ``` ## 2. Create a web Session Create the Session from your trusted process. For the default published Presence, omit `presence_id`: ```http POST https://api.bohita.com/v1/sessions Authorization: Bearer $BOHITA_API_KEY Idempotency-Key: Content-Type: application/json { "external_id": "your-opaque-user-or-guest-id", "surface": {"type": "web"}, "startup": "managed" } ``` Keep `Idempotency-Key` stable for network retries of this user action. Generate a new key only for a new deliberate Session attempt. A pre-ready replacement triggered by `restart_required` is a new attempt and therefore uses a new key. `external_id` should be your own opaque identifier. Do not send an email address or another unnecessary personal identifier. A successful response includes the admitted Session and a short-lived hosted client URL: ```json { "session_id": "psn_...", "presence_id": "presence/default", "version": 1, "external_id": "your-opaque-user-or-guest-id", "surface": {"type": "web"}, "state": "ready", "embed_url": "https://...short-lived-scoped-url...", "expires_at_unix": 1787000000, "max_seconds": 300, "replayed": false, "created_at_unix": 1786999700.25, "recording": { "available": true, "policy_version": "presence.recording.v1", "disclosure": "Server-owned participant disclosure text" } } ``` Relay only the browser-safe fields, normally `session_id`, `embed_url`, and `expires_at_unix`. Never relay the API key or the complete upstream response. The create response is this direct object, not an envelope. `replayed: true` means the service returned the result previously stored for the same `Idempotency-Key` and identical input. Handle HTTP `402` as insufficient credits and `429` as rate or capacity pressure. Do not retry either in a tight loop. The request body itself is required, but all its fields have defaults. It also accepts `presence_id`, `version`, and `requested_max_seconds`. The requested duration must be `1..86400`; the current product clamps an admitted Session to at most 3,600 seconds. Insufficient credits return `402` rather than silently reducing the request to the available balance. Unknown fields return `422`. ### Session lifetime `max_seconds` and `expires_at_unix` describe different limits: - `max_seconds` is the maximum connected media time admitted for this Session. A launch demo permits 300 connected seconds. Paid requests are clamped to product policy and available credit. - `expires_at_unix` is the absolute Unix-time expiry of the scoped connection capability and any unused reservation. It may include bounded admission and reconnect grace. Do not derive it from `max_seconds`. Mount `embed_url` immediately. If its capability expires before the first media connection, the URL becomes unusable and the unconsumed reservation is released. Reaching the connected-time limit closes media and finalizes the Session normally. Capability expiry does not terminate an already-open media connection, but a transport loss after expiry cannot reconnect. Reconnecting while the capability remains valid does not reset connected usage. There is no refresh or extend operation. Continuing requires a newly admitted Session with a new idempotency key. A host may make one bounded replacement when the iframe reports the documented pre-ready `restart_required` phase. Otherwise, create another Session only after an explicit user action. Session expiry is independent of Firebase login and API-key expiry. ## 3. Mount the returned URL unchanged ```html ``` Keep `display-capture` in the iframe permission policy so the hosted client can offer screen sharing after an explicit user action. Screen sharing is optional and transient Session input. Some mobile browsers do not support display capture and leave the control unavailable. Treat `embed_url` as opaque. It is an HTTPS document containing the complete hosted client, not a WebSocket address. Do not parse, rewrite, persist, proxy, construct, or replace it. The hosted client owns media, character rendering, audio playback, captions, expressions, in-frame artifacts, interruption, reconnection, and cleanup. Explain why camera and microphone access are useful before requesting browser permission. Apply your own authentication, eligibility, quota, and abuse controls to the endpoint that creates Sessions. Camera behavior belongs to the published Presence version, not the Session creation request. A `required` policy makes camera readiness part of startup. The default `on_request` policy starts voice without prompting for a new camera grant; it may reuse an existing grant or ask conversationally when visual context would help. Refusing camera does not block audio in `on_request` mode. Do not add a camera-policy field to `POST /v1/sessions`. Use `startup: "managed"` for the managed arrival treatment. `startup: "host"` is also accepted for a host that implements the readiness contract below. A successful reservation means the scoped URL can be mounted; it does not mean first audio is already playing. When enabled, conversational readiness is posted by the iframe: ```json {"type":"presence.ready","protocol":"bohita.embed.v1","session_id":"ses_..."} ``` Accept messages only from the iframe's `contentWindow`, the exact origin of the returned `embed_url`, and the current Session ID. ### Pre-ready recovery If the hosted runtime can no longer recover before becoming conversationally ready, it posts: ```json { "type": "presence.startup", "protocol": "bohita.embed.v1", "session_id": "ses_...", "phase": "restart_required" } ``` Accept this message only from the current iframe's `contentWindow`, the exact origin of its returned `embed_url`, protocol `bohita.embed.v1`, and the current Session ID. The host may make one automatic replacement for the user-started launch. It must create a new Session as a new attempt with a new idempotency key, then replace the iframe with the new opaque `embed_url`. A browser BFF obtains fresh user authentication and abuse proof when required; a customer server uses its existing API key. This creates a replacement Session and does not refresh the old one. Permit at most one automatic replacement per user action. Do not retry the old Session, loop replacements, or ask the iframe to create its own Session. After the bounded replacement, another attempt requires an explicit user action. Other startup progress, including `phase: "stalled"`, may still be resolved by permission or an in-frame retry and must not trigger Session replacement. ## 4. Artifacts and sharing The iframe may emit `presence.text`, `artifact_available`, and `presence.share.requested`. `presence.text` and `artifact_available` require no host UI. When durable sharing is enabled, handle `presence.share.requested` through the authenticated share endpoint and reply with `presence.share.ready` or `presence.share.failed`. The authenticated host, not the iframe, grants the durable permalink. ## 5. End the Session Remove the iframe when the host surface closes. Also end the Session from your trusted process when practical: ```http GET https://api.bohita.com/v1/sessions/{session_id} GET https://api.bohita.com/v1/sessions/{session_id}/events?after=evt_...&limit=100 GET https://api.bohita.com/v1/sessions/{session_id}/outputs Authorization: Bearer $BOHITA_API_KEY ``` End the Session with a separate stable idempotency key for this cleanup attempt. The header is required; omitting it returns HTTP `400`. ```http POST https://api.bohita.com/v1/sessions/{session_id}/end Authorization: Bearer $BOHITA_API_KEY Idempotency-Key: Content-Type: application/json {"reason":"customer_ended"} ``` The end body is optional. `customer_ended` is the default reason, and the reason may contain at most 100 characters. Session end is explicit and idempotent. Preserve this cleanup key for retries with identical input. Reusing it with changed input returns `409`; use a new key for a different end attempt. The event log returns `events`, `has_more`, and an optional `next_after` cursor. Pass `next_after` as the next `after` value when `has_more` is true. The default page limit is 100 and the allowed range is `1..500`. An empty `events` array means the reader is caught up. ## 6. Read what the Session left behind Ending a Session and reading its results are separate operations. `/end` requests shutdown and returns Session status. The owner-authorized `/outputs` endpoint returns the durable output manifest for that Session: ```http GET https://api.bohita.com/v1/sessions/{session_id}/outputs Authorization: Bearer $BOHITA_API_KEY ``` ```json { "session_id": "psn_...", "state": "ended", "outputs": [ { "kind": "transcript", "output_id": "transcript", "schema_version": "1", "content": [ {"t_ms": 1200, "role": "user", "text": "Hello"}, {"t_ms": 1840, "role": "assistant", "text": "Hi"} ] }, { "kind": "artifact", "output_id": "session-summary", "artifact_id": "session-summary", "artifact_ref": "urn:presence:artifact/...", "renderer": "presence.board", "schema_version": "1", "shareable": true }, { "kind": "recording", "recording_id": "rec_...", "schema_version": "1", "manifest_asset_ref": "opaque-do-not-parse" } ] } ``` Ask the Presence to leave behind the outcome the product needs: a session summary, meeting notes, what someone got stuck on, what it helped with, or what still needs attention. Those retained outcomes appear as artifacts in this manifest. Their content and shape follow the published Presence rather than a single universal summary schema. For example, a Presence that sits with people trying a developer product can be authored to retain a `session-summary` artifact with this body: ```json { "where_they_got_stuck": [ { "area": "Clerk quickstart", "signal": "Paused after a 500 error" } ], "what_it_helped_with": [ { "issue": "Dashboard route was excluded", "resolution": "Updated the middleware matcher" } ], "what_it_left_behind": [ { "kind": "artifact", "title": "Working middleware rule" } ] } ``` This is an example authored artifact body, not a fixed response schema. A tutoring Presence, support Presence, or meeting Presence can retain a different shape. The `/outputs` response remains the stable manifest that tells the owner which retained transcript, artifact, and recording outputs exist. To post that summary body directly to an endpoint, publish the Presence with a strict end-of-session function and a server-side tool webhook: ```json { "tools": [{ "type": "function", "name": "report_product_trial", "description": "Call once when the product trial ends with the useful outcome.", "parameters": { "type": "object", "properties": { "where_they_got_stuck": {"type": "array", "items": {"type": "object"}}, "what_it_helped_with": {"type": "array", "items": {"type": "object"}}, "what_it_left_behind": {"type": "array", "items": {"type": "object"}} }, "required": [ "where_they_got_stuck", "what_it_helped_with", "what_it_left_behind" ], "additionalProperties": false }, "strict": true }], "tool_runtime": { "type": "webhook", "url": "https://example.com/presence/tools" } } ``` Tell the Presence in its role to call `report_product_trial` once when closing the trial and to retain the same outcome as its `session-summary` artifact. Bohita delivers the function arguments to the configured tool webhook; the durable artifact remains discoverable through `/outputs`. An abruptly expired or externally terminated Session may not complete a conversational tool call, so use lifecycle subscriptions when the receiver must also learn that the Session itself ended. To receive these outcomes without polling, configure lifecycle subscriptions on the published Presence for `artifact.created`, `transcript.finalized`, and `session.ended`: ```json { "event_subscriptions": [{ "type": "webhook", "url": "https://example.com/presence/events", "events": ["artifact.created", "transcript.finalized", "session.ended"] }] } ``` Bohita stores the destination server-side and owns signing, delivery, retry, audit, and deduplication. Verify the webhook signature and process deliveries idempotently. Lifecycle delivery tells the receiver that a durable outcome or Session transition exists; the authenticated `/outputs` manifest remains the owner-authorized source of truth. Small transcripts may be returned inline. Artifact and recording entries use opaque references; recording entries do not contain audio or video bytes. Never construct a download or public URL from a reference. Sharing remains a separate authorized operation. Outputs can appear after `/end` while transcript, artifact, or recording work finishes. Treat `output_id`, `artifact_id`, and `recording_id` as stable identities, and do not assume the first response is final. If nothing was retained, the response contains `"outputs": []`. ## HTTP errors - `400`: malformed request or missing required `Idempotency-Key`. Do not retry unchanged. - `401`: `Authorization` is missing, malformed, invalid, expired, or revoked. - `402`: insufficient Presence credits. Purchase credits, then retry with the existing API key and a new `Idempotency-Key`. - `403`: the authenticated principal is not authorized for the operation. - `404`: the Session does not exist or is not visible to this authenticated customer. - `409`: request conflicts with current state, including reusing an idempotency key with changed input. - `422`: JSON or query validation failed, including unknown request fields. - `429`: request-rate or live-capacity limit. Use bounded backoff; `Retry-After` is not currently guaranteed. - `500`: internal failure. Retry only with bounded backoff. - `503`: temporary unavailability. Retry with bounded backoff and preserve the `Idempotency-Key` for the same mutation attempt. Error bodies currently use a top-level `detail`. Its value can be a string, a typed `{code,message}` object, or an array of validation details containing `type`, `loc`, `msg`, and optional `field_path`. Branch on HTTP status first, then use a typed code when present. The exact union, request, response, authentication, and header schemas are at https://presence.bohita.com/openapi.json. ## Privacy and retention The `presence_server` customer API key supports Presence drafts and publishing, custom role/guidance, knowledge, and tools, plus web, voice, meeting, and approved outbound-phone Sessions. Inbound phone remains carrier-originated; managed outbound phone requires Bohita approval. Use the public Presence API contract rather than inferring routes from the repository. - Camera and microphone use require participant permission. - Live media may be processed without being retained. - Recording and durable media are off by default. Retention requires Session-scoped authority and applicable participant consent. - Browser clients receive Session-scoped credentials, never API keys. - Treat transcripts, recordings, artifacts, and visible-screen content as private customer data unless an explicit sharing policy says otherwise. - The launch demo at presence.bohita.com is not recorded. Public API compatibility boundary: `/v1`. Public contract release: `v0.1.5`, commit `5d1d6f4d2276253dc3f037e0c83e3af4cc405a42`. - Changelog: https://presence.bohita.com/changelog.txt - Support: mailto:hello@bohita.com