Salesforce worked example
A real Lightning Web Component + Apex integration — salesforce-confcall — embeds a
live confcall room on a Salesforce record page and writes call outcomes back as Activities. It's
a working Dev-org proof of concept, built against the same embed API documented on this site.
salesforce-confcall sample
(a standalone sfdx project that deploys to a Salesforce org): the Apex broker
ConfcallController.cls, the conferenceRoom Lightning Web Component, and
the backend smoke.sh. They are trimmed for length but not paraphrased.
Architecture
-
conferenceRoom— a Lightning Web Component that renders the Phase 3<conference-room>widget (see Widget) inside an iframe on a record page's utility bar or page layout. -
ConfcallController— an Apex class that creates an embed room and mints a room-scoped host session via the confcall embed API using ansk_key, so the browser never sees the secret. This is exactly the two-call sequence on the overview page:POST /api/embed/rooms→POST /api/embed/sessions. -
Each room lifecycle event (call started, participant joined, call ended) is written back to
the Salesforce record as a completed Activity/Task, via the widget's
conference-joined/conference-participant-joined/conference-meeting-endedevents (see Widget → Events).
1 · De-risk the backend first (scripts/smoke.sh)
Before any Salesforce, the sample proves the key + endpoints standalone — create a room, mint a session, print an embed URL you can open in a browser:
KEY="${1:?pass your sk_test_ key}"
BASE="${2:-https://meet.confcall.app}"
# 1) create embed room
ROOM_JSON=$(curl -sS -X POST "$BASE/api/embed/rooms" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"roomName":"sfdc-smoke"}')
ROOM_ID=$(printf '%s' "$ROOM_JSON" | sed -n 's/.*"roomId":"\([^"]*\)".*/\1/p')
# 2) mint host session token
SESS_JSON=$(curl -sS -X POST "$BASE/api/embed/sessions" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"roomId\":\"$ROOM_ID\",\"externalUserId\":\"sfdc-smoke-user\",\"displayName\":\"Smoke Advisor\",\"role\":\"host\",\"ttlSeconds\":3600}")
TOKEN=$(printf '%s' "$SESS_JSON" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
echo "$BASE/embed/room/$ROOM_ID?token=$TOKEN&name=Smoke+Advisor"
2 · The Apex broker (ConfcallController.cls)
The sk_ secret is read only server-side (from custom metadata) and never reaches the
browser. One Apex round-trip creates an embed room and mints a short-lived host token — exactly
the two embed API calls:
public with sharing class ConfcallController {
@AuraEnabled
public static EmbedSession startSession(Id recordId) {
Confcall_Setting__mdt c = cfg(); // holds Secret_Key__c + Base_Url__c
String base = c.Base_Url__c;
String roomId = createRoom(base, c.Secret_Key__c, 'sfdc-' + recordId);
String token = mintSession(base, c.Secret_Key__c, roomId,
UserInfo.getUserId(), UserInfo.getName(), 'host', 3600);
EmbedSession s = new EmbedSession();
s.roomId = roomId; s.token = token; s.displayName = UserInfo.getName();
return s;
}
private static HttpResponse call(String endpoint, String key, Map<String, Object> payload) {
HttpRequest req = new HttpRequest();
req.setEndpoint(endpoint);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setHeader('X-API-Key', key); // the sk_ key, server-side only
req.setBody(JSON.serialize(payload));
return new Http().send(req);
}
/** The value loop: turn a room lifecycle event into a completed Task on the record. */
@AuraEnabled
public static void logConferenceEvent(Id recordId, String kind, String payload) {
insert new Task(WhatId = recordId, Subject = 'confcall: ' + kind,
Description = payload, Status = 'Completed', ActivityDate = Date.today());
}
}
3 · The Lightning Web Component (conferenceRoom.js)
The LWC calls startSession, then creates the <conference-room>
element with the room id + host token (injection mode → no popup login) and forwards its
CustomEvents back to Apex as Activities:
const session = await startSession({ recordId: this.recordId });
const el = document.createElement('conference-room');
el.setAttribute('room-id', session.roomId);
el.setAttribute('base-url', 'https://meet.confcall.app');
el.setAttribute('auth-token', session.token); // injection mode → no popup login
el.setAttribute('display-name', session.displayName);
el.style.height = '640px'; // iframe is height:100%, needs a resolved height
el.addEventListener('conference-joined', (e) => this._log('Call started', e.detail));
el.addEventListener('conference-participant-joined',(e) => this._log('Participant joined', e.detail));
el.addEventListener('conference-meeting-ended', (e) => this._teardown('Call ended', e.detail, el));
this.template.querySelector('.room').appendChild(el); // lwc:dom="manual" — LWS blocks unknown tags in markup
The widget bundle is loaded from a Static Resource (an external <script> is
blocked in Salesforce), and the custom element is created in JS under
lwc:dom="manual" because Lightning Web Security blocks unknown tags in template markup.
Production hardening (open in RFC-Conference-Room-Embed Phase 5): move the
sk_ key into a Named + External Credential (auto-injected X-API-Key,
encrypted at rest) instead of admin-readable Custom Metadata; reuse one persisted room per record
rather than a fresh room per "Start"; and verify camera/mic inside the Lightning iframe.
Status
Working Dev-org PoC: the LWC render, the Apex-minted host session, and the Activity/Task write-back are built and verified. Named-Credential secret storage, persisted room reuse, and a buyer demo remain open — see RFC-Conference-Room-Embed Phase 5 for the current acceptance criteria.