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
- Full HelpScout-style shared inbox (folders, reply, notes, attachments) inside your UI
- Agents identified by email (same address in your app and SupportCaptain)
- Secure short-lived JWT (never put the embed secret in the browser)
- Works in Safari / cross-site iframes (API auth via Bearer JWT, not third-party cookies alone)
- Open full app with auto-login via handoff URL
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):
-
Open Settings → Embed SDK
-
Enable embed
-
Add allowed origins (exact scheme + host, no path), e.g.
https://app.yourproduct.com
https://admin.firma.dk -
Copy:
- Public key
ws_…→ safe in frontend - Secret
emb_…→ server only
- Public key
-
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
- Create SupportCaptain workspace
- Connect Microsoft 365 mailbox(es)
- Invite agents (email = their login in your product)
- Settings → Embed → enable + add your product origin(s)
- Store
ws_…+emb_…in your product settings (encrypted) - 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
-
emb_…only on the server - Allowed origins exact match (
https://host— no trailing path) - Tokens short-lived (≤ 15 minutes default)
- HTTPS everywhere
- Agents deactivated in SC when they leave the company
- Rotate secret if a server is compromised
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) |
Related
- CRM Context Integration — show orders/contacts from your ERP in the SC sidebar
- REST API — broader API access
- Brugermanual — end-user features