Agentic Workforce ME Developer PortalDocs 1.21 · Widget 0.5.0 · API —

Reference

REST reference

The public /v1/embed/* endpoints, SSE event types, error codes, the admin /v1/embed-keys API and the SDK.

Everything the widget does goes through six public endpoints under /v1/embed/ plus the hosted bundle. You only need them if you build a custom UI (a native app, a server-side integration, a different chat surface). Teams automating key management use the admin endpoints under /v1/embed-keys with a platform API key, or @hive/sdk.

Public endpoints

GET/v1/embed/configPublic display config for a key — what a widget needs before a session exists. The widget calls this on connect and fires hive:ready.
Auth
Publishable key in the X-Hive-Embed-Key header + Origin on the allowlist
Rate limit
60/min per IP
Errors
EMBED_KEY_INVALID EMBED_KEY_DISABLED EMBED_ORIGIN_DENIED VALIDATION

Request

FieldTypeDescription
X-Hive-Embed-Keyheader, stringThe publishable key hive_pk_… (a header, never the URL, so it stays out of access logs).

Response

FieldTypeDescription
key_iduuidEmbed key id.
agent{ name, emoji, avatar }Agent display name, emoji, and talking avatar (avatar is null unless the key enables it). See Talking avatars.
themeEmbedThemeServer theme.
configEmbedDisplayConfigServer display config.
identity_requiredbooleanAnonymous sessions refused when true.
voice{ handsfree, ptt }Effective voice flags for this key’s agent.
  • No session and no credentials are involved; a page view costs exactly this one call.
  • Browsers omit Origin on same-origin GETs, so a missing header is evaluated as the implied page origin (Referer origin, else the request’s own origin) against the allowlist.
Shell
curl https://console.agenticworkforce.me/v1/embed/config \
  -H "Origin: https://www.acme.com" -H "X-Hive-Embed-Key: $EMBED_KEY"

# → 200
# { "key_id": "…", "agent": { "name": "Docs assistant", "emoji": null },
#   "theme": { "primary": "#4f46e5" }, "config": { "welcome": "Hi!" },
#   "identity_required": false }
GET/v1/embed/avatar/modelThe bound agent’s 3D avatar bytes (model/gltf-binary), for a widget whose key shows a 3D face. The ?v= query is a cache-buster; the route does not read it.
Auth
Publishable key in the X-Hive-Embed-Key header + Origin on the allowlist
Rate limit
6/min per IP, shared with GET /v1/embed/avatar/photo
Errors
EMBED_KEY_INVALID EMBED_KEY_DISABLED EMBED_ORIGIN_DENIED NOT_FOUND RATE_LIMITED VALIDATION
  • 404 NOT_FOUND unless config.avatar.enabled is true, the agent’s avatar mode is 3d, and the model bytes exist under that agent’s storage prefix.
  • Cache-Control: private, max-age=86400, X-Content-Type-Options: nosniff. A shared cache must not store the bytes — they are served only against the key.
  • The widget fetches this after lazy-loading hive-embed-avatar3d.js. See Talking avatars.
GET/v1/embed/avatar/photoThe bound agent’s animated-photo bytes (JPEG, PNG or WebP), for a widget whose key shows a photo face. The ?v= query is a cache-buster; the route does not read it.
Auth
Publishable key in the X-Hive-Embed-Key header + Origin on the allowlist
Rate limit
6/min per IP, shared with GET /v1/embed/avatar/model
Errors
EMBED_KEY_INVALID EMBED_KEY_DISABLED EMBED_ORIGIN_DENIED NOT_FOUND RATE_LIMITED VALIDATION
  • 404 NOT_FOUND unless config.avatar.enabled is true, the agent’s avatar mode is photo, and the photo bytes exist under that agent’s storage prefix.
  • Cache-Control: private, max-age=86400, X-Content-Type-Options: nosniff. Content type follows the stored file.
  • The widget fetches this from hive-embed-avatar.js and falls back to the 2D look if the download fails. See Talking avatars.
POST/v1/embed/sessionExchange the publishable key (+ optional identity JWT) for a 15-minute session token.
Auth
Publishable key in the body + Origin header on the allowlist
Rate limit
30 mints/min per IP, then 10 × the key’s rate_limit_per_min per key
Errors
EMBED_KEY_INVALID EMBED_KEY_DISABLED EMBED_ORIGIN_DENIED EMBED_IDENTITY_REQUIRED EMBED_IDENTITY_INVALID EMBED_IDENTITY_UNAVAILABLE RATE_LIMITED VALIDATION

Request

FieldTypeDescription
keystringPublishable key hive_pk_….
visitor_idstring (8–128 url-safe chars)Stable per-browser visitor id generated by the widget.
identitystring (JWT), optionalHS256 JWT signed with the key’s identity secret. Required when the key has identity_required.

Response

FieldTypeDescription
tokenstringOpaque signed session token — send as Authorization: Bearer.
expires_atISO date-timeToken expiry (15 minutes). The widget re-mints lazily: when < 30 s remain before a call, or after a 401.
ttl_secnumberToken lifetime in seconds (900).
visitor_idstringEcho of the visitor id the token is bound to.
end_user{ id, name } | nullPresent when an identity JWT was accepted.
key_iduuidThe embed key’s id (never the key itself).
agent{ name, emoji, avatar }Display name, emoji, and the talking avatar (null unless the key’s config.avatar.enabled is true and the agent has a face). See Talking avatars.
themeEmbedThemeServer-side theme stored on the key.
configEmbedDisplayConfigServer-side display config stored on the key.
identity_requiredbooleanWhether anonymous sessions are refused for this key.
voice{ handsfree, ptt }Effective voice flags (deployment ∧ tenant ∧ agent manifest ∧ key). Both false when voice is off.
  • The widget mints lazily — on the first open (launcher), on mount (inline) or on the first send() — never on page load.
  • Identified mints (a valid identity JWT) are audited (embed.session, target = the contact); anonymous mints are counted in metrics only.
  • When the key allows * and the caller sent no Origin, the token is origin-less (o: "*"); otherwise it is pinned to the request origin.
Shell
curl -X POST https://console.agenticworkforce.me/v1/embed/session \
  -H "Origin: https://www.acme.com" -H "Content-Type: application/json" \
  -d '{"key":"'"$EMBED_KEY"'","visitor_id":"visitor-9f1c2b3a4d5e"}'

# → 200
# { "token": "…", "expires_at": "2026-09-03T10:15:00.000Z", "ttl_sec": 900,
#   "visitor_id": "visitor-9f1c2b3a4d5e", "end_user": null, "key_id": "…",
#   "agent": { "name": "Docs assistant", "emoji": null },
#   "theme": { "primary": "#4f46e5" }, "config": { "welcome": "Hi!" },
#   "identity_required": false }

# Identified visitor: add the backend-signed JWT
#   -d '{"key":"…","visitor_id":"…","identity":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…"}'
POST/v1/embed/messages202 AcceptedSend an end-user message; creates or continues the visitor’s thread and enqueues a governed run.
Auth
Session token (Authorization: Bearer) + the same Origin the token was minted for
Rate limit
key rate_limit_per_min (default 60) and 20 messages/min per visitor
Errors
EMBED_UNAUTHORIZED EMBED_ORIGIN_MISMATCH EMBED_ORIGIN_DENIED EMBED_KEY_DISABLED EMBED_RUN_IN_FLIGHT EMBED_THREAD_FULL EMBED_THREAD_REQUIRED_FOR_ATTACHMENT INVALID_ATTACHMENT RATE_LIMITED AGENT_NOT_PUBLISHED NOT_FOUND VALIDATION

Request

FieldTypeDescription
contentstring (1–8000 chars)The user message (trimmed).
thread_iduuid, optionalContinue an existing thread owned by this visitor; omit to start a new one.
attachment_idsuuid[], optional (max 8)Documents uploaded to this owned thread through the attachment endpoint and included in this turn.

Response

FieldTypeDescription
thread_iduuidThe thread (new or continued).
run_iduuidThe queued run — follow it on the events endpoint.
message_iduuidThe stored user message.
  • Thread ownership: threads.created_by = embed:eu:<end_user_id> (identified) or embed:vid:<visitor_id>; a foreign thread_id is a 404, never a 403.
  • The run is a normal chat run with input._origin = { embed_key_id, visitor_id, end_user_id? } — the console shows origin "Embedded widget".
  • One reply at a time per thread: a message while the previous run is queued/running is a 409 EMBED_RUN_IN_FLIGHT (with the run_id); a thread holds at most 200 messages (409 EMBED_THREAD_FULL).
Shell
curl -X POST https://console.agenticworkforce.me/v1/embed/messages \
  -H "Origin: https://www.acme.com" -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"content":"How do I embed the widget in Next.js?"}'

# → 202
# { "thread_id": "…", "run_id": "…", "message_id": "…" }
POST/v1/embed/threads/:id/attachments201 CreatedUpload images or PDF documents to the visitor’s existing thread before sending a message.
Auth
Session token + matching Origin; multipart/form-data
Rate limit
key rate_limit_per_min and 20 uploads/min per visitor
Errors
EMBED_UNAUTHORIZED EMBED_ORIGIN_MISMATCH EMBED_ORIGIN_DENIED EMBED_KEY_DISABLED TOO_MANY_FILES UNSUPPORTED_TYPE FILE_TOO_LARGE NO_FILE RATE_LIMITED NOT_FOUND VALIDATION

Request

FieldTypeDescription
filesmultipart files (1–8)PNG, JPEG, WebP or PDF; at most 20 MB per file.

Response

FieldTypeDescription
items[]{ id, thread_id, filename, mime, size_bytes, kind, page_count, created_at }Stored attachment metadata. Pass each id in the next message’s attachment_ids.
  • The thread must already exist and belong to this visitor. The widget enables attachments after the first accepted turn.
  • Uploaded bytes are stored privately; the response exposes metadata only.
GET/v1/embed/threads/:id/messagesRestore the visitor’s own thread (newest 100 messages, oldest first).
Auth
Session token + matching Origin
Rate limit
per-visitor read bucket: 60/min shared by history, cancel and event streams
Errors
EMBED_UNAUTHORIZED EMBED_ORIGIN_MISMATCH EMBED_ORIGIN_DENIED EMBED_KEY_DISABLED RATE_LIMITED NOT_FOUND VALIDATION

Response

FieldTypeDescription
thread_iduuidThe thread.
items[].iduuidMessage id.
items[].role'user' | 'agent'Only user/agent turns are returned (no system/tool rows).
items[].textstringPlain text of the message.
items[].run_iduuid | nullRun that produced an agent message.
items[].created_atISO date-timeCreation time.
Shell
curl "https://console.agenticworkforce.me/v1/embed/threads/$THREAD_ID/messages" \
  -H "Origin: https://www.acme.com" -H "Authorization: Bearer $SESSION_TOKEN"

# → 200
# { "thread_id": "…", "items": [
#   { "id": "…", "role": "user",  "text": "How do I …", "run_id": null, "created_at": "…" },
#   { "id": "…", "role": "agent", "text": "Use the React wrapper …", "run_id": "…", "created_at": "…" } ] }
GET/v1/embed/runs/:id/eventsServer-Sent Events relay of one run (redacted vocabulary), with ?since= backfill and a 15 s heartbeat.
Auth
Session token + matching Origin; Accept: text/event-stream
Rate limit
per-visitor read bucket (60/min) + at most 2 live streams per visitor and key (429 EMBED_TOO_MANY_STREAMS, Retry-After: 5); finished runs are served from the backfill without taking a slot
Errors
EMBED_UNAUTHORIZED EMBED_ORIGIN_MISMATCH EMBED_ORIGIN_DENIED EMBED_KEY_DISABLED EMBED_TOO_MANY_STREAMS RATE_LIMITED NOT_FOUND VALIDATION

Request

FieldTypeDescription
sincequery, integer, optionalHighest step idx already seen (from llm.delta / tool.* payloads); the server backfills persisted steps after it, then streams live.
  • event: is the event type; data: is the JSON payload. The stream ends after run.completed or run.failed.
  • Use fetch + ReadableStream (the token travels in the Authorization header; EventSource cannot set headers and is unsupported).
  • When the agent has output guardrails, llm.delta is withheld and the final text arrives only in run.completed.
Shell
curl -N "https://console.agenticworkforce.me/v1/embed/runs/$RUN_ID/events?since=-1" \
  -H "Origin: https://www.acme.com" -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Accept: text/event-stream"

# event: run.started
# data: {"type":"run.started","run_id":"…","status":"running"}
#
# event: tool.called
# data: {"type":"tool.called","idx":1}
#
# event: tool.result
# data: {"type":"tool.result","idx":1,"ok":true}
#
# event: llm.delta
# data: {"type":"llm.delta","idx":2,"text":"Use the "}
#
# event: llm.delta
# data: {"type":"llm.delta","idx":2,"text":"React wrapper…"}
#
# event: run.completed
# data: {"type":"run.completed","status":"succeeded","text":"Use the React wrapper…"}
POST/v1/embed/runs/:id/cancel202 AcceptedCancel the visitor’s own in-flight run (best effort).
Auth
Session token + matching Origin
Rate limit
per-visitor read bucket (60/min)
Errors
EMBED_UNAUTHORIZED EMBED_ORIGIN_MISMATCH EMBED_ORIGIN_DENIED EMBED_KEY_DISABLED RATE_LIMITED NOT_FOUND VALIDATION

Response

FieldTypeDescription
run_iduuidThe run.
statusstringRun status at the time of the request.
Shell
curl -X POST https://console.agenticworkforce.me/v1/embed/runs/$RUN_ID/cancel \
  -H "Origin: https://www.acme.com" -H "Authorization: Bearer $SESSION_TOKEN"

# → 202  { "run_id": "…", "status": "running" }
POST/v1/embed/voice/ticket201 CreatedMint a single-use, 60-second voice ticket for the widget’s hands-free / push-to-talk WebSocket. A browser socket cannot send headers, so the ticket — never the session token — travels as ?ticket= on the upgrade.
Auth
Session token + matching Origin
Rate limit
per-visitor read bucket (60/min)
Errors
EMBED_UNAUTHORIZED EMBED_ORIGIN_MISMATCH EMBED_ORIGIN_DENIED EMBED_KEY_DISABLED VOICE_DISABLED RATE_LIMITED NOT_FOUND VALIDATION

Request

FieldTypeDescription
thread_iduuid, optionalContinue this owned thread; omitted ⇒ the first voice turn opens a new one.

Response

FieldTypeDescription
ticketstringOpaque signed ticket — spend it once.
expires_inintegerLifetime in seconds (60).
urlstring | nullThe WebSocket URL (wss://…/v1/embed/voice); null when the deployment has no public API origin — derive it from apiUrl then.
  • Refused with 403 VOICE_DISABLED unless GET /v1/embed/config reports an effective voice.ptt or voice.handsfree of true (deployment ∧ tenant ∧ agent manifest ∧ key — most-restrictive wins).
GET/v1/embed/voiceThe voice WebSocket (?ticket=): one long-lived socket carrying many utterances and many governed turns — 20 ms PCM16 16 kHz frames up, the server’s energy endpointer decides end of turn, the run executes on the worker like every other embed message, synthesized sentences stream back as binary frames with agent.delta captions.
Auth
Single-use voice ticket + Origin matching the session origin
Rate limit
one open voice socket per visitor and VOICE_EMBED_MAX_SESSIONS_PER_KEY per key (429 VOICE_BUSY); uplink byte-rate cap; 60 s no-speech idle close; per-agent session length cap
Errors
EMBED_UNAUTHORIZED VOICE_TICKET_USED EMBED_ORIGIN_MISMATCH EMBED_KEY_DISABLED VOICE_BUSY VOICE_SESSION_PROTOCOL VOICE_HANDSFREE_DISABLED VOICE_DISABLED VOICE_RATE NOT_FOUND AGENT_NOT_PUBLISHED VALIDATION
  • Control frames are JSON text (hello, speech_start, speech_end, commit, barge_in {heard_chars?}, mute, text, bye); audio frames are binary. The server answers with ready, state, utterance.committed, transcript, turn.started, agent.delta, audio_start + binary audio, a sentence {index, chars} marker before each sentence’s audio (chars = cumulative reply characters — what a client reports back as barge_in.heard_chars), replace, resumed, interrupted {heard_chars?}, turn, error.
  • hello.mode: "handsfree" is refused (VOICE_HANDSFREE_DISABLED, socket closed) unless the effective voice.handsfree flag is true; ptt requires voice.ptt.
  • Refusals on the upgrade are plain problem+json responses (the browser sees a failed handshake). Inside the session, error.fatal: false (e.g. VOICE_TRANSCRIBE_FAILED, or VOICE_RATE when the key’s rate_limit_per_min — counted per utterance and per typed text, queued ones included — is spent) keeps the session listening; error.fatal: true is followed by the close.
  • ready.handsfree reports the GRANTED capability (all opt-in layers), not the requested mode; ready.session_id identifies this socket’s voice session. At most one turn runs, and talking over the agent stops its VOICE, never its work. While the agent is THINKING, speech and typed text are queued, never an interruption: each utterance is transcribed at once (utterance.committed + transcript), up to four wait (oldest dropped) and everything queued runs as ONE merged follow-up turn after the current reply. While it SPEAKS, barge_in {heard_chars} (or a typed text) PAUSES the voice while the run keeps working; the server transcribes the interruption and answers resumed for an acknowledgment ("mm-hmm", "okay" and their Arabic equivalents) or "continue" — also after ~2.5 s of nothing — or interrupted {heard_chars} when speech stops for good: "enough" ⇒ the run finishes silently, no new turn; "cancel" ⇒ the run is cancelled; anything else ⇒ the run finishes silently and the words become the next turn. The spoken commands are matched in English and Arabic. barge_in within 300 ms of audio_start is ignored (echo guard). A spoken sentence that trails off on a connector ("and", "focusing on", or the Arabic counterparts) is held up to 1.5 s (3 s max) for the rest; typed text is never held. mute never cancels a run. turn.barged_in is true whenever the voice was paused or stopped; turn.heard_chars says how much was heard. turn.error carries the error code only (never a provider message).
  • Deltas are withheld — and only the governed final text is spoken — when the agent has output guardrails, exactly like the SSE relay.
OPTIONS/v1/embed/*204 No ContentCORS preflight. Reflects the request origin; enforcement happens on the actual request (fail-closed: no Access-Control-Allow-Origin on denial).
Auth
none
Shell
curl -i -X OPTIONS https://console.agenticworkforce.me/v1/embed/session \
  -H "Origin: https://www.acme.com" -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type"

# HTTP/1.1 204 No Content
# access-control-allow-origin: https://www.acme.com
# access-control-allow-methods: GET, POST, OPTIONS
# access-control-allow-headers: authorization, content-type, x-hive-embed-key
# access-control-max-age: 600
# vary: origin
GET/embed/v1/:assetThe hosted widget bundle: hive-embed.js (script tag, IIFE), hive-embed.mjs (ESM), react.mjs (React wrapper; react stays external), hive-embed-voice.js (the voice UI, loaded lazily on the first mic tap), hive-embed-avatar.js (the talking face, loaded when the key shows one) and hive-embed-avatar3d.js (the 3D renderer, loaded only for a 3D face).
Auth
none (public, Access-Control-Allow-Origin: *)
Errors
NOT_FOUND EMBED_BUNDLE_UNAVAILABLE
  • Cache-Control: public, max-age=3600, stale-while-revalidate=86400, ETag (304 on If-None-Match), X-Content-Type-Options: nosniff.
  • /embed/v1/ is the major line; patches update it in place; breaking changes ship at /embed/v2/.
Shell
curl -I https://console.agenticworkforce.me/embed/v1/hive-embed.js

# HTTP/1.1 200 OK
# content-type: application/javascript; charset=utf-8
# cache-control: public, max-age=3600, stale-while-revalidate=86400
# etag: "…"
# access-control-allow-origin: *
# x-content-type-options: nosniff

SSE event types

GET /v1/embed/runs/:id/events relays a redacted subset of the platform’s run events. Each frame is event: <type> + data: <JSON>; the JSON always repeats type.

typedataMeaning
run.started{ run_id, status }The worker picked the run up.
llm.delta{ idx, text }A streamed chunk of the assistant reply — append to the pending bubble. idx is the step counter the chunk belongs to (see ?since). Withheld when the agent has output guardrails: then only the final text arrives in run.completed.
tool.called{ idx }A tool is running — no name, no arguments. The widget shows a generic "Working…" pill.
tool.result{ idx, ok }The tool finished — success flag only (no output).
run.waiting{}Parked for a human approval (HITL). The widget shows "Waiting for a human…". Decided in the console, never in the widget.
run.resumed{}The approval was decided and the run continues.
run.completed{ status, text }Terminal. text is the final assistant text when present (null otherwise). The stream closes.
run.failed{ code }Terminal. A stable code only — no internal message. The widget shows an error bubble with Retry.
heartbeat{}Every 15 s while the run is live; keeps proxies from closing the stream.

Not relayed — by design, so a widget never sees internals: step.started, llm.thinking, retrieval.result, node.started, node.completed, handoff, tool.progress, plus every payload field beyond the ones listed (no tool names, arguments or output, no prompts, no token or cost figures, no step metadata). idx on step-bound events is the persisted step index — pass the last one you saw as ?since= when reconnecting.

Consuming the stream

TypeScript
// Reading the relay with fetch (what the widget does). EventSource cannot send
// the Authorization header, so it is not supported.
const res = await fetch(`${apiUrl}/v1/embed/runs/${runId}/events?since=-1`, {
  headers: { authorization: `Bearer ${token}`, accept: 'text/event-stream' },
  signal,
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  let idx;
  while ((idx = buffer.indexOf('\n\n')) !== -1) {
    const frame = buffer.slice(0, idx); buffer = buffer.slice(idx + 2);
    const data = frame.split('\n').find((l) => l.startsWith('data:'))?.slice(5).trim();
    if (data) handle(JSON.parse(data)); // { type: 'llm.delta', text } …
  }
}
// Or: import { createSseParser } from '@hive/embed' and feed it chunks.

Reconnect with ?since=<last step idx> to backfill what you missed; the widget retries three times with exponential backoff before reporting STREAM_LOST. A run that is already terminal returns its backfill and closes immediately.

Error codes

HTTP
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json

{
  "type": "about:blank",
  "title": "Origin not allowed for this embed key",
  "status": 403,
  "code": "EMBED_ORIGIN_DENIED",
  "detail": "Add https://evil.example to the embed key's allowed origins."
}
CodeStatusWhereMeaningWhat to do
EMBED_KEY_INVALID401publicUnknown publishable key — it was never issued, was rotated, or was revoked.Copy the current key from the console (Agent → Embed). After a rotation, redeploy the new key.
EMBED_KEY_DISABLED401publicThe key exists but enabled is false (paused) or it is revoked.Re-enable the key in the console, or create a new one.
EMBED_ORIGIN_DENIED403publicThe request Origin is not on the key’s allowlist (or a non-browser client sent no Origin and the key does not allow *). No Access-Control-Allow-Origin header is sent, so browsers report a CORS error.Add the exact origin (https://host[:port], no path) to the key’s allowed origins. Local dev: http://localhost:5173 and http://127.0.0.1:5173 are different origins.
EMBED_ORIGIN_MISMATCH403publicA session token — or a voice ticket on the GET /v1/embed/voice upgrade — is being used from a different origin than it was minted for. A mismatched voice ticket is spent (first use wins).Do not share tokens or tickets across origins; let each page mint its own session and its own ticket right before new WebSocket(...).
EMBED_UNAUTHORIZED401publicMissing, malformed or expired session token (a key cannot be rebound to another agent after creation).Mint a new session (POST /v1/embed/session). The widget does this automatically on 401.
EMBED_IDENTITY_REQUIRED401publicThe key has identity_required and the session request carried no identity JWT.Mint an identity JWT on your backend for signed-in users and pass it as user: { token }.
EMBED_IDENTITY_INVALID401publicThe identity JWT was rejected; reason is one of malformed, alg, signature, expired, claims, exp_too_far, iat_in_future (iat more than 5 min ahead), not_yet_valid (nbf more than 60 s ahead).Sign with HS256 and the key’s CURRENT identity secret; include sub and an exp at most 24 h ahead; keep server clocks in sync; keep meta ≤ 16 keys.
EMBED_RUN_IN_FLIGHT409publicA second message was posted to a thread whose previous run is still queued or running; run_id names the run in flight.Wait for run.completed / run.failed (or cancel the run) before sending the next message. The widget disables the composer while a reply streams.
EMBED_THREAD_FULL409publicThe thread holds the maximum of 200 messages (user + agent).Start a new thread: post the next message without thread_id (the widget offers Reset).
EMBED_THREAD_REQUIRED_FOR_ATTACHMENT400publicAttachment ids were submitted before an owned conversation thread existed.Send the first text turn to create the thread, then upload and attach documents to a later turn.
EMBED_TOO_MANY_STREAMS429publicThis visitor already holds the maximum of 2 live event streams for the key; Retry-After: 5 is set. Finished runs are always served from the backfill without taking a slot.Abort streams you no longer read (AbortController.abort() on the fetch; EventSource cannot be used here) — one open stream per tab is enough.
EMBED_IDENTITY_UNAVAILABLE401publicAn identity JWT was sent but the key has no identity secret configured.Create the key with with_identity_secret: true or issue a secret via POST /v1/embed-keys/:id/identity-secret.
EMBED_BUNDLE_UNAVAILABLE503publicThe API is running but the widget bundle was not built into the deployment.Operators: run pnpm --filter @hive/embed build and restart the API (the API image does this automatically).
INVALID_ATTACHMENT400publicAn attachment does not belong to this conversation, does not exist, or was already sent.Use only fresh attachment ids returned by the upload endpoint for this same thread.
TOO_MANY_FILES413publicA single embedded-chat turn attempted to upload more than eight files.Select no more than eight PNG, JPEG, WebP or PDF files for one turn.
UNSUPPORTED_TYPE415publicAn embedded-chat attachment used a media type outside the accepted set.Upload PNG, JPEG, WebP or PDF files only.
FILE_TOO_LARGE413publicAn embedded-chat attachment exceeded the 20 MB per-file upload limit.Compress or split the document so every uploaded file is at most 20 MB.
NO_FILE400publicThe multipart attachment request contained no file parts to store.Add at least one supported file to the multipart request before submitting it.
RATE_LIMITED429bothA token bucket is empty (per IP on session mint, per key or per visitor on messages). Retry-After and retry_after (seconds) are set.Back off for retry_after seconds. Raise the key’s rate_limit_per_min if the traffic is legitimate.
AGENT_NOT_PUBLISHED409publicThe bound agent has no published version yet.Publish the agent in the console (Agent → Versions) before embedding it.
AGENT_DISABLED409publicThe bound agent is disabled: its identity was suspended or revoked, so it starts no new conversations.An owner or an administrator resumes it on the agent’s Identity tab in the console.
NOT_FOUND404bothUnknown id — or a thread/run that belongs to a different visitor (existence is never revealed).Forget the stored thread_id and start a new thread (the widget does this on 404).
VOICE_DISABLED403publicVoice is not enabled for this widget: the deployment has no voice gateway, the tenant has not allowed hands-free voice, the agent manifest has voice.ptt: false, or the key’s config.voice leaves both handsfree and ptt off (the default).Turn on config.voice.ptt and/or config.voice.handsfree on the embed key (console → Agent → Embed) and check GET /v1/embed/config reports the effective voice flags as true.
VOICE_TICKET_USED401publicThe voice ticket on the WebSocket upgrade was already spent — tickets are single-use (and expire after 60 s).Mint a fresh ticket (POST /v1/embed/voice/ticket) right before every new WebSocket(...); the widget does this on each connect.
VOICE_BUSY429publicA voice socket is already open for this visitor (one microphone per visitor), or the embed key reached its concurrent voice-session ceiling.End the other voice session first; for busy sites raise VOICE_EMBED_MAX_SESSIONS_PER_KEY on the deployment.
VOICE_HANDSFREE_DISABLED403publicThe hello asked for mode: "handsfree" but continuous listening is not granted for this widget: the tenant has not allowed hands-free voice, the agent manifest has voice.handsfree.enabled: false (the default), or the key’s config.voice.handsfree is off. Sent as a fatal error frame, then the socket closes (1008).Check GET /v1/embed/config — voice.handsfree must be true — or open the session with mode: "ptt" (push-to-talk) when only voice.ptt is on. ready.handsfree tells the client which it was granted.
VOICE_GLASSES_DISABLED403publicThe hello asked for surface: "glasses". The widget socket never grants it: the glasses surface belongs to the smart-glasses companion app on the console voice session (deployment voice ready, tenant allow_voice_glasses, manifest voice.glasses.enabled). Sent as a fatal error frame, then the socket closes (1008).Leave surface out of the widget’s hello.
VOICE_PROTOTYPE_DISABLED403publicThe hello asked for surface: "prototype". The widget socket never grants it: the prototype surface is the prototype builder’s Advisor on the console voice session (manifest voice.prototype.enabled). Sent as a fatal error frame, then the socket closes (1008).Leave surface out of the widget’s hello.
VOICE_RATE429publicInside a voice session. Non-fatal (error.fatal: false): the key’s rate_limit_per_min is spent — every committed utterance and every typed text counts one — so that utterance was dropped and the session keeps listening. Fatal (error.fatal: true, then close 1008): audio arrived faster than 1.5× real time (a client flooding frames rather than streaming a microphone).Non-fatal: wait a moment and speak again (the budget refills continuously). Fatal: send 20 ms PCM16 frames as they are captured; never replay a buffered file faster than real time.
VOICE_SESSION_PROTOCOL400publicThe voice WebSocket received a frame outside the protocol: audio before hello, a malformed control message, a second hello, or an agent_id that does not match the ticket.Send one hello first (protocol: 1, codec: pcm16, sample_rate: 16000, frame_ms: 20), then 640-byte PCM16 frames and the documented control messages only.
NAME_TAKEN409adminA live embed key with this name already exists for the agent.Pick another name or revoke the old key first.
VALIDATION400bothThe body or query failed schema validation; detail lists path: message pairs.Compare with the request tables on this page (content 1–8000 chars, origins without paths, hex colors, …).
INTERNAL500bothAn unexpected server-side failure — an unhandled exception, or a broken invariant such as the end-user upsert on session mint returning no row. The body carries no detail on purpose.Retry once after a short delay; if it persists, report the time, route and visitor id to support — nothing on your side triggers this code.

Admin endpoints — /v1/embed-keys

Tenant-admin operations on embed keys. Authenticate with a platform API key (Authorization: Bearer hive_…) and the tenant (X-Tenant-Id); see API keys. The console’s Embed tab uses exactly these routes. Every mutation is audited.

GET/v1/embed-keysList embed keys ({ items: EmbedKey[] }) — never the hash or the identity secret.
Auth
API key (Authorization: Bearer hive_…) + X-Tenant-Id, tenant admin
Errors
VALIDATION

Request

FieldTypeDescription
agent_idquery, uuid, optionalFilter by agent.
include_revokedquery, boolean, optionalInclude revoked keys (default live only).

Response

FieldTypeDescription
iduuidKey id (stable across rotations).
agent_iduuidThe one agent this key can talk to.
namestringDisplay label, unique per agent among live keys.
key_prefixstringhive_pk_ab12cd34… — the only part of the key ever shown again.
allowed_originsstring[]Normalized origins (https://app.acme.com); * = any.
themeEmbedThemeServer-side theme.
configEmbedDisplayConfigServer-side display config.
identity_requiredbooleanRefuse anonymous sessions.
has_identity_secretbooleanWhether an identity secret exists (the secret itself is never returned).
rate_limit_per_mininteger 1–10000Messages per minute across all visitors of the key (default 60).
enabledbooleanKill switch; takes effect on the next request, sessions included.
last_used_atISO date-time | nullBumped on session mint (throttled to once a minute).
revoked_atISO date-time | nullSet by revoke; revoked rows are kept for audit.
created_at / updated_atISO date-timeTimestamps.
Shell
curl "https://console.agenticworkforce.me/v1/embed-keys?agent_id=$AGENT_ID" \
  -H "Authorization: Bearer $HIVE_API_KEY" -H "X-Tenant-Id: $TENANT_ID"
POST/v1/embed-keys201 CreatedCreate a key. Returns the plaintext key (and identity_secret when requested) exactly once.
Auth
API key + X-Tenant-Id, tenant admin
Errors
NOT_FOUND NAME_TAKEN VALIDATION

Request

FieldTypeDescription
agent_iduuidAgent to bind (must exist in the tenant).
namestring 1–80Display label.
allowed_originsstring[] ≤ 50Origins like https://app.acme.com (no path), or *. Default [] = nothing allowed.
themeEmbedTheme, optionalServer-side theme.
configEmbedDisplayConfig, optionalServer-side display config.
identity_requiredboolean, default falseRefuse anonymous sessions.
with_identity_secretboolean, default falseAlso mint an identity secret (returned once).
rate_limit_per_mininteger 1–10000, default 60Per-key message budget.
enabledboolean, default trueKill switch.

Response

FieldTypeDescription
iduuidKey id (stable across rotations).
agent_iduuidThe one agent this key can talk to.
namestringDisplay label, unique per agent among live keys.
key_prefixstringhive_pk_ab12cd34… — the only part of the key ever shown again.
allowed_originsstring[]Normalized origins (https://app.acme.com); * = any.
themeEmbedThemeServer-side theme.
configEmbedDisplayConfigServer-side display config.
identity_requiredbooleanRefuse anonymous sessions.
has_identity_secretbooleanWhether an identity secret exists (the secret itself is never returned).
rate_limit_per_mininteger 1–10000Messages per minute across all visitors of the key (default 60).
enabledbooleanKill switch; takes effect on the next request, sessions included.
last_used_atISO date-time | nullBumped on session mint (throttled to once a minute).
revoked_atISO date-time | nullSet by revoke; revoked rows are kept for audit.
created_at / updated_atISO date-timeTimestamps.
keystringThe publishable key — shown once.
identity_secretstring, optionalHS256 secret for identity JWTs — shown once, only with with_identity_secret.
Shell
curl -X POST https://console.agenticworkforce.me/v1/embed-keys \
  -H "Authorization: Bearer $HIVE_API_KEY" -H "X-Tenant-Id: $TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "'"$AGENT_ID"'",
    "name": "Marketing site",
    "allowed_origins": ["https://www.acme.com", "http://localhost:5173"],
    "theme": { "primary": "#0f766e", "radius": 14 },
    "config": { "welcome": "Hi! How can we help?", "locale": "auto" },
    "with_identity_secret": true,
    "rate_limit_per_min": 120
  }'

# → 201  { …EmbedKey, "key": "hive_pk_…", "identity_secret": "…" }   (both shown once)
GET/v1/embed-keys/:idRead one key.
Auth
API key + X-Tenant-Id, tenant admin
Errors
NOT_FOUND VALIDATION

Response

FieldTypeDescription
iduuidKey id (stable across rotations).
agent_iduuidThe one agent this key can talk to.
namestringDisplay label, unique per agent among live keys.
key_prefixstringhive_pk_ab12cd34… — the only part of the key ever shown again.
allowed_originsstring[]Normalized origins (https://app.acme.com); * = any.
themeEmbedThemeServer-side theme.
configEmbedDisplayConfigServer-side display config.
identity_requiredbooleanRefuse anonymous sessions.
has_identity_secretbooleanWhether an identity secret exists (the secret itself is never returned).
rate_limit_per_mininteger 1–10000Messages per minute across all visitors of the key (default 60).
enabledbooleanKill switch; takes effect on the next request, sessions included.
last_used_atISO date-time | nullBumped on session mint (throttled to once a minute).
revoked_atISO date-time | nullSet by revoke; revoked rows are kept for audit.
created_at / updated_atISO date-timeTimestamps.
Shell
curl https://console.agenticworkforce.me/v1/embed-keys/$KEY_ID \
  -H "Authorization: Bearer $HIVE_API_KEY" -H "X-Tenant-Id: $TENANT_ID"
PATCH/v1/embed-keys/:idUpdate name, origins, theme, config, identity_required, rate limit or enabled (at least one field).
Auth
API key + X-Tenant-Id, tenant admin
Errors
NOT_FOUND VALIDATION

Request

FieldTypeDescription
namestring, optional
allowed_originsstring[], optionalReplaces the whole list.
themeEmbedTheme, optionalReplaces the whole theme.
configEmbedDisplayConfig, optionalReplaces the whole config.
identity_requiredboolean, optional
rate_limit_per_mininteger, optional
enabledboolean, optionalPause without revoking.

Response

FieldTypeDescription
iduuidKey id (stable across rotations).
agent_iduuidThe one agent this key can talk to.
namestringDisplay label, unique per agent among live keys.
key_prefixstringhive_pk_ab12cd34… — the only part of the key ever shown again.
allowed_originsstring[]Normalized origins (https://app.acme.com); * = any.
themeEmbedThemeServer-side theme.
configEmbedDisplayConfigServer-side display config.
identity_requiredbooleanRefuse anonymous sessions.
has_identity_secretbooleanWhether an identity secret exists (the secret itself is never returned).
rate_limit_per_mininteger 1–10000Messages per minute across all visitors of the key (default 60).
enabledbooleanKill switch; takes effect on the next request, sessions included.
last_used_atISO date-time | nullBumped on session mint (throttled to once a minute).
revoked_atISO date-time | nullSet by revoke; revoked rows are kept for audit.
created_at / updated_atISO date-timeTimestamps.
Shell
curl -X PATCH https://console.agenticworkforce.me/v1/embed-keys/$KEY_ID \
  -H "Authorization: Bearer $HIVE_API_KEY" -H "X-Tenant-Id: $TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{"allowed_origins": ["https://www.acme.com", "https://help.acme.com"], "enabled": true}'
POST/v1/embed-keys/:id/rotateIssue a new publishable key. The old one stops matching immediately; live sessions (bound to the key id) keep working.
Auth
API key + X-Tenant-Id, tenant admin
Errors
NOT_FOUND VALIDATION

Response

FieldTypeDescription
iduuidKey id (stable across rotations).
agent_iduuidThe one agent this key can talk to.
namestringDisplay label, unique per agent among live keys.
key_prefixstringhive_pk_ab12cd34… — the only part of the key ever shown again.
allowed_originsstring[]Normalized origins (https://app.acme.com); * = any.
themeEmbedThemeServer-side theme.
configEmbedDisplayConfigServer-side display config.
identity_requiredbooleanRefuse anonymous sessions.
has_identity_secretbooleanWhether an identity secret exists (the secret itself is never returned).
rate_limit_per_mininteger 1–10000Messages per minute across all visitors of the key (default 60).
enabledbooleanKill switch; takes effect on the next request, sessions included.
last_used_atISO date-time | nullBumped on session mint (throttled to once a minute).
revoked_atISO date-time | nullSet by revoke; revoked rows are kept for audit.
created_at / updated_atISO date-timeTimestamps.
keystringThe new publishable key — shown once.
Shell
curl -X POST https://console.agenticworkforce.me/v1/embed-keys/$KEY_ID/rotate \
  -H "Authorization: Bearer $HIVE_API_KEY" -H "X-Tenant-Id: $TENANT_ID"

# → 200  { …EmbedKey, "key": "hive_pk_NEW…" }
POST/v1/embed-keys/:id/identity-secret(Re)generate the HS256 identity secret. JWTs signed with the old secret stop verifying immediately.
Auth
API key + X-Tenant-Id, tenant admin
Errors
NOT_FOUND VALIDATION

Response

FieldTypeDescription
iduuidKey id.
identity_secretstringThe new secret — shown once. Store it server-side only.
Shell
curl -X POST https://console.agenticworkforce.me/v1/embed-keys/$KEY_ID/identity-secret \
  -H "Authorization: Bearer $HIVE_API_KEY" -H "X-Tenant-Id: $TENANT_ID"

# → 200  { "id": "…", "identity_secret": "…" }
DELETE/v1/embed-keys/:id204 No ContentRevoke (soft delete). Every widget and session using the key stops on its next request.
Auth
API key + X-Tenant-Id, tenant admin
Errors
NOT_FOUND VALIDATION
Shell
curl -X DELETE https://console.agenticworkforce.me/v1/embed-keys/$KEY_ID \
  -H "Authorization: Bearer $HIVE_API_KEY" -H "X-Tenant-Id: $TENANT_ID"

# → 204

With @hive/sdk

The TypeScript SDK wraps the admin endpoints as client.embedKeys:

MethodCallsReturns
list({ agentId?, includeRevoked? })GET /v1/embed-keys{ items: EmbedKey[] }
create(input)POST /v1/embed-keysEmbedKeyCreated (with key, identity_secret?)
get(id)GET /v1/embed-keys/:idEmbedKey
update(id, input)PATCH /v1/embed-keys/:idEmbedKey
rotate(id)POST /v1/embed-keys/:id/rotateEmbedKeyCreated (new key)
rotateIdentitySecret(id)POST /v1/embed-keys/:id/identity-secret{ id, identity_secret }
revoke(id)DELETE /v1/embed-keys/:idvoid
provision.tsTypeScript
import { HiveClient } from '@hive/sdk';

const hive = new HiveClient({
  baseUrl: 'https://console.agenticworkforce.me',
  apiKey: process.env.HIVE_API_KEY!,      // hive_… from the console (API keys)
  tenantId: process.env.HIVE_TENANT_ID!,
});

// Create a key for a new customer site
const created = await hive.embedKeys.create({
  agent_id: AGENT_ID,
  name: 'Customer: Acme',
  allowed_origins: ['https://www.acme.com'],
  with_identity_secret: true,
});
console.log(created.key);              // hive_pk_… — hand to the customer once
console.log(created.identity_secret);  // store in THEIR backend secrets, never in a browser

// Rotate on schedule
const rotated = await hive.embedKeys.rotate(created.id);

// Pause / resume
await hive.embedKeys.update(created.id, { enabled: false });

// Clean up
await hive.embedKeys.revoke(created.id);