SupportCaptain

Partner documentation

Embed SDK

Connect SupportCaptain from your own product so agents only open the inbox while already logged into your app.

Start: enable embed Multi-product setup https://supportcaptain.com/sdk/v1/supportcaptain.js

Embed the agent shared inbox inside your own product (ERP, admin portal, shop backend).

Audience: partner developers connecting another app to a SupportCaptain workspace.
Live SDK: https://supportcaptain.com/sdk/v1/supportcaptain.js
Public guide: https://supportcaptain.com/docs/embed


What you get


Architecture

Your app (agent logged in)
    │
    ├─ Frontend: SupportCaptain.init({ getToken })
    │       └─ iframe → https://supportcaptain.com/embed/mailbox?token=JWT
    │
    └─ Backend: POST /api/embed/sessions
            Authorization: Bearer emb_…
            body: { "email": "agent@firma.dk" }
                    │
                    ▼
            SupportCaptain validates secret + finds workspace member
            returns { token, embed_url, user }

Inside the iframe, the SPA calls /api/* with:

Authorization: Bearer <same embed JWT>

so API calls work even when browsers block third-party session cookies.


1. Enable embed (workspace admin)

In SupportCaptain (full app):

  1. Open Settings → Embed SDK

  2. Enable embed

  3. Add allowed origins (exact scheme + host, no path), e.g.
    https://app.yourproduct.com
    https://admin.firma.dk

  4. Copy:

    • Public key ws_… → safe in frontend
    • Secret emb_…server only
  5. Ensure every agent who will open the embed exists as an active user in this workspace with the same email as in the partner app.

Tip: invite teammates under Settings → Team first, then connect embed.


2. Mint a session token (your backend)

Never call this from browser JavaScript with the secret.

POST https://supportcaptain.com/api/embed/sessions
Authorization: Bearer emb_YOUR_SECRET
Content-Type: application/json
Accept: application/json

{
  "email": "agent@firma.dk",
  "ttl": 900,
  "view": "unassigned"
}
Field Required Notes
email recommended Must match an active workspace member
user_id optional Alternative to email
ttl optional Seconds, 60–3600 (default 900)
view optional e.g. unassigned, mine, all, needs_attention
mailbox_id optional Filter to one mailbox, or omit for all

Success (201):

{
  "token": "eyJ…",
  "expires_in": 900,
  "expires_at": "2026-07-18T14:00:00+00:00",
  "embed_url": "https://supportcaptain.com/embed/mailbox?token=eyJ…&view=unassigned",
  "user": { "id": 42, "name": "Anne Agent", "email": "agent@firma.dk" }
}

Common errors

HTTP Meaning
401 Wrong/missing secret
422 Embed disabled, or no active user for that email
429 Rate limited

Example (PHP)

$ch = curl_init('https://supportcaptain.com/api/embed/sessions');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $embedSecret,
        'Content-Type: application/json',
        'Accept: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'email' => $loggedInAgentEmail,
        'view'  => 'unassigned',
        'ttl'   => 900,
    ]),
]);
$data = json_decode(curl_exec($ch), true);
// return $data to your frontend (token only — never the emb_ secret)

3. Frontend: drop-in SDK

<div id="support-captain" style="height:100vh;min-height:640px;"></div>
<script src="https://supportcaptain.com/sdk/v1/supportcaptain.js"></script>
<script>
  let sc;

  async function mintFromYourBackend() {
    const r = await fetch('/api/your-app/sc-embed-token', { credentials: 'include' });
    if (!r.ok) throw new Error('Mint failed');
    return r.json(); // { token } or full mint response
  }

  async function mount() {
    if (window.SupportCaptain?.destroy) SupportCaptain.destroy();
    sc = await SupportCaptain.init({
      workspaceKey: 'ws_…',           // public key from Settings → Embed
      appUrl: 'https://supportcaptain.com',
      target: '#support-captain',
      view: 'unassigned',
      height: '100%',
      getToken: mintFromYourBackend,
      onEvent: (type, payload) => {
        if (type === 'sc:auth-expired') {
          // Remint and remount (do NOT full-page reload in a tight loop)
          mount().catch(console.error);
        }
        if (type === 'sc:thread-open') {
          console.log('Opened thread', payload.threadId);
        }
      },
    });
  }

  mount().catch(console.error);
</script>

Host page requirements

Requirement Why
Fixed height container (min-height: 640px+) Avoid zero-height iframe
Your origin in SC allowed origins CSP frame-ancestors
getToken hits your backend only Secret stays server-side
Agent email exists in SC Mint returns 422 otherwise

CSP (if you set Content-Security-Policy)

Allow the SDK script and iframe:

script-src … https://supportcaptain.com;
frame-src  … https://supportcaptain.com;
connect-src … https://supportcaptain.com;

Do not set X-Frame-Options on pages that only host the partner app (your site frames SC; SC does not need to frame you).


4. SDK reference

SupportCaptain.init(options)Promise<{ destroy, openThread, setView, iframe }>

Option Type Required Description
workspaceKey string yes ws_… public key
target string | Element yes CSS selector or DOM node
getToken () => Promise<string|object> yes* Returns JWT string or mint JSON
token string yes* Static token (prefer getToken)
appUrl string no Default https://supportcaptain.com
view string no Initial folder view
mailboxId string|number no Initial mailbox filter
height string no CSS height (100%, auto, …)
jobprep 'host' | 'native' no 'host': Jobforberedelse click posts sc:job-prep to the host instead of opening SC's own desk (requires instance feature job_prep). Default 'native'
onEvent (type, payload) => void no postMessage events from iframe

* One of getToken or token is required.

Methods

Method Description
SupportCaptain.init(opts) Mount iframe
SupportCaptain.destroy() Remove iframe and listeners
SupportCaptain.openThread(id) Navigate to a thread
SupportCaptain.setView(view) Change folder view

Events (onEvent / postMessage from iframe)

Type Payload When
sc:ready { view } Shell loaded
sc:thread-open { threadId } Agent opened a conversation
sc:job-prep { threadId } Jobforberedelse clicked while jobprep: 'host' — host should open its own job-prep flow
sc:auth-expired {} API auth failed — remint once
sc:error { message } Recoverable error
sc:resize { height } When height: 'auto'

Host → iframe messages use source: 'supportcaptain-host'.
Iframe → host messages use source: 'supportcaptain'.


5. Open full app (auto-login)

Do not link bare https://supportcaptain.com/mailbox (no session).

Mint a token, then open handoff in a new top-level tab (first-party cookies):

const { token } = await mintFromYourBackend();
const u = new URL('https://supportcaptain.com/embed/handoff');
u.searchParams.set('token', token);
u.searchParams.set('view', 'unassigned');
window.open(u.toString(), '_blank', 'noopener');

Handoff verifies the JWT, logs the agent in, and redirects to /mailbox.


6. Agent matching & multi-product setup

Your app session SupportCaptain
Logged-in agent email Active workspace user with same email
Synthetic / system emails (e.g. platform-admin@…) Map server-side to a real default agent email

Recommended onboarding for a new customer

  1. Create SupportCaptain workspace
  2. Connect Microsoft 365 mailbox(es)
  3. Invite agents (email = their login in your product)
  4. Settings → Embed → enable + add your product origin(s)
  5. Store ws_… + emb_… in your product settings (encrypted)
  6. Embed page mints with the current agent’s email

One SupportCaptain workspace can be connected from multiple partner products (add every origin).
Each partner product stores its own copy of the keys (or you rotate secrets per environment).


7. Security checklist


8. Troubleshooting

Symptom Fix
Blank iframe Origin not in allowed list; CSP frame-src; container height 0
No active workspace member Create/activate SC user with that email
Flash then white / remint loop Host must send mint response token into SDK; update to latest SC (Bearer API). Remint with cooldown, not hard reload every 400ms
Open full app asks for password Use /embed/handoff?token=… not bare /mailbox
401 from /api/* in iframe Ensure iframe loaded with ?token= so page receives embedApiToken (current build)

9. Reference URLs

URL Purpose
GET /sdk/v1/supportcaptain.js Browser SDK
GET /api/embed/config?workspace_key=ws_… Public branding/config
POST /api/embed/sessions Mint JWT (secret)
GET /embed/mailbox?token=… Iframe app
GET /embed/handoff?token=… Top-level auto-login
GET /docs/embed This guide (HTML)

Source of truth also lives in the repo as docs/embed-sdk.md. Workspace admins configure keys under Settings → Embed SDK.