☎️ Click2Call — ontwikkelaarsdocumentatie

← Sluiten OpenAPI JSON

Click2Call — embed Callston calling in your own platform

Click2Call lets the people who work in your CRM, helpdesk or back-office make and receive phone calls through Callston without leaving your pages. Two pieces work together:

Outgoing calls are requested server-to-server by your backend. The phone number goes from your database to Callston directly; when the integration policy hides numbers, the employee's browser only ever sees an opaque call reference and the display name you chose.

Base URL of this installation: https://callston.com · SDK: https://callston.com/assets/js/click2call-sdk.js · OpenAPI: /developers/click2call/openapi.json

1. Concepts

TermMeaning
IntegrationOne connection between a Callston account (personal, or a business) and one of your platforms. Has a public integration id (c2c_…), a list of exact allowed origins, a server key and a policy.
Server keyc2ck_… — held by your backend only. Creates widget sessions and requests calls. Shown once when created or rotated; stored hashed. Never put it in a page or a snippet.
Widget tokenc2cw_… — a short-lived (30 min, renewable) credential for one agent on one origin. Your backend obtains it for the logged-in employee and hands it to the page. It can only read that agent's state and send controls.
Launch ticketsingle-use, 10-minute ticket the widget requests to open the call window. Carried in the URL fragment, consumed once by the window, then removed.
Enginea paired call window. One live engine per agent per integration. It registers with the provider under an opaque identity (c2c_<id>) inside the billing owner's provider account.
Call intenta server-side record of a requested outgoing call with the policy frozen in it (number visibility, cap, rate). Delivered only to the authorized engine; consumed once by the provider webhook.
Agent identifierhow your backend names an employee: their Callston email or a stable UUID (integration setting). Identifiers select an existing, enabled, assigned member — they never create anything.

2. Setting up an integration

  1. In Callston open Click2Call (personal dashboard) or Business → Click2Call (owners/admins, or managers with the Configure Click2Call permission).
  2. Create an integration. Copy the server key now — it is shown once. Store it in your backend's secret store.
  3. Add the exact origins of the pages that will embed the widget, e.g. https://crm.example.com (scheme + host + port, no path). Requests from any other origin are refused. Lines that are not origins (a path, no scheme) are not saved — the page says which. While you build and test, you can tick Allow all origins (for testing): any site may then use the integration's widget (your backend still decides who gets a session, and each session works only on the origin it was made for); the integration shows a warning until you switch it off and list your exact origins. Frame-shell mode always needs the exact origin.
  4. Choose the identifier mode (email or UUID). In UUID mode each member's stable Callston UUID is listed on the page; you can also map your own UUID per member.
  5. Set the widget defaults, permissions (outgoing / incoming), caller ID, destination countries, concurrency and duration limits, and number visibility (show / hide / masked).
  1. Businesses: choose who is covered — everyone, selected members, or departments — and whether future members inherit automatically. Per-agent overrides and their effective policy are on the same page.

Rotating a key invalidates the old one immediately. Revoking the key stops your backend; revoking the integration also ends every widget session and call window at once (calls in progress end through the provider).

3. Your backend: sessions

Every page load re-authenticates: your page asks your backend, your backend checks who is logged in and asks Callston for a widget token for that employee.

POST https://callston.com/api/v1/click2call/sessions
Authorization: Bearer c2ck_…
Content-Type: application/json

{ "integration_id": "c2c_…", "agent": { "type": "email", "value": "[email protected]" }, "origin": "https://crm.example.com" }

Response 201:

{ "ok": true, "widget_token": "c2cw_…", "expires_at": 1727612345,
  "agent": { "name": "Dana", "identifier": "[email protected]" },
  "policy": { "outbound": true, "inbound": true, "number_visibility": "hide", "controls": ["mute","dtmf"], "widget": { … }, "max_call_seconds": 3600 },
  "engine": null }

Return the token (and, if you like, policy/engine) to the page. Errors: origin_not_allowed, agent_unknown, agent_ambiguous, agent_not_assigned, agent_disabled, identifier_mode, integration_disabled, platform_disabled.

