1import{j as e}from"./query-DsCa_Pyl.js";import{a,S as n,N as o,F as i}from"./index-DTOh3Yy2.js";import"./react-vendor-BCqHlMf_.js";import"./supabase-Bj-H2d61.js";import"./recharts-g6Jqsn6E.js";import"./framer-yhwvsixF.js";import"./tiptap-B6mHXrL0.js";function t({children:r}){return e.jsx("pre",{className:"rounded-lg border border-border bg-muted/40 p-4 overflow-x-auto text-xs leading-relaxed",children:e.jsx("code",{className:"font-mono",children:r})})}function s({children:r}){return e.jsx("h2",{className:"text-xl font-semibold text-foreground mt-10 mb-3",children:r})}function p(){return e.jsxs("div",{className:"min-h-screen bg-background",children:[e.jsx(a,{}),e.jsx(n,{title:"Embed integration API",description:"Sign server-to-server requests against an ACE AI Spark avatar embed: pre-call capacity, per-caller context, call duration and transcripts.",path:"/docs/embed-api"}),e.jsx(o,{}),e.jsxs("main",{id:"main",className:"container mx-auto px-4 py-12 max-w-3xl",children:[e.jsx("h1",{className:"font-display text-3xl font-bold text-foreground",children:"Embed integration API"}),e.jsx("p",{className:"text-muted-foreground mt-3",children:"Embed an avatar in your own product and talk to it from your server: check capacity before a call, cap how long a call may run, pass in context about the caller, and read back the duration and transcript afterwards."}),e.jsxs("p",{className:"text-muted-foreground mt-3",children:["Everything here is optional. A plain ",e.jsx("code",{className:"text-xs",children:"/embed/<key>"})," ","iframe works with none of it."]}),e.jsx(s,{children:"1. Get a secret"}),e.jsxs("p",{className:"text-sm text-muted-foreground",children:["In ",e.jsx("strong",{children:"AI Avatars â Share links"}),", open the link you want to integrate and choose ",e.jsx("strong",{children:"Generate"})," under âIntegration secretâ. It is shown once. Keep it on your server â never in a browser, an app bundle, or a repository."]}),e.jsx("p",{className:"text-sm text-muted-foreground mt-2",children:"Rotating replaces it immediately; revoking stops signed requests while leaving the share link itself working for ordinary visitors."}),e.jsx(s,{children:"2. Signing"}),e.jsxs("p",{className:"text-sm text-muted-foreground",children:["Every signed request uses ",e.jsx("strong",{children:"HMAC-SHA256"}),", hex-encoded, lowercase. Timestamps are Unix ",e.jsx("strong",{children:"seconds"})," and must be within ",e.jsx("strong",{children:"±300s"})," of our clock. There is no nonce."]}),e.jsx(t,{children:`const crypto = require("crypto"); 2 3function sign(secret, payload) { 4 return crypto.createHmac("sha256", secret).update(payload).digest("hex"); 5}`}),e.jsx(s,{children:"3. Start a capped session"}),e.jsxs("p",{className:"text-sm text-muted-foreground",children:["Sign ",e.jsx("code",{className:"text-xs",children:"${embedKey}.${maxSeconds}.${expiresAt}.${sessionRef}"})," ","and build the iframe URL. ",e.jsx("code",{className:"text-xs",children:"sessionRef"})," is yours: any unique string up to 128 characters, one per conversation. Store it before opening the iframe â it is the only key the result and transcript calls look a conversation up by."]}),e.jsx(t,{children:"const sessionRef = crypto.randomUUID();\nconst maxSeconds = 900; // caps this call at 15 min\nconst expiresAt = Math.floor(Date.now() / 1000) + 300; // URL valid for 5 min\n\nconst sig = sign(SECRET, `${EMBED_KEY}.${maxSeconds}.${expiresAt}.${sessionRef}`);\n\nconst url = `https://aceaispark.com/embed/${EMBED_KEY}`\n + `?session_max=${maxSeconds}`\n + `&session_exp=${expiresAt}`\n + `&session_ref=${sessionRef}`\n + `&session_sig=${sig}`\n + `&visitor_name=${encodeURIComponent(firstName)}`; // optional"}),e.jsxs("p",{className:"text-sm text-muted-foreground mt-3",children:["The signed cap can only ",e.jsx("em",{children:"shorten"})," a call â never past the ownerâs configured maximum or what their credit balance affords. The floor is 60 seconds. Adding"," ",e.jsx("code",{className:"text-xs",children:"visitor_name"})," skips the widgetâs âwhatâs your name?â prompt, which you want whenever your own users are already signed in."]}),e.jsx(s,{children:"4. Server-to-server actions"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"One endpoint, four actions. No auth headers â the signature is the authentication."}),e.jsx(t,{children:'POST https://dlgafzwwwugollkfslzu.supabase.co/functions/v1/partner-embed-session\n{ action, embedKey, sessionRef?, context?, ts, signature }\n\nsignature = HMAC(`${embedKey}.${action}.${sessionRef ?? ""}.${ts}`)'}),e.jsxs("p",{className:"text-sm text-muted-foreground mt-3",children:[e.jsx("code",{className:"text-xs",children:"capacity"})," has no sessionRef, so that slot is empty and the two dots stay: ",e.jsx("code",{className:"text-xs",children:"${embedKey}.capacity..${ts}"}),"."]}),e.jsxs("div",{className:"mt-5 space-y-4 text-sm",children:[e.jsxs("div",{children:[e.jsx("p",{className:"font-medium text-foreground",children:"capacity â before you open the iframe"}),e.jsxs("p",{className:"text-muted-foreground",children:["Returns"," ",e.jsx("code",{className:"text-xs",children:"{ active_partner_calls, partner_slots, owner_
5credits_remaining }"}),". Queue the caller when slots are full: the widget only shows a generic error, so this is where you get a real reason."]})]}),e.jsxs("div",{children:[e.jsx("p",{className:"font-medium text-foreground",children:"prime â optional, before the call"}),e.jsxs("p",{className:"text-muted-foreground",children:["Stash a context blob (up to 8,000 characters) for this"," ",e.jsx("code",{className:"text-xs",children:"sessionRef"}),"; the avatar opens knowing it. Consumed once, then deleted."]})]}),e.jsxs("div",{children:[e.jsx("p",{className:"font-medium text-foreground",children:"result â after the call"}),e.jsxs("p",{className:"text-muted-foreground",children:["Returns ",e.jsx("code",{className:"text-xs",children:"{ status, duration_seconds, started_at, ended_at }"}),", or"," ",e.jsx("code",{className:"text-xs",children:'{ status: "not_found" }'}),". Poll until status is no longer ",e.jsx("code",{className:"text-xs",children:"active"}),", then bill or record from"," ",e.jsx("code",{className:"text-xs",children:"duration_seconds"}),"."]})]}),e.jsxs("div",{children:[e.jsx("p",{className:"font-medium text-foreground",children:"transcript â after the call"}),e.jsxs("p",{className:"text-muted-foreground",children:["Returns ",e.jsx("code",{className:"text-xs",children:"{ transcript, conversation_id }"}),", where transcript is a JSON string you parse. While the call is still finishing you get"," ",e.jsx("code",{className:"text-xs",children:"{ transcript: null, pending: true }"})," with HTTP 200 â that is a normal state, not an error, so back off and retry rather than polling hard."]})]})]}),e.jsx(s,{children:"5. Errors"}),e.jsxs("p",{className:"text-sm text-muted-foreground",children:["Real failures are non-200 with an ",e.jsx("code",{className:"text-xs",children:"error"})," field:"," ",e.jsx("code",{className:"text-xs",children:"400"})," invalid_action / missing_embed_key / missing_session_ref, ",e.jsx("code",{className:"text-xs",children:"403"})," unauthorized / stale_timestamp,"," ",e.jsx("code",{className:"text-xs",children:"404"})," invalid_embed_key, ",e.jsx("code",{className:"text-xs",children:"429"})," ","at_capacity, ",e.jsx("code",{className:"text-xs",children:"503"})," partner_sessions_unavailable (no secret configured). Anything carrying ",e.jsx("code",{className:"text-xs",children:"pending"})," or"," ",e.jsx("code",{className:"text-xs",children:"not_found"})," is a state, not a failure."]}),e.jsx(s,{children:"6. Events in the browser"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"The embed posts to the parent window as a call starts and ends:"}),e.jsx(t,{children:`window.addEventListener("message", (e) => { 6 if (e.origin !== "https://aceaispark.com") return; 7 const m = e.data; 8 if (m?.source !== "ace-embed") return; 9 // m.event: "conversation_started" | "conversation_ended" 10 // m.conversationId: our id for this conversation 11});`}),e.jsx("p",{className:"text-sm text-muted-foreground mt-3",children:"Treat these as live UI state â a timer, a âsession in progressâ badge â and not as your record of what happened. A closing tab can drop the final message. The signed actions above are the source of truth."}),e.jsx(s,{children:"Billing"}),e.jsxs("p",{className:"text-sm text-muted-foreground",children:["Calls started through a partner integration bill the ",e.jsx("strong",{children:"embed ownerâs"})," ","credits, exactly like any other embed call. If you resell access, price above your own per-minute c
11ost. Partner-originated calls are also capped so they can never consume every concurrent slot on the account."]}),e.jsxs("p",{className:"text-sm text-muted-foreground mt-10",children:["Questions: ",e.jsx("a",{className:"underline",href:"mailto:[email protected]",children:"[email protected]"}),"."]})]}),e.jsx(i,{})]})}export{p as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.