Building Blocks / CRM Workspace
The Dock (one widget)
One widget for a CRM: click a contact, dial, text, and open inbox.
Drop-in widget. Load
https://webforms.dropcowboy.com/latest/dropcowboy-dock.min.jsand pass it a site token your server mints. Do not rebuild WebRTC calling, the inbox, or the contact panel; the widget already does it.Calling setup comes with the site token. Your page never configures SIP, STUN, or TURN servers; the script does.
When to use this vs REST
Use the Dock when agents work inside your CRM and need to dial, text, and see the inbox without leaving it. Use REST when no person is at a screen, for example sending a follow-up text from a job.
Do not rebuild
- A softphone, call controls, or audio device handling.
- The unified inbox, contact card, timeline, or pipeline board.
- Caller ID selection. Local presence picks the number.
Drop-in
- On your server, trade your API key for a short-lived site token (the key needs
numbers:write):
// Your server. Browser code never sees KEY/SECRET.
app.post('/dropcowboy/token', async function (req, res) {
const r = await fetch('https://api-v2.dropcowboy.com/phone/public/embed/token', {
method: 'POST',
headers: {
'x-key': process.env.DC_KEY,
'x-secret': process.env.DC_SECRET,
'Content-Type': 'application/json'
},
body: JSON.stringify({ site_id: process.env.DC_SITE_ID, scope: ['dialer:webrtc', 'contacts'], ttl_seconds: 900 })
});
const body = await r.json();
if (!r.ok) return res.status(r.status).json(body);
// The API wraps results in data. Hand the browser just the token.
res.json({ token: body.data.token, expires_at: body.data.expires_at });
});
Or test the mint with cURL:
# On your server: trade your API key (needs numbers:write) for a site token. 1 hour max.
# site_id is one UUID per app; generate your own in place of the example.
curl -s -X POST https://api-v2.dropcowboy.com/phone/public/embed/token \
-H "x-key: $KEY" -H "x-secret: $SECRET" \
-H "Content-Type: application/json" \
-d '{"site_id":"00000000-0000-4000-8000-000000000000","scope":["dialer:webrtc","contacts"],"ttl_seconds":900}'
- Load the script, init with the token, and call a method:
<script src="https://webforms.dropcowboy.com/latest/dropcowboy-dock.min.js"></script>
<script>
// Tokens last at most an hour. The Dock calls getToken before expiry and once after a 401.
function getToken() {
return fetch('/dropcowboy/token', { method: 'POST' })
.then(function (r) { return r.json(); })
.then(function (body) { return body.token; });
}
(async function () {
await DropCowboy.dock.init({
token: await getToken(),
getToken: getToken,
from: '+15125550100',
mode: 'floating',
panes: ['dialer', 'messages', 'inbox', 'contacts']
});
DropCowboy.dock.setTheme({ theme: 'LIGHT', primaryColor: '#009efb' });
DropCowboy.dock.addCallEndedListener(function (event) {
console.log('call ended', event.contact_id, event.disposition, event.duration_ms);
});
DropCowboy.dock.addErrorListener(function (event) {
console.warn(event.code, event.message);
});
document.getElementById('contacts').addEventListener('click', function (e) {
var row = e.target.closest('[data-contact-id]');
if (!row) return;
var contactId = row.getAttribute('data-contact-id');
DropCowboy.dock.setContact({ contact_id: contactId });
if (e.target.closest('[data-action="text"]')) {
DropCowboy.dock.text({ contact_id: contactId });
} else {
DropCowboy.dock.dial({ contact_id: contactId });
}
});
})();
</script>
Pin a version with SRI
/latest/ changes whenever we ship. To pin a release, load the same file from /vX.Y.Z/ and add its Subresource Integrity hash. Each release lists its hashes at https://webforms.dropcowboy.com/vX.Y.Z/building-blocks-manifest.json: take the integrity of the entry whose file is dropcowboy-dock.js. It covers dropcowboy-dock.min.js as well, because both names serve the same bytes.
<script src="https://webforms.dropcowboy.com/vX.Y.Z/dropcowboy-dock.min.js"
integrity="sha384-INTEGRITY_FROM_MANIFEST"
crossorigin="anonymous"></script>
Do not put integrity on a /latest/ URL. The file changes under it and the browser will refuse to run it.
JS API / HTML tag
| Method | What it does |
|---|---|
init({ token, getToken, from, mode, panes, dockRole, partnerName, getCampaignsToken }) |
Mounts the dock. getToken resolves to a fresh site token (dialer:webrtc and contacts scopes) from your server; the dock calls it before the token expires and once after a 401, and every pane shares it. from is the business number texts are sent from. mode is 'floating', 'pinned', or 'fill'. panes is dialer, messages, inbox, contacts, pipeline, campaigns. The campaigns pane needs getCampaignsToken, which returns a separate site token with the campaigns scope; without it the pane is hidden. If getCampaignsToken fails, init() still resolves: the error listener gets { code: 'campaigns_token_unavailable', pane: 'campaigns' } and the pane says campaign status is not available and why. <dc-dock> tags already in your page, or added later, get the session once init() resolves unless you set their tokenManager yourself. |
setTheme({ theme, primaryColor }) |
Applies LIGHT/DARK and your accent color to every pane. |
setContact({ contact_id }) | setContact(null) |
Binds the contact the agent has open in your app. The Contacts pane shows that record, and the header offers Call and Text. Until you call it the dock shows an empty state. |
dial('+15125550148' | { contact_id, phone }) |
Opens the dialer pane and places the call. With only contact_id the dock looks up the number. A call that cannot be placed is reported through addErrorListener. |
text('+15125550148' | { contact_id, phone }) |
Opens the messages pane on that conversation and loads earlier texts when contact_id is known. Sends include contact_id so recorded consent for that contact is checked. |
open(pane, { contact, container }) |
Opens a specific pane, optionally binding a contact first. |
setMode('floating' | 'pinned' | 'fill') |
Changes layout at runtime. |
setPanes([pane, ...]) |
Shows or hides nav items. If the active pane is removed, the dock switches to the first remaining pane. |
addCallStartedListener(fn) -> stop |
Fires { number, contact_id, line_id } when a call is placed. The element also dispatches dc-call-started. |
addCallEndedListener(fn) -> stop |
Fires { number, contact_id, line_id, disposition, duration_ms } when a call ends (dc-call-ended). |
addMessageSentListener(fn) -> stop |
Fires { to, contact_id, sms_id } after a text is accepted (dc-message-sent). |
addContactSelectedListener(fn) -> stop |
Fires { contact_id, phone, source } when the agent picks a conversation in the inbox or a card in the pipeline; source is 'inbox' or 'pipeline' (dc-contact-selected). |
addErrorListener(fn) -> stop |
Fires { code, message, reason, pane } (dc-error). code consent_required means the contact has no recorded consent; reason is phone_mismatch, no_granted_consent, opted_out, or contact_dnc. |
addPaneChangeListener(fn) -> stop |
Fires { pane } on nav clicks and open()/dial()/text()/setPanes(). |
addDockToggleListener(fn) -> stop |
Fires { open } when the dock expands or minimizes. |
setToken(token) |
Swaps in a site token you minted yourself. The next request uses it. |
window event 'dc-session-expired' |
Fires when the token expired and could not be refreshed. The dock keeps the call in progress and shows a Retry banner that calls getToken again; without getToken, mint a new token and call setToken(). |
getTokenManager() -> manager | null |
The token manager every pane shares. Pass it as tokenManager to another block's init() that needs the same scopes (for example DropCowboy.contacts) so both refresh together. The other block borrows it and never closes it. |
getCampaignsTokenManager() -> manager | null |
The separate campaigns-scope token manager, or null when getCampaignsToken was not given or failed. |
close() |
Minimizes the dock, drops the session token, and stops refreshing it. Panes never release the token on their own. |
HTML tag:
<dc-dock mode="floating" panes="dialer,messages,inbox,contacts" contact-id="6c7d8e9f-0a1b-4c2d-8e3f-4a5b6c7d8e9f" from="+15125550100" open></dc-dock>
Auth and scopes
Site token. Your API secret stays on your server.
Site token scope: dialer:webrtc contacts. Mint it with POST https://api-v2.dropcowboy.com/phone/public/embed/token using an API key with numbers:write. Tokens last up to one hour.
Limits
- Site tokens last at most one hour (the default). Mint a fresh one per page load or session; do not cache one across users.
- Calls and texts need a connected carrier (BYOC) and prepaid balance. Without them the mint returns 402 or 403 for telephony scopes.
- Calls belong to the Drop Cowboy® user who created the API key that minted the site token. They need no dialer seat, and any number of your users can be on calls at once with tokens from the same key. A token minted from a key with no creator is refused with 403
acting_user_required. - To tell your users apart, pass your own user id as
subwhen you mint the token. Thecontact.call.*webhooks for the call carry it back asexternal_user_id, along withsite_id. - Outbound SMS needs a consent_id captured with Consent Capture for that phone number.
- US carriers deliver business texts only from a number on a registered brand and campaign (10DLC). Until the sending number is registered, a send fails with 400 and
detail.code: sms_registration_required, and the block tells the user to register it. - To text or call back a contact who already gave consent in Drop Cowboy, turn on "Use existing contact consent" on the Building Blocks page (or POST /register/public/building-blocks/contact-consent) and send contactId. The number must be one of that contact's numbers, and opt-outs, STOP, and Do Not Call still block the send. It is off by default.
- The Dock token needs the dialer:webrtc and contacts scopes. Campaign status needs a second site token with the campaigns scope, passed as getCampaignsToken; without it the Campaigns pane stays hidden. If getCampaignsToken fails, init() still succeeds, the error listener gets campaigns_token_unavailable, and the pane says campaign status is not available and why. The pane shows progress only; start and pause campaigns in Drop Cowboy.
<dc-dock>tags you write in your own page get the session once init() resolves, including tags added later. A tag whose tokenManager you set yourself is left alone.- Texting needs from (a business number). Until your app binds a contact with setContact() or dial()/text(), the Contacts and Messages panes show an empty state.
- Transfer and call recording are not available from the Dock yet.
Related REST
Routes are on https://api-v2.dropcowboy.com.
- POST /phone/public/embed/token - mint the site token
- POST /sms - send a text from your server
- GET /contact/public/contacts - look up contacts
- GET /phone/public/sms/thread/{contact_id} - the conversation the Messages pane loads
Full reference: Embed site tokens, Texts API, Contacts API.