The widget refreshes its token itself (POST /widget/refresh) before expiry and re-asks your session endpoint once if a token is rejected. Revocation, disabling or un-assigning an agent apply on the next request.

4. Your page: the SDK

<script src="https://callston.com/assets/js/click2call-sdk.js"></script>
<script>
  var widget = CallstonClick2Call.init({
    integrationId: 'c2c_…',
    sessionEndpoint: '/callston/session',        // your backend → returns {widget_token, expires_at, …}
    requestCallEndpoint: '/callston/call',       // your backend → forwards to Callston with the number
    mode: 'persistent-window',
    windowMode: 'tab',                           // optional: 'popup' (default), 'tab' or 'extension' — overrides the integration setting
    position: 'bottom-right',                    // optional visual overrides
    language: 'en'
  });
  document.getElementById('enable-calls').onclick = function () { widget.enableCalls(); }; // inside a click
  widget.on('statechange', function (s) { console.log(s.state, s.call); });
</script>

The SDK posts to sessionEndpoint and requestCallEndpoint with credentials: 'same-origin' and a JSON body; add your CSRF header with headers: { 'X-CSRF-Token': … } if your backend needs one.

Return Callston's answer unchanged from both endpoints — its HTTP status and JSON. The widget reads them: a refusal ({"ok": false, "error": "agent_offline"} with 409, for example) is shown to the agent at once; an accepted call (202) shows Requesting call… until the call window picks it up. An endpoint that always answers 200 "OK" hides Callston's refusals: the widget then waits, and after 30 s says The call did not start.

MethodWhat it does
enableCalls()Must run inside a user gesture (not needed in frame-shell mode). Opens the call window — as a small popup window, a normal browser tab, the embedded shell panel or the browser extension, per the integration's Call window opens as setting or the windowMode init option (see Call window modes below). If a live window already exists it is asked to come to the front — the SDK never opens a replacement. If a popup is blocked the widget shows an "Open in new tab" link. Returns true when a window was opened or found.
requestCall({ contact_reference, … })Posts to your requestCallEndpoint; your backend looks up the number and calls Callston. Resolves with your backend's response.
answer(), decline()Incoming call controls (delivered to the window).
hangup()Ends the current call: the server ends the provider legs and tells the window. Works from any page — and also when the call window lost the call or is gone: whatever the server still holds for the agent is ended at the provider. An unreachable provider is reported as hangup_uncertain, never as a confirmed hangup.
setMuted(bool), sendDigits('123#')When the policy enables the control.
setAvailability(bool)Whether incoming calls may ring this agent.
getState(){ state, engine, call, policy, agent, session }. engine.client is window or extension.
on(event, fn) / offstatechange, callrequested, detached, commandfailed ({ kind, error } — a control the server refused; the widget also shows the reason).
detach()Stops polling and removes the widget. Not a hangup: the call window keeps running.
endSession()Revokes the widget credential (use on logout). The window keeps its own session.

States: init, connecting, offline (no call window), enabling, ready, unavailable, no_mic, call, reconnecting, unauthorized, error. call.status follows the intent lifecycle: queued, claimed, dialing, ringing, connected, completed, rejected, busy, no_answer, canceled, failed, expired; incoming calls report incoming then connected.

Multipage sites include the snippet on every page (the widget re-attaches to the existing window through the server). SPAs call init() once and endSession() on logout. The widget is rendered in a closed Shadow DOM, is keyboard operable, announces status changes to screen readers and switches to RTL for Hebrew and Arabic.

Call window modes

The call window owns the microphone and the audio, so it must live somewhere that survives your page loads. Call window opens as (per integration) picks where:

ModeWhere the calls liveWhat you doBest for
Popup window (default)a small window on the Callston originnothing extramost sites
New browser taba normal tab (agents can pin it)nothing extraagents who lose popups
Frame shellan embedded panel inside a shell page on your site; your pages run in a full-screen frameadd one shell page (below)classic multipage sites that should feel fully integrated, with no second window
Browser extensionthe Callston extension for Chrome / Edge (a hidden background document)nothing extra; agents install the extension onceteams that can install an extension; no window or panel at all

