> For the complete documentation index, see [llms.txt](https://docs.klink.cloud/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.klink.cloud/integrations/voice-sdk-click-to-call-from-any-app.md).

# Voice SDK: Click to Call From Any App

The K-LINK Voice SDK (@klink.cloud/call-sdk) lets you place and receive real phone calls from inside your own web application, using your klink.cloud telephony extension.

### Overview

The K-LINK Voice SDK (`@klink.cloud/call-sdk`) lets you place and receive real phone calls from inside your own web application, using your klink.cloud telephony extension.

It is headless, which means it handles the calling and gives you no interface. You build the interface, so the softphone can look and behave exactly how you want. PBX credentials are never exposed in the browser: the SDK authenticates with an API token and resolves the extension credentials on the server side.

**What you can build with it**

* A custom softphone inside your own CRM, portal or admin panel
* A click to call button in a tool like Airtable, Notion, Retool or a Google Sheet
* Call steps inside a custom workflow, for example call the customer, then log the outcome
* An embedded dialer in an internal app so agents never switch tabs

### Before you start

1. **API token.** Create one in klink.cloud under **Settings > API Tokens**. It looks like `kpt_...`. Treat it like a password. On a public web page it should be delivered by your backend, not written into the page.
2. **A user with telephony permission.** You only need the email address of a klink.cloud user who can already make calls inside app.klink.cloud. Nothing else to configure, and no extension settings to check.
3. **Microphone permission** in the browser.
4. **HTTPS.** Browsers only allow microphone access on secure pages.

### How it works

1. Your page loads the SDK and calls `init()` with the member email and API token.
2. The SDK fetches the webphone credentials and registers with the PBX.
3. You call `sdk.call(number)` from your own button, and audio flows through an `<audio>` element you provide.
4. The call is logged automatically in klink.cloud, with recording, under Call Logs.

### Minimal setup

```html
<audio id="remote" autoplay></audio>
<button id="dial">Call</button>
```

```js
import { KlinkCallSdk } from '@klink.cloud/call-sdk'

const sdk = await KlinkCallSdk.init({
  userEmail: 'agent@yourcompany.com',
  accessToken: 'kpt_...'   // fetch this from your backend
})

// Required, otherwise the call connects but you hear nothing
sdk.attachAudio(document.querySelector('#remote'))

sdk.on('sessionStarted', () => console.log('Call started'))
sdk.on('sessionEnded', ({ cause }) => console.log('Call ended:', cause))

document.querySelector('#dial').onclick = () => sdk.call('+6585900251')
```

That is the whole integration. Everything else is your own design.

### Example: click to call from Airtable

{% embed url="<https://www.loom.com/share/7cd00c75743e404aa58d96dcb95be071>" %}

This is the setup shown in the demo video. It takes a normal Airtable base of contacts and turns it into a working outbound calling tool.

**Step 1. Build a small dialer page**

Create a single web page that loads the Voice SDK and reads the phone number from the URL, for example `https://yourcompany.com/dialer?number=+6585900251`.

```js
const number = new URLSearchParams(location.search).get('number')
sdk.call(number)
```

Host it anywhere you like. Keep the API token behind your backend.

**Step 2. Add a call button in Airtable**

In your contacts table, add a **Button** field, set the action to **Open URL**, and use a formula that appends the row's phone number:

```
CONCATENATE("https://yourcompany.com/dialer?number=", {Phone})
```

**Step 3. Call from the record**

Click the button on any row. A new tab opens and starts dialing that contact straight away. Talk as normal, then close or end the call.

**Step 4. Log the result back in Airtable**

Return to the record and fill in your fields, for example:

* Outcome: Connected
* Notes: Requested a demo, follow up next week
* Next step and date

Save the record. Your call notes now live with the contact in Airtable, while the call itself, including the recording, is stored in the klink.cloud call log.

You can go further and have the dialer page write the outcome back automatically through the Airtable API when the `sessionEnded` event fires.

### Useful events

| Event              | When it fires                  |
| ------------------ | ------------------------------ |
| `ready`            | Engine started, registering    |
| `incoming`         | Inbound call ringing           |
| `sessionStarted`   | Call established or dialing    |
| `sessionEnded`     | Call ended, with the reason    |
| `remoteStream`     | Remote audio is available      |
| `registeredChange` | SIP registration state changed |
| `error`            | Runtime error                  |

### Call controls

`call(number)`, `answer(callId)`, `reject(callId)`, `hangup(callId)`, `hold` and `unhold`, `mute` and `unmute`, `dtmf(callId, tones)`, `attachAudio(element)`, `getCalls()`, `isRegistered`, `destroy()`

### Troubleshooting

| Issue                               | What to check                                                          |
| ----------------------------------- | ---------------------------------------------------------------------- |
| Call connects but there is no sound | You did not call `attachAudio()` or handle the `remoteStream` event    |
| `auth_failed`                       | The API token is wrong, expired or revoked                             |
| `extension_not_found`               | The member email has no telephony extension assigned                   |
| `extension_not_provisioned`         | The extension exists but is not fully set up on the PBX                |
| Nothing happens on click            | Microphone permission was denied, or the page is not served over HTTPS |
| `network_error`                     | The API base URL is unreachable, or the request timed out              |

### Security

* Issue one API token per integration so a leak can be revoked on its own.
* Rotate or revoke tokens any time from **Settings > API Tokens**.
* Never hard code a token in a public page. Serve it from your backend.
* A token can only reach extensions inside its own tenant.

### Reference

Full SDK documentation: <https://developers.klink.cloud/klink-cloudcall-sdk-2202660m0> Example app: <https://github.com/K-Link-Tech/call-sdk>
