Embedded components
Show the Swarmhit inbox, tasks and campaigns inside your own product, each user reaching only the LinkedIn accounts and actions you give them.
Embedded components put live Swarmhit screens inside your product: the inbox, the tasks and the campaigns. Each of your users reaches only the LinkedIn accounts you assign them, and only what you allow them to do. It takes one endpoint on your backend and a few lines on your page.
| Element | What it shows |
|---|---|
<swarmhit-inbox> | The conversations of the user's LinkedIn accounts, with the lead panel. |
<swarmhit-tasks> | Calls to make, and AI comment and voice-note drafts waiting for approval. |
<swarmhit-campaigns> | The campaigns that send only from the user's accounts, with the Analytics, Leads, Sequence, Senders and Config tabs. |
New messages, replies and campaign progress appear live, and every improvement Swarmhit ships reaches your users with nothing to update on your side.
How it works
- Your backend creates a session for one of your users with Create an embedded session, using your API key. It gets back a one-time client secret.
- Your page hands that secret to the element, which signs the user in.
- The element shows the Swarmhit screen for that user.
Your API key never reaches the browser: the page only ever sees a one-time secret. What each user reaches is enforced by Swarmhit's servers, not by hiding buttons, so a user can only ever reach the accounts and actions your backend named for them. Embedded users have no access to your workspace settings, members, billing or LinkedIn account management.
Before you start
- Embedded components turned on for your workspace. Ask us. Until then, creating a session answers
403withcode: feature_disabled. - An API key, created on the Access tokens page in Swarmhit (the API keys card) and stored as a secret on your backend. See Authentication.
- Your LinkedIn account ids, from
GET /accounts. Map each of your users to the accounts they work: a recruiter to their own account, an assistant to their own and the CEO's.
Quickstart
A complete example: an Express server with the session endpoint, and a page that shows the inbox. You need Node 18 or later.
1. Add the session endpoint
The endpoint must know who is signed in from your own session, never from the request body or a query parameter, and decide that user's accounts and permissions on the server. It asks Swarmhit for a fresh secret on every call and never caches it.
import express from 'express';
const app = express();
app.use(express.static('public'));
// Replace with your own login. The user comes from your session, never from the request.
const requireLogin = (req, res, next) => {
req.user = { id: 'recruiter_481', name: 'Jane Doe', swarmhitAccountIds: [process.env.SWARMHIT_ACCOUNT_ID] };
next();
};
app.post('/api/swarmhit-session', requireLogin, async (req, res) => {
const r = await fetch('https://app.swarmhit.com/api/v1/embed/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SWARMHIT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user: { id: String(req.user.id), name: req.user.name },
accountIds: req.user.swarmhitAccountIds,
permissions: ['inbox', 'tasks', 'campaigns.analytics', 'campaigns.leads.manage', 'campaigns.status'],
}),
});
if (!r.ok) {
console.error('Swarmhit session failed', r.status, await r.text());
return res.status(502).send('Could not start the Swarmhit session');
}
const { data } = await r.json();
res.set('Cache-Control', 'no-store').type('text/plain').send(data.clientSecret);
});
app.listen(3000, () => console.log('Open http://localhost:3000'));The request body:
| Field | Required | Meaning |
|---|---|---|
user.id | yes | Your id for the user, up to 200 characters. The same id always maps to the same Swarmhit user. |
user.name | no | Up to 120 characters, shown as the author of what they do in Swarmhit. |
accountIds | yes | 1 to 50 LinkedIn account ids of the workspace that owns the key. |
permissions | yes | What the user may do: see Permissions. |
The response is { "data": { "clientSecret", "expiresAt", "user", "accountIds", "permissions" } }, where permissions lists every permission the session holds, with screens and implied reads spelled out.
2. Add the element to your page
Load embed.js once, place the elements where you want them, and call Swarmhit.init once with a function that asks your backend for a secret.
<!doctype html>
<html>
<body style="margin: 0">
<swarmhit-inbox theme="auto" style="height: 100vh"></swarmhit-inbox>
<script src="https://app.swarmhit.com/embed.js"></script>
<script>
Swarmhit.init({
fetchClientSecret: async () => {
const res = await fetch('/api/swarmhit-session', { method: 'POST', credentials: 'include' });
if (!res.ok) throw new Error('Could not start the Swarmhit session');
return res.text();
},
});
</script>
</body>
</html>- Each element calls
fetchClientSecretwhen it loads, and again by itself when its session ends, so the user is never asked to sign in. - The elements can be in the page before or after
Swarmhit.initruns. fetchClientSecretmay return the secret as text, or an object holding it: the API's parsed JSON (data.clientSecret) or{ clientSecret }.
3. Open the page
npm install express # and @swarmhit/swarmhit-sdk for the SDK version
SWARMHIT_API_KEY=swh_live_... SWARMHIT_ACCOUNT_ID=<an id from GET /accounts> node server.mjsOpen http://localhost:3000. The inbox appears with the conversations of that LinkedIn account, and the element fires swarmhit:ready. Swap the element for <swarmhit-tasks> or <swarmhit-campaigns> to see the other screens. If it shows "Could not connect" instead, see Troubleshooting.
Permissions
permissions is the boundary of what a user can do, not a display setting. Swarmhit's servers check every action and every piece of data an embedded user requests against it, and refuse anything outside it. Screens and tabs the user lacks are not shown.
A screen's name (inbox, tasks, campaigns) grants everything in that screen. An edit or manage permission includes its read.
| Permission | Allows |
|---|---|
inbox | Everything below in the inbox |
inbox.read | Read conversations and the lead panel, mark them read, star them |
inbox.reply | Reply (with attachments and voice notes), mark leads interested, pause or stop their sequence |
tasks | Everything below in tasks |
tasks.read | See calls to make and drafts waiting for approval |
tasks.manage | Log, skip and stop calls; approve, reject and re-draft comments and voice notes |
campaigns | Everything below in campaigns |
campaigns.analytics | The Analytics tab |
campaigns.leads | The Leads tab |
campaigns.leads.manage | Pause, resume, stop and retry leads, mark them interested |
campaigns.sequence | The Sequence tab, read only |
campaigns.sequence.edit | Edit the sequence (messages, voice notes, delays, steps, templates) |
campaigns.senders | The Senders tab, read only |
campaigns.senders.edit | Choose the campaign's senders, among the session's accountIds |
campaigns.settings | The Config tab, read only |
campaigns.settings.edit | Edit the schedule and options, rename the campaign |
campaigns.status | Launch, pause and resume campaigns |
campaigns.create | Create campaigns (blank or from a template) and duplicate them |
campaigns.delete | Delete campaigns and their leads |
Ready-made roles:
| Role | permissions |
|---|---|
| Recruiter who runs campaigns | ["inbox", "tasks", "campaigns.analytics", "campaigns.leads.manage", "campaigns.status"] |
| Manager who builds campaigns | ["inbox", "tasks", "campaigns"] |
| Read-only viewer | ["inbox.read", "tasks.read", "campaigns.analytics", "campaigns.sequence"] |
What a user sees:
- Conversations: those of their
accountIds. - Campaigns: a campaign appears only when every sender it uses is one of their accounts. A campaign that also sends from someone else's account stays hidden, so that account's conversations never show.
- New campaigns: a campaign the user creates sends from their accounts from the start.
- Without
campaigns.sequence: the user still sees each lead's stage, but never the messages.
Look and size
| Attribute | Values | Default |
|---|---|---|
theme | light, dark, or auto (follows the visitor's system setting) | light |
accent | A six-digit hex colour such as #0ea5e9, for buttons, links and highlights | Swarmhit indigo |
campaign | On <swarmhit-campaigns>: the id of a campaign to open | the list |
- Live changes: all three attributes can change at any time without a reload. Switch the theme when your app does:
el.setAttribute('theme', 'dark'). - Sizing: an element is a block that fills the width it is given; you set the height. Give it a fixed height (
height: 720px) or let it fill a flex or grid area (height: 100%,flex: 1). Without one it is 600px tall, and never less than 320px. - Narrow spaces: the screens adapt to the element's own width, not the window's, so they work in a side panel or on a phone.
- No flash: the element stays transparent until its screen has painted, so a dark page never flashes white.
Events and opening a campaign
Events are dispatched on the element and bubble, so you can listen on the element or on document:
| Event | detail | When |
|---|---|---|
swarmhit:ready | {} | The user is signed in and the screen is up. |
swarmhit:error | { message } | The session could not start, or fetchClientSecret failed. |
swarmhit:campaign-created | { campaignId } | The user created a campaign in <swarmhit-campaigns>. |
The elements and the API work on the same data, so you can combine them:
- Open a campaign from your own UI: set
campaign="<id>"on<swarmhit-campaigns>, for example from a link on your job page. - Add leads from your platform: call Add leads to a campaign from your backend. For example, when
swarmhit:campaign-createdfires, attach the job's candidates to the new campaign. The Leads tab and the analytics update live. - React on your backend: subscribe to webhooks such as
message.receivedorcampaign.finished.
document.addEventListener('swarmhit:campaign-created', (e) => {
// Your backend then calls POST /campaigns/{id}/leads with the job's candidates.
fetch(`/api/jobs/${jobId}/swarmhit-campaign`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ campaignId: e.detail.campaignId }),
});
});Sessions
- The secret works once, within 5 minutes. The session it starts lasts 12 hours, then the element asks for a new secret by itself.
- Changing access: your latest session for a user sets what all of their open screens reach. Creating a session with fewer accounts or permissions signs their open screens out, and they sign back in by themselves with the narrower access.
- Signing a user out everywhere: call End a user's embedded sessions when they sign out of your platform, leave your company or lose access. Secrets not used yet stop working and their open screens are signed out within moments.
revokedisfalsewhen that user never had a session.
curl -X DELETE https://app.swarmhit.com/api/v1/embed/users/recruiter_481/sessions \
-H "Authorization: Bearer swh_live_..."With the SDK: await swarmhit.embed.users.revokeSessions('recruiter_481').
Security checklist
- Keep the API key on your backend. Never put it in a page, an app bundle or a log.
- Authenticate your own user before creating a session: the session endpoint sits behind your login.
- Never trust a user id, account ids or permissions sent by the browser. Derive them on your backend from your own data.
- If your site sets a Content-Security-Policy, allow
script-src https://app.swarmhit.comandframe-src https://app.swarmhit.com.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
403 with code: feature_disabled | Embedded components are not on for your workspace yet: contact us. |
400 with code: invalid-request naming accountIds | An id is not a LinkedIn account of the workspace that owns the key. List them with GET /accounts. |
400 naming permissions | permissions is required and must use the names in Permissions. |
| The element shows "Could not connect" after about 20 seconds | No secret arrived: check that Swarmhit.init runs on the page and that fetchClientSecret resolves. swarmhit:error says why. |
| "This session is not valid any more" | The secret was used already or is older than 5 minutes. Return a fresh one on every call and never cache it. |
| "This screen is not part of your access" | The session has no permission for that element's screen. |
| A campaign is missing from the list | It sends from an account the user is not assigned. Assign that account too, or change the campaign's senders. |
| Nothing renders and the console shows a CSP error | Allow https://app.swarmhit.com in script-src and frame-src. |
Go-live checklist
- Embedded components are on for your production workspace.
- The API key is a backend secret, and the session endpoint requires your login.
accountIdsandpermissionscome from your own user data, on the server.fetchClientSecretreturns a fresh secret every time (Cache-Control: no-store).- Your CSP allows
https://app.swarmhit.comfor scripts and frames. - Each element has a height, fixed or filling its container.
- Sessions are revoked when a user loses access.
- You tested with your smallest role: what that user should not see is not there.
Direct actions
Send connection requests, messages, InMails, emails and other one-off actions from a sender account or a mailbox, outside campaigns, with idempotent retries and an approvals queue.
Webhooks
Subscribe to Swarmhit events, verify signed deliveries, and handle every event type with example payloads.