All four use the same backend calls (sessions, calls, hangup) and the same widget. Only where the call engine runs changes.

Frame-shell mode: what you need to do

In frame-shell mode, the agent's browser tab shows a shell page from your site. The shell shows your real pages in a full-screen, same-origin frame and keeps the Callston call window in a small embedded panel. Clicking links, submitting forms and reloading pages inside the frame never touches the call. No popup, no extra tab, and no click is needed to enable calls.

1. Add a shell page to your site, on the same origin as your app, for example https://crm.example.com/callston-shell.html:

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>CRM</title></head>
<body>
<script src="https://callston.com/assets/js/click2call-sdk.js"></script>
<script>
  CallstonClick2Call.shell({
    integrationId: 'c2c_…',
    sessionEndpoint: '/callston/session',      // the same endpoints as init()
    requestCallEndpoint: '/callston/call',
    start: '/crm/'                             // first page to show; ?p=/some/page overrides it
    // headers: { 'X-CSRF-Token': … },  panelPosition: 'left' | 'right',  autoEnable: true
  });
</script>
</body>
</html>

2. Keep init() on your pages as it is. Inside the shell's frame, init() finds the shell and returns the shell's widget (widget.inShell === true): the same methods and events, one session, one widget UI (drawn by the shell, so it does not flicker on page loads). Listeners registered by a page are removed when that page unloads.

3. In Callston, set Call window opens as → Frame shell and enter the Shell page URL (/callston-shell.html or a full URL on the same origin). The shell page's origin must be in the integration's allowed origins; the Callston call window may then be framed by exactly those origins, and by nothing else.

4. Allow same-origin framing of your own pages. Your app pages are shown in a frame of your own shell, so they must not forbid that: use X-Frame-Options: SAMEORIGIN or Content-Security-Policy: frame-ancestors 'self' (not DENY / 'none'). If your pages have a CSP, it also needs frame-src https://callston.com on the shell page (for the call panel), plus the script-src / connect-src entries listed in Deployment notes.

What the shell does for you:

Things to keep outside the shell: login pages that must be top-level (some OAuth/SSO providers refuse to be framed; send those to the top window and come back to the shell), payment pages of third parties, and pages that set top.location. A page that navigates window.top leaves the shell and ends the call's audio.

Browser extension mode

With Call window opens as → Browser extension, the call engine runs inside the Callston extension for Google Chrome and Microsoft Edge, in a hidden background document. There is no window or panel to keep open, calls survive every page load, and the agent answers from the extension's toolbar button or from a desktop notification.

5. Your backend: requesting a call (hidden numbers)

POST https://callston.com/api/v1/click2call/calls
Authorization: Bearer c2ck_…
Idempotency-Key: crm-call-8842
Content-Type: application/json

{ "integration_id": "c2c_…",
  "agent": { "type": "email", "value": "[email protected]" },
  "phone_number": "+14155550101",
  "contact_reference": "crm-8842", "display_name": "Alice Johnson", "display_company": "Northwind",
  "metadata": { "ticket": 4411 } }

Response 202 Accepted — queued, not answered:

{ "ok": true, "request_id": "ci_…", "call_id": "ci_…", "status": "queued",
  "display": { "name": "Alice Johnson", "company": "Northwind", "number": null, "contact_reference": "crm-8842" },
  "expires_at": 1727612405, "server_time": 1727612345 }

What happens: Callston validates the integration, the agent (membership, assignment, enabled, outgoing allowed), the number (E.164, destination countries, do-not-call list), the account (status, funds, concurrency) and that the agent has a ready call window with a microphone; freezes the policy into an intent; the window claims it atomically and connects with nothing but the reference; the provider webhook consumes the intent once and dials the real destination. Double-clicks with the same Idempotency-Key return the same intent; the same key with a different body is rejected (409 idempotency_conflict). An intent that no window picks up within the expiry (default 60 s) becomes expired.

Errors (all { "ok": false, "error": "…", "message": "…" }): agent_offline (409, no live window), agent_not_ready (409, no microphone), agent_busy (409), outbound_not_allowed (403), invalid_phone_number (422), destination_not_allowed / destination_blocked (403), concurrency_limit (429), insufficient_balance (402), no_caller_id (422), idempotency_conflict (409).

Status and hangup:

GET  https://callston.com/api/v1/click2call/calls/{request_id}
POST https://callston.com/api/v1/click2call/calls/{request_id}/hangup

duration is computed from the authoritative answered timestamp and the server clock, never from a browser timer.

6. Incoming calls

Incoming calls follow the account's normal routing: number → IVR → department → queue for businesses; forwarding / dashboard ringing for personal accounts. An agent is routable through Click2Call when their integration and member are enabled, incoming is allowed, and their call window is live, registered, has a microphone and is set Available. The queue then rings the window (client:c2c_<engine>) with the existing ring strategy; busy, decline and no-answer follow the department's timeout, overflow, voicemail and callback rules. Membership is re-validated at accept time. One live window per member; a second window is refused while the first is on a call.

The window and the widget show the department, a safe caller label (a lead name when known) and the number according to the inbound visibility policy. Enabling incoming never enrolls the agent in campaign auto-dialing. Browsers cannot open the call window for an incoming call without a gesture: the agent enables calls once per browser session.

7. Lifecycle, reliability and billing

8. Security

Rate limits: 300 requests/min per IP and 600/min per integration for server-key endpoints; 600/min per IP for widget endpoints. Exceeding them returns 429 rate_limited with Retry-After.

9. Examples

cURL — request a call:

curl -X POST https://callston.com/api/v1/click2call/calls \
  -H "Authorization: Bearer $CALLSTON_SERVER_KEY" -H "Idempotency-Key: crm-8842-$(date +%s)" \
  -H "Content-Type: application/json" \
  -d '{"integration_id":"c2c_…","agent":{"type":"email","value":"[email protected]"},"phone_number":"+14155550101","contact_reference":"crm-8842","display_name":"Alice Johnson"}'

PHP backend — session and call endpoints:

<?php
// /callston/session — the employee is already authenticated by your app
$employee = current_user();
$res = callston('POST', '/api/v1/click2call/sessions', [
    'integration_id' => CALLSTON_INTEGRATION_ID,
    'agent' => ['type' => 'email', 'value' => $employee->email],
    'origin' => 'https://crm.example.com',
]);
header('Content-Type: application/json');
echo json_encode(['widget_token' => $res['widget_token'] ?? null, 'expires_at' => $res['expires_at'] ?? null, 'error' => $res['error'] ?? null]);

// /callston/call — {contact_reference} from the page; the number stays on the server
$contact = contacts_find($_POST['contact_reference']);
$res = callston('POST', '/api/v1/click2call/calls', [
    'integration_id' => CALLSTON_INTEGRATION_ID,
    'agent' => ['type' => 'email', 'value' => $employee->email],
    'phone_number' => $contact->phone, 'contact_reference' => $contact->id, 'display_name' => $contact->name,
], ['Idempotency-Key' => 'crm-' . $contact->id . '-' . time()]);
echo json_encode($res);

function callston(string $method, string $path, array $body, array $headers = []): array {
    $ch = curl_init(CALLSTON_URL . $path);
    $h = ['Authorization: Bearer ' . CALLSTON_SERVER_KEY, 'Content-Type: application/json'];
    foreach ($headers as $k => $v) { $h[] = "$k: $v"; }
    curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POSTFIELDS => $method === 'POST' ? json_encode($body) : null, CURLOPT_TIMEOUT => 15]);
    return json_decode((string) curl_exec($ch), true) ?: ['ok' => false, 'error' => 'unreachable'];
}

Vanilla multipage install: include the two script tags in your shared layout so every page has the widget; put the "Enable calls" button anywhere (it can also be the widget's own button).

SPA (React/Vue/etc.): call init() once when the app mounts after login; keep the returned widget in app state; call endSession() on logout and detach() when unmounting a preview.

Incoming: nothing to integrate — configure inbound routing in Callston and let agents set themselves Available in the widget or the window.

10. Deployment notes

11. Troubleshooting

The widget shows the reason after Calls unavailable; your session endpoint receives the same code from POST /sessions.

Widget showsCauseFix
Browser console: blocked by CORS policy … No 'Access-Control-Allow-Origin' headerThe browser asked Callston whether your page's origin may call the widget API and got no. The origin is not in Allowed origins of any active integration (or the integration is disabled).Add the exact origin (or tick Allow all origins while testing) and make sure the integration is Active. Then hard-refresh the page — browsers cache a refused check for a few minutes.
origin_not_allowedThe page's origin is not in the integration's Allowed origins, or your backend sent a different origin than the page's own.In Callston add the exact origin — scheme + host + port, no path (open the page, run location.origin in the console and copy it). Your backend must pass on the origin the SDK posts, not a fixed or production value and never the full URL. Run the SDK in the top page, not in an iframe from another host.
identifier_modeagent.type is not the integration's Agent identifier mode — or the agent object is missing or shaped differently.Send "agent": { "type": "email", "value": "…" } for email mode, or "type": "uuid" for UUID mode, exactly as in section 3.
agent_unknown / agent_not_assigned / agent_disabledThe value names no active member (email: their Callston login email; UUID: their Callston UUID or the UUID mapped to them), the member is not covered by the integration, or their override switches calls off.Fix the identifier, or the integration's coverage / per-agent settings.
Calls are not connected in this browser (and the Online switch is greyed out)No call engine is connected for this agent: the call window was closed, the extension was never connected, or an old window is still on record (Reconnecting…). The switch drives the engine, so it cannot work.Press Enable calls (in the widget, or your own button calling enableCalls() inside the click). In extension mode a short tab opens, hands over to the extension and closes; the extension then shows Connected.
This extension is not allowed on this server (in the hand-off tab)The agent's extension is not this installation's (another build, or loaded unpacked).Remove it and install it from https://callston.com/click2call/extension.
Waiting for the microphone…The call window or the extension has no microphone permission.Allow the microphone in the window, or on the extension's Settings & microphone page.
Requesting call…, then The call did not startYour call endpoint answered "OK" but no call started: either Callston refused the request and your backend did not pass the refusal on, or Callston accepted it and the agent's call window never picked it up (it was closed, offline, or in another browser — the request expires after 60 s).Return Callston's status and JSON unchanged (see section 4) — the widget then shows the real reason. Check the agent's widget shows Ready before calling, with the call window or extension connected in the same browser.
A popup never opensenableCalls() ran outside a user click (after an await or an AJAX call), or popups are blocked.Call it directly in the click handler; or use windowMode: 'tab' / Browser extension.

12. Endpoint reference

Method & pathCredentialPurpose
POST /api/v1/click2call/sessionsserver keywidget token for one agent + origin
POST /api/v1/click2call/callsserver keycreate a call intent (Idempotency-Key)
GET /api/v1/click2call/calls/{id}server keyintent status
POST /api/v1/click2call/calls/{id}/hangupserver keyend the call
GET /api/v1/click2call/widget/statewidget tokenengine, call, policy, server time
POST /api/v1/click2call/widget/launch-ticketwidget tokensingle-use ticket + window URL
POST /api/v1/click2call/widget/commandswidget tokenhangup, answer, decline, mute, unmute, dtmf, availability, focus (with request_id)
POST /api/v1/click2call/widget/refreshwidget tokennew widget token
POST /api/v1/click2call/widget/revokewidget tokenend the widget session
POST /api/v1/click2call/engine/pairlaunch ticketbecome the agent's engine (internal, used by the window)
POST /api/v1/click2call/engine/voice-token, heartbeat, claim, hangup, closeengine tokeninternal, used by the window and the extension
GET /click2call/window?embed=c2c_…nonethe call window for a frame shell; framable only by that integration's allowed origins
GET /click2call/extensionnoneagent page: install and set up the browser extension

Full schemas: the OpenAPI document.