# kasookoo-click-to-call-widget

A drop-in **click-to-call widget** for your website: a floating call button,
dial pad, and in-call window, delivered as a single custom HTML element —
`<kasookoo-dialer>`. Visitors can dial a real phone number and talk to you
over the browser, no app or plugin required.

[npm package](https://www.npmjs.com/package/kasookoo-click-to-call-widget)

Building a custom call UI instead of using `<kasookoo-dialer>`? See
[`kasookoo-sdk`](https://www.npmjs.com/package/kasookoo-sdk) — Kasookoo's
full frontend SDK, for calls, messaging, and more.

## Install

**Via npm** (bundler-based apps — React, Angular, Vue, Svelte, etc.):

```bash
npm install kasookoo-click-to-call-widget
```

```js
import "kasookoo-click-to-call-widget" // registers <kasookoo-dialer>
```

**Via a plain `<script>` tag** (no build step, no npm):

```html
<script type="module" src="https://unpkg.com/kasookoo-click-to-call-widget/dist/kasookoo-dialer.js"></script>
```

## Connecting once

Call `KasookooClickToCall.init()` **once**, anywhere early in your app —
before or after your `<kasookoo-dialer>` tags render, it doesn't matter which
comes first. Every tag on the page automatically shares the one connection
this creates, so tags themselves don't take a publishable key or a caller
name:

```js
import { KasookooClickToCall } from "kasookoo-click-to-call-widget"

await KasookooClickToCall.init({
  publishableKey: "pk_live_...",
  subject: "Website visitor", // a display name for whoever is placing calls — see below
  intentEndpoint: "/my-api/call-intent", // your backend — see below
})
```

- `publishableKey` — the key you get from the admin portal at [admin.kasookoo.ai](https://admin.kasookoo.ai).
- `subject` — a display name for the caller. Any name is fine (e.g.
  `"Website visitor"`, or a logged-in user's name) — it does not need to be
  an email address or match an account. It **does** need to match the
  `subject` your backend passes to
  [`kasookoo-server-sdk`](https://www.npmjs.com/package/kasookoo-server-sdk)'s
  `callIntents.create()` — see [Subject must match your backend's call
  intent](#subject-must-match-your-backends-call-intent) below.
- `intentEndpoint` — a route on **your own backend** that the widget calls
  right before every call it places. See [What your `intentEndpoint`
  receives](#what-your-intentendpoint-receives) below for the exact request
  and the response it expects.

### What your `intentEndpoint` receives

Right before every call — from the dial pad, a static button, or `dial()`, no
per-tag wiring needed — the widget makes this request to your route:

```http
POST /my-api/call-intent HTTP/1.1
Content-Type: application/json

{
  "phoneNumber": "+18005551234",
  "subject": "Website visitor"
}
```

Or, as the equivalent `fetch` call:

```js
await fetch("/my-api/call-intent", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    phoneNumber: "+18005551234",
    subject: "Website visitor",
  }),
})
```

| Field | Value |
|---|---|
| `phoneNumber` | The number being dialed, taken from **whichever tag the visitor just used**: a [static tag](#static-call-this-number-buttons)'s `fixed-number`, or whatever the visitor typed into a [dynamic tag](#quick-start)'s dial pad. Different tags on the same page therefore send different numbers to the same route. |
| `subject` | Always the exact value you passed to `init()` — see [Subject must match your backend's call intent](#subject-must-match-your-backends-call-intent). |

Your route creates a call intent for that number using
[`kasookoo-server-sdk`](https://www.npmjs.com/package/kasookoo-server-sdk) and
responds with the intent id and client secret — see that package for the
backend half of this handoff.

The response is parsed leniently — any JSON shape works as long as the two
values are in there somewhere under a recognizable key (`intentId` /
`intent_id` / `id`, and `clientSecret` / `client_secret` / `secret`,
case- and nesting-insensitive), so `{ intentId, clientSecret }`,
`{ data: { intent_id, client_secret } }`, or any other envelope your route
already returns all work without reshaping it to match. A transient failure
(network error or a 5xx) is retried once before giving up.

**Customizing the request.** Pass a bare URL string (as above), or
`{ url, headers?, credentials?, body? }` if the route needs a bearer token,
another custom header, cookies, or a different request body shape — enough to
point `intentEndpoint` straight at an existing API instead of standing up a
translation route in front of it:

```js
intentEndpoint: {
  url: "/my-api/call-intent",
  headers: () => ({ Authorization: `Bearer ${getMyAppToken()}` }),
  credentials: "include", // send cookies with the request
  // defaults to `{ phoneNumber, subject }` — override when the route
  // expects different field names or extra fixed fields. Once you
  // override `body`, you're responsible for including `subject`
  // yourself if your route needs it — see the note below.
  body: (phoneNumber) => ({ phone_number: phoneNumber }),
}
```

### Subject must match your backend's call intent

The `subject` you pass to `init()` here and the `subject` your backend passes
to `kasookoo-server-sdk`'s `callIntents.create()` must be the **exact same
value** — `init()` requires `subject`, and Kasookoo checks it against the one
your backend used to create the call intent, rejecting the intent if they
don't match. The widget's dial then fails with a `call-error` event (see
[Events](#events)) rather than placing the call.

With the default `intentEndpoint` body, this is handled for you: the widget
always sends `{ phoneNumber, subject }`, using the exact `subject` given to
`init()`, so your route can read it straight off the request body and pass it
through to `callIntents.create()` unchanged — nothing to keep in sync by hand.

**Only if you override `body`** (as shown above) do you take this over
yourself — your override replaces the default entirely, so include `subject`
in it if your route needs it:

```js
intentEndpoint: {
  url: "/my-api/call-intent",
  body: (phoneNumber) => ({ phoneNumber, subject: "Website visitor" }), // same value passed to init() below
}
```

Whatever your route ultimately passes as `subject` to `callIntents.create()`
has to be identical to the `subject` in this widget's own `init()` call.

Calling `init()` more than once is harmless — later calls just resolve to the
same connection the first call set up, so it's safe to call it from a
component that might mount more than once.

## Quick start

```html
<kasookoo-dialer></kasookoo-dialer>
```

That's it — this renders a round dial button. Clicking it opens a popup with
the full dial pad; when a visitor enters a number and presses call, the
widget places the call and shows a floating in-call card with mute/hang-up
controls, automatically.

### A floating, draggable button

By default the button sits wherever the tag is in your page. Add `floating`
to pin it to the bottom-right corner of the screen and let visitors drag it
anywhere — by the small handle above the button, by the bar across the top of
the dial pad, or by the button itself. It stays inside the screen.

```html
<kasookoo-dialer floating></kasookoo-dialer>
```

`floating` applies to dial-pad tags only; it is ignored on a tag with
`fixed-number`.

## Phone number format

The widget dials the number **exactly as it is written** — it doesn't add a
country code and doesn't validate the format. Use the international form with
the country code, for example `+923034606113`. A national number such as
`03034606113` (a leading `0`, no country code) is rejected by the SIP trunk
(`404 Prefix Not Supported`), and the visitor sees the call fail to connect
with the `call-error` code `call_connect_failed`.

To steer visitors toward the right form, set a [`defaultCountryCode`](#config-property)
(prefills the dial pad, e.g. `"+92"`) or a [`placeholder`](#config-property)
that shows an example such as `"+92 300 1234567"`. A static
[`fixed-number`](#static-call-this-number-buttons) is dialed as written, so
write it with its country code.

## Static "call this number" buttons

Set `fixed-number` on a tag to turn it into a button for one specific number
instead of a dial pad. Instead of a dots icon, it shows the number itself as
its label, calls that number directly on click (behind a confirmation modal
by default), and shows a small call icon beside the number on hover:

```html
<!-- Dynamic (default): visitor dials any number -->
<kasookoo-dialer></kasookoo-dialer>

<!-- Static: shows "+1 800 555 1234", confirms, then calls it -->
<kasookoo-dialer fixed-number="+1 800 555 1234"></kasookoo-dialer>
```

Two more attributes only apply to a static tag — both default to on:

```html
<!-- No confirmation modal, no hover icon: one click, dials immediately -->
<kasookoo-dialer
  fixed-number="+18005551234"
  confirm-before-call="false"
  show-call-icon-on-hover="false"
></kasookoo-dialer>
```

The number is dialed exactly as you write it in `fixed-number` (spaces and
formatting are stripped automatically before it's actually dialed) — so
format it however reads best to a visitor.

### Showing a label instead of the number

By default a static tag shows the number itself. Add `label` to show a name
instead — the number is still what gets dialed:

```html
<kasookoo-dialer fixed-number="+18005551234" label="Talk to Sales"></kasookoo-dialer>
```

The label appears on the button, in the confirmation modal, and as the name on
the in-call window. `label` only takes effect together with `fixed-number` —
on a dynamic tag (no `fixed-number`) it's ignored. The `phoneNumber` in
[`call-state`](#events) events is always the real number;
the label is added alongside it as `label`.

## One call at a time

Every `<kasookoo-dialer>` tag on the page — dynamic or static — shares the
same active-call state. If a visitor is already on a call and tries to start
another one from any tag, the widget doesn't place a second call: it fires a
`call-error` event with `code: "call_in_progress"` on the tag they just
clicked, and every other tag's button disables itself for the duration of
the active call.

## Placing a call programmatically

A static tag covers the common case — a button that always calls the same
number, no JavaScript required. For a number that isn't known until runtime
(looked up from an API, chosen per click, etc.), call the element's `dial`
method directly instead:

```html
<kasookoo-dialer id="ctc"></kasookoo-dialer>
<button id="call-support">Call support for my region</button>

<script type="module">
  document.getElementById("call-support").addEventListener("click", async () => {
    const number = await resolveSupportNumberForVisitor() // your own logic
    document.getElementById("ctc").dial(number)
  })
</script>
```

On a tag that has `fixed-number` set, `dial()` ignores whatever you pass it
and always calls that configured number instead — a static tag's whole point
is to only ever call the one number it's configured for.

**Labels work the same way as for a click.** If that static tag also has a
[`label`](#showing-a-label-instead-of-the-number), a `dial()` call shows it as
the name on the in-call window, exactly like clicking the button would:

```html
<kasookoo-dialer id="ctc" fixed-number="+18005551234" label="Call Support"></kasookoo-dialer>

<script type="module">
  document.getElementById("ctc").dial() // call window shows "Call Support"
</script>
```

On a dynamic tag (no `fixed-number`), `dial(number)` has no label to show —
`label` is ignored there — so the call window shows the number.

The in-call window renders the same way regardless of how the call started —
`dial()`, a static button, and a number typed into the dial pad all go
through the same code path.

## The widget instance

`init()` resolves to a `KasookooClickToCall` instance. It deliberately has no way to place a call — only a real user clicking a `<kasookoo-dialer>` tag can do that, so a script on the page can't dial numbers silently. It offers read-only inspection and teardown:

```js
const widget = await KasookooClickToCall.init({ publishableKey: "pk_live_...", subject: "Website visitor", intentEndpoint: "/my-api/call-intent" })

widget.session      // SessionInfo | null — sessionId, subject, organizationId, expiresAt
widget.activeCall   // Call | null — the call currently connecting/connected, if any (see below)
widget.close()      // ends any active call, removes every mounted <kasookoo-dialer>, clears the session
```

### The `Call` object

`activeCall` returns a `Call` — the same object that drives every
`<kasookoo-dialer>`'s built-in call window internally:

| Member | Type | Description |
|---|---|---|
| `roomName` | `string` | Call/room identifier. |
| `callTraceId` | `string` | Stable id for this call's lifecycle — useful for correlating logs/support requests. |
| `remote` | `{ name: string, type?: string, id?: string }` | The other side of the call — `name` is the dialed phone number. |
| `state` | `CallState` | `"connecting" \| "connected" \| "ended"`. |
| `muted` | `boolean` | Whether the local microphone is muted. |
| `setMuted(muted)` | `(muted: boolean) => Promise<void>` | Mutes/unmutes the local microphone. |
| `end()` | `() => Promise<void>` | Hangs up, whether or not the phone number has answered yet. |
| `on("state", cb)` | `(state: CallState) => void` | Fires on every state transition. |
| `on("error", cb)` | `(err: KasookooError) => void` | Fires if something goes wrong on this specific call. |

## Configuration

Simple values go on the element as HTML attributes (`fixed-number`, `label`, `confirm-before-call`, `show-call-icon-on-hover`), and everything else on its `config` property in JavaScript. Every option is listed in the [API Reference](#kasookoo-dialer-attributes).

```js
const dialer = document.querySelector("kasookoo-dialer")
dialer.config = {
  participantName: "Jane (Sales Inquiry)", // shown as the caller's name on the call, defaults to the `subject` given to init()
  defaultCountryCode: "+1",                // prefilled in the dial pad's number field (dynamic tags only)
  placeholder: "Enter phone number",       // dial pad input placeholder (dynamic tags only)
  customCss: ".kasookoo-ctc-trigger--static { background: #7c3aed; }", // see Styling below
}
```

## Events

The element reports what's happening through standard browser
[`CustomEvent`s](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) —
listen with `element.addEventListener(name, handler)`.

| Event | Detail (`event.detail`) | Description |
|---|---|---|
| `call-state` | `{ state: "connecting" \| "connected" \| "ended", phoneNumber: string, label?: string }` | Fires on every step of a call's life, from dialing to hang-up. `phoneNumber` is always the real number dialed; `label` is present only when the call came from a static tag that has one. Every call is outbound — there is no "incoming" state. |
| `call-error` | `{ code: string, message: string }` | Fires when something goes wrong — a failed call, a denied microphone permission, a call already in progress (`code: "call_in_progress"`), etc. If `code` is `"missing_scope"`, your publishable key isn't provisioned for calling; if it is `"invalid_sip_configuration"`, your organization's SIP trunk isn't set up — see the error table below. |
| `session-expired` | `{ reason: string }` | Fires if the widget's session expires mid-use; reload the page to recover. |

```js
dialer.addEventListener("call-error", (e) => {
  // e.detail = { code: string, message: string }
  console.warn("something went wrong:", e.detail.message)
})
```

Common `code` values:

| Code | Meaning |
|---|---|
| `call_in_progress` | Another call is already active — end it first. |
| `microphone_permission_denied` | The visitor denied (or dismissed) the microphone permission. |
| `microphone_not_found` / `microphone_in_use` / `microphone_unavailable` | No microphone found, it's busy in another app, or the browser context doesn't allow microphone access. |
| `missing_scope` | Your publishable key isn't provisioned for calling — contact Kasookoo. |
| `invalid_sip_configuration` | Your organization's SIP trunk isn't ready. The message says which: `SIP trunk not configured` (no outbound SIP trunk set up) or `SIP trunk not enabled` (configured but not yet provisioned). Fix it in the admin portal at [admin.kasookoo.ai](https://admin.kasookoo.ai) or contact Kasookoo. |
| `invalid_params` | A required value (e.g. the phone number) is missing or invalid. |
| `missing_call_intent` | Your `intentEndpoint` responded without an intent id and client secret. |
| `call_start_failed` / `invalid_call_tokens` | Kasookoo couldn't start the call, or its response was missing what the widget needs to join it. |
| `call_connect_failed` | The widget couldn't connect to the call's room. A common cause is a number the SIP trunk rejected — usually one without its country code (`03034606113` instead of `+923034606113`). See [Phone number format](#phone-number-format). |
| `network_error` | A network request failed — check the visitor's connection. |
| `not_initialized` | `KasookooClickToCall.init()` hasn't finished yet. |

Errors returned by the Kasookoo API pass their own `code` straight through;
anything else surfaces as `unknown_error`.

`KasookooClickToCall.init()` itself rejects instead of firing `call-error`: with `invalid_config` when `publishableKey`, `subject`, `intentEndpoint` or `callRingTimeoutMs` is missing or invalid, and with `init_failed` when setup fails for any other reason. A failed `init()` can be retried.

## Styling

Set `config.customCss` to a raw CSS string to restyle a tag. It's injected as
a `<style>` element into **that tag's own shadow root**, last — after the
widget's built-in styles — so a rule using one of the class names below beats
the built-in one on specificity ties, no `!important` needed.

```js
dialer.config = {
  customCss: `
    .kasookoo-ctc-trigger--static { background: #7c3aed; border-radius: 8px; }
    .kasookoo-ctc-trigger--static:hover { background: #6d28d9; }
  `,
}
```

If you use Tailwind or another CSS build step, compile it and pass the
resulting CSS text here — this widget's shadow root isn't part of your page's
normal build output, so utility classes from your app's stylesheet can't
reach in on their own.

### What `customCss` reaches — and what it doesn't

Each `<kasookoo-dialer>` tag has its **own** shadow root, and `customCss` is
scoped to that one tag only — set it per-tag if you have several with
different looks. Within one tag's shadow root, it reaches:

- The **trigger button** (the dots icon, or a static number button)
- The **dial pad popup** (dynamic tags) — opened by the trigger button, mounted into the same shadow root
- The **confirmation modal** (static tags only, when `confirm-before-call` isn't disabled)

It does **not** reach the **floating in-call window** (the card with mute/hang-up controls once a call connects). That UI is intentionally shared and page-level — one call window serves the call regardless of which tag placed it — so it renders straight into `document`, outside of any tag's shadow root, and has **no styling hook today**.

### Class reference

**Trigger button** (`kasookoo-ctc-trigger*`)

| Class | Applies to | Notes |
|---|---|---|
| `.kasookoo-ctc-trigger-wrap` | Outer wrapper `<div>` around the button | Positioning context for the popup/modal |
| `.kasookoo-ctc-trigger-wrap--floating` | The wrapper, `floating` tags only | Added *in addition to* `.kasookoo-ctc-trigger-wrap` — fixes the button to the bottom-right; restyle it to move the starting corner |
| `.kasookoo-ctc-handle` | The small drag handle above the button | `floating` tags only; hidden on narrow screens, where the button itself drags |
| `.kasookoo-ctc-trigger` | The button itself | Present on both dynamic and static buttons |
| `.kasookoo-ctc-trigger--static` | The button, static tags only | Added *in addition to* `.kasookoo-ctc-trigger` — target `.kasookoo-ctc-trigger--static` to style only static buttons |
| `.kasookoo-ctc-trigger__number` | `<span>` showing the phone number (or the `label`, when set) | Static tags only |
| `.kasookoo-ctc-trigger__hover-icon` | `<span>` wrapping the call icon that fades in on hover | Static tags only, and only when `show-call-icon-on-hover` isn't disabled |

**Dial pad popup positioning** (`kasookoo-ctc-trigger-popup*`) — dynamic tags only, applied to the popup wrapper based on available screen space:

| Class | Meaning |
|---|---|
| `.kasookoo-ctc-trigger-popup` | Always present on the popup wrapper |
| `.kasookoo-ctc-trigger-popup--above` / `--below` | Opens above or below the trigger button |
| `.kasookoo-ctc-trigger-popup--left` / `--right` | Aligned to the trigger button's left or right edge |

**Confirmation modal** (`kasookoo-ctc-confirm*`) — static tags only, shown when `confirm-before-call` isn't disabled:

| Class | Applies to |
|---|---|
| `.kasookoo-ctc-confirm-overlay` | Full-screen dimmed backdrop |
| `.kasookoo-ctc-confirm` | The modal card |
| `.kasookoo-ctc-confirm__text` | The "Are you sure you want to call …?" text |
| `.kasookoo-ctc-confirm__actions` | Wrapper around the two buttons |
| `.kasookoo-ctc-confirm__btn` | Both buttons |
| `.kasookoo-ctc-confirm__btn--cancel` | Cancel button only (in addition to `__btn`) |
| `.kasookoo-ctc-confirm__btn--confirm` | Call/confirm button only (in addition to `__btn`) |

**Dial pad** (`kasookoo-ctc-dialer*`) — dynamic tags only, the numeric keypad itself:

| Class | Applies to |
|---|---|
| `.kasookoo-ctc-dialer` | The dial pad card |
| `.kasookoo-ctc-dialer__handle` | The drag bar across the top of the dial pad (`floating` tags only) |
| `.kasookoo-ctc-dialer__input-row` | Wrapper around the number field + clear button |
| `.kasookoo-ctc-dialer__input` | The editable number field |
| `.kasookoo-ctc-dialer__backspace` | The clear button — clears the whole number; only shown once a number is entered |
| `.kasookoo-ctc-dialer__pad` | Grid wrapper around the 12 number keys |
| `.kasookoo-ctc-dialer__key` | Each individual key (`0`–`9`, `*`, `#`) |
| `.kasookoo-ctc-dialer__key-digit` | The digit on a key |
| `.kasookoo-ctc-dialer__key-letters` | The small ABC/DEF/… letters under a key |
| `.kasookoo-ctc-dialer__call-row` | Wrapper around the call button |
| `.kasookoo-ctc-dialer__call` | The call button — disabled until a number is entered |

## OpenTelemetry

The widget reports its own logs and traces to Kasookoo so integration problems can be diagnosed. This starts automatically when the optional OpenTelemetry packages are installed, and does nothing when they aren't. It never affects calling.

```bash
npm install @opentelemetry/api @opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base \
  @opentelemetry/exporter-trace-otlp-http @opentelemetry/instrumentation \
  @opentelemetry/instrumentation-fetch @opentelemetry/resources \
  @opentelemetry/semantic-conventions @opentelemetry/sdk-logs \
  @opentelemetry/exporter-logs-otlp-http
```

- Every API request is logged with the action performed (`event`, e.g. `sip_call_make`) and the API called (`api_url`), plus the response body with `token`, `secret` and `password` fields masked.
- Problems that never reach the API — a denied microphone, an expired session, why a call ended — are logged as named events.
- A call is one trace, and its logs, events and requests share one `call_trace_id`.

Kasookoo's own collector always receives this; it can't be turned off or redirected. To also send to a collector of your own:

```js
await KasookooClickToCall.init({
  publishableKey: "pk_live_...",
  subject: "Website visitor",
  intentEndpoint: "/my-api/call-intent",
  telemetry: { endpoint: "https://your-collector.example.com/v1/traces" },
})
```

`telemetry` also takes `logsEndpoint`, `serviceName` and `environment`; they only affect your own collector. This is bundler-only: the plain `<script>` build has no telemetry.

## Framework usage

Every example below reuses the same `intentEndpoint` shown in
[Connecting once](#connecting-once).

### Plain HTML / vanilla JS

```html
<script type="module" src="https://unpkg.com/kasookoo-click-to-call-widget/dist/kasookoo-dialer.js"></script>

<!-- Dynamic: dots icon, opens the dial pad -->
<kasookoo-dialer id="ctc"></kasookoo-dialer>

<!-- Static: shows the number, calls it on click -->
<kasookoo-dialer fixed-number="+18005551234"></kasookoo-dialer>

<script type="module">
  import { KasookooClickToCall } from "https://unpkg.com/kasookoo-click-to-call-widget/dist/kasookoo-dialer.js"

  await KasookooClickToCall.init({ publishableKey: "pk_live_...", subject: "Website visitor", intentEndpoint: "/my-api/call-intent" })

  const dialer = document.getElementById("ctc")
  dialer.config = { defaultCountryCode: "+1" }
  dialer.addEventListener("call-state", (e) => console.log("call state:", e.detail.state))
</script>
```

### React

```tsx
import { useEffect, useState } from "react"
import { KasookooClickToCall } from "kasookoo-click-to-call-widget" // registers <kasookoo-dialer> once

// Call init() once, high in your app (e.g. a top-level effect) — every
// <kasookoo-dialer> tag rendered anywhere afterward shares that connection.
function App() {
  const [ready, setReady] = useState(false)

  useEffect(() => {
    KasookooClickToCall.init({ publishableKey: "pk_live_...", subject: "Website visitor", intentEndpoint: "/my-api/call-intent" }).then(() =>
      setReady(true)
    )
  }, [])

  if (!ready) return null
  return (
    <>
      {/* @ts-expect-error -- custom element, not a typed JSX component */}
      <kasookoo-dialer />
      {/* Static button needs no ref or click handler — the tag is the whole button: */}
      {/* @ts-expect-error -- custom element, not a typed JSX component */}
      <kasookoo-dialer fixed-number="+18005551234" />
    </>
  )
}
```

### Vue

```vue
<template>
  <!-- Dynamic -->
  <kasookoo-dialer ref="dialerEl" @call-state="onCallState" />
  <!-- Static: shows the number, calls it on click -->
  <kasookoo-dialer fixed-number="+18005551234" />
</template>

<script setup>
import { ref, onMounted } from "vue"
import { KasookooClickToCall } from "kasookoo-click-to-call-widget"

const dialerEl = ref(null)
const onCallState = (e) => console.log("call state:", e.detail.state)

onMounted(async () => {
  await KasookooClickToCall.init({ publishableKey: "pk_live_...", subject: "Website visitor", intentEndpoint: "/my-api/call-intent" })
  dialerEl.value.config = { defaultCountryCode: "+1" }
})
</script>
```

Vue treats unrecognized tags with a dash in the name as custom elements
automatically — no extra configuration needed.

### Angular

```html
<!-- Dynamic -->
<kasookoo-dialer #ctc (call-state)="onCallState($event)"></kasookoo-dialer>
<!-- Static: shows the number, calls it on click -->
<kasookoo-dialer fixed-number="+18005551234"></kasookoo-dialer>
```

```ts
// In the module bootstrapping this component:
// schemas: [CUSTOM_ELEMENTS_SCHEMA]
import { KasookooClickToCall } from "kasookoo-click-to-call-widget"

@Component({ /* ... */ })
export class ClickToCallComponent implements OnInit {
  async ngOnInit() {
    await KasookooClickToCall.init({ publishableKey: "pk_live_...", subject: "Website visitor", intentEndpoint: "/my-api/call-intent" })
  }
  onCallState(e: CustomEvent) {
    console.log("call state:", e.detail.state)
  }
}
```

### Svelte

```svelte
<script>
  import { onMount } from "svelte"
  import { KasookooClickToCall } from "kasookoo-click-to-call-widget"

  onMount(() => {
    KasookooClickToCall.init({ publishableKey: "pk_live_...", subject: "Website visitor", intentEndpoint: "/my-api/call-intent" })
  })
</script>

<!-- Dynamic -->
<kasookoo-dialer on:call-state={(e) => console.log("call state:", e.detail.state)} />
<!-- Static: shows the number, calls it on click -->
<kasookoo-dialer fixed-number="+18005551234" />
```

The pattern is the same everywhere: `KasookooClickToCall.init()` is called
once per app, plain text values are HTML attributes, anything more complex
is a JS property set on the element (via `ref`, `:prop`, `[prop]`, or plain
binding, depending on your framework), and anything the widget needs to tell
you comes back as a `CustomEvent`.

## Requirements

- A modern browser (Chrome, Edge, Firefox, Safari — recent versions).
- The page must be served over **HTTPS** (or `localhost` during development)
  — browsers only allow microphone access on secure origins.
- The visitor will be prompted to allow microphone access the first time
  `init()` runs; a call cannot be placed without it.

## What this widget does not do

- It does not receive incoming calls — it only places outbound calls to
  phone numbers.
- It does not offer a way to swap in your own dial pad or call window design
  — both are fixed, built-in UI (though a static tag's trigger button and
  dial pad can be restyled — see [Styling](#styling)).
- It does not support call hold, DTMF tones (in-call keypad input), or
  choosing a specific microphone device.

## API Reference

### `KasookooClickToCall.init(config)`

Called once per app, before or after your `<kasookoo-dialer>` tags render.

| Option | Type | Default | Description |
|---|---|---|---|
| `publishableKey` | `string` | *required* | The key you get from the admin portal at [admin.kasookoo.ai](https://admin.kasookoo.ai). |
| `subject` | `string` | *required* | Display name for whoever is placing calls. Any name works — not required to be an email or match an account. |
| `intentEndpoint` | `string \| { url, headers?, credentials?, body? }` | *required* | Your call-intent route. See [What your `intentEndpoint` receives](#what-your-intentendpoint-receives), [Subject must match your backend's call intent](#subject-must-match-your-backends-call-intent), and [`kasookoo-server-sdk`](https://www.npmjs.com/package/kasookoo-server-sdk). |
| `callRingTimeoutMs` | `number \| false` | `60000` | How long an unanswered call rings before it's cancelled automatically. Pass `false` to disable — the call then rings until answered or manually ended. |
| `logging` | `KasookooLoggingConfig` | `-` | Structured JSON console logging. See below. |
| `telemetry` | `KasookooTelemetryConfig` | `-` | OpenTelemetry trace/log export — automatic once the optional peer packages are installed, no config needed. See [OpenTelemetry](#opentelemetry). |

`logging` options:

| Option | Type | Default | Description |
|---|---|---|---|
| `enabled` | `boolean` | `true` | Set to `false` to silence all console logging. |
| `level` | `"debug" \| "info" \| "warn" \| "error"` | `"info"` | Minimum level emitted. |
| `service` | `string` | `"kasookoo-click-to-call-widget"` | Logical service name attached to each log line. |
| `host` | `string` | `window.location.hostname` | Host label attached to each log line. |

Log lines are structured JSON emitted via `console.debug`/`info`/`warn`/`error`. Sensitive fields (tokens, passwords, secrets, etc.) are automatically redacted before logging.

```js
await KasookooClickToCall.init({
  publishableKey: "pk_live_...",
  subject: "Website visitor",
  logging: { enabled: false }, // or e.g. { level: "warn" }
})
```

### `<kasookoo-dialer>` attributes

Plain HTML attributes — values are always strings.

| Attribute | Type | Default | Description |
|---|---|---|---|
| `fixed-number` | `string` | `-` | Presence turns this tag into a static button showing and calling this one number instead of a dial pad. |
| `label` | `string` | `-` | Static tags only (ignored without `fixed-number`). Shown on the button, confirmation modal and in-call window instead of the number. The number is still what's dialed. |
| `confirm-before-call` | `"false"` to disable | on | Static tags only. Shows a confirmation modal before dialing `fixed-number`. |
| `show-call-icon-on-hover` | `"false"` to disable | on | Static tags only. Shows a call icon beside the number on hover. |
| `floating` | boolean | off | Dial-pad tags only (ignored with `fixed-number`). Pins the button to the bottom-right of the screen and makes it draggable. See [A floating, draggable button](#a-floating-draggable-button). |

### `config` property

Set as a JS property (`element.config = {...}`), not an attribute — this is
where anything beyond a plain string goes.

| Prop | Type | Default | Description |
|---|---|---|---|
| `participantName` | `string` | value of `subject` given to `init()` | Caller name shown on the call itself. |
| `defaultCountryCode` | `string` | `-` | Dynamic tags only. Prefilled dialing prefix in the dial pad's number field, e.g. `"+1"`. Purely a starting value — the visitor can edit or clear it. |
| `placeholder` | `string` | `"Enter phone number"` | Dynamic tags only. Placeholder text for the dial pad's number input. |
| `customCss` | `string` | `-` | Raw CSS injected into this tag's own shadow root. See [Styling](#styling). |

### Methods

| Method | Signature | Description |
|---|---|---|
| `dial` | `(phoneNumber?: string) => void` | Places a call directly, bypassing the trigger button and dial pad. On a tag with `fixed-number` set, the argument is ignored and that number is always dialed instead. |
