kasookoo-click-to-call-widget 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,549 @@
1
+ # kasookoo-click-to-call-widget
2
+
3
+ A drop-in **click-to-call widget** for your website: a floating call button,
4
+ dial pad, and in-call window, delivered as a single custom HTML element —
5
+ `<kasookoo-dialer>`. Visitors can dial a real phone number and talk to you
6
+ over the browser, no app or plugin required.
7
+
8
+ ## Why a custom element
9
+
10
+ `<kasookoo-dialer>` is a [web component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) —
11
+ a real HTML element the browser understands natively, the same way it
12
+ understands `<video>` or `<select>`. That means it works identically
13
+ everywhere:
14
+
15
+ - Drop it into a plain HTML page with one `<script>` tag, no build step.
16
+ - Drop it into a React, Angular, Vue, or Svelte app — every framework already
17
+ has its own normal way of placing a DOM element on the page and reading
18
+ values off it (a `ref`, a template binding, etc.). Nothing framework-specific
19
+ needs to be installed or maintained.
20
+ - Use as many of them as you want, anywhere in your app — one shared
21
+ connection backs every tag (see [Connecting once](#connecting-once) below).
22
+
23
+ Each tag is entirely self-contained: its own look, its own dial pad, its own
24
+ in-call window. It does not use or expose any of your page's CSS, and your
25
+ page's CSS cannot accidentally break it either.
26
+
27
+ ## Install
28
+
29
+ **Via npm** (bundler-based apps — React, Angular, Vue, Svelte, etc.):
30
+
31
+ ```bash
32
+ npm install kasookoo-click-to-call-widget
33
+ ```
34
+
35
+ ```js
36
+ import "kasookoo-click-to-call-widget" // registers <kasookoo-dialer>
37
+ ```
38
+
39
+ **Via a plain `<script>` tag** (no build step, no npm):
40
+
41
+ ```html
42
+ <script type="module" src="https://unpkg.com/kasookoo-click-to-call-widget/dist/kasookoo-dialer.js"></script>
43
+ ```
44
+
45
+ ## Connecting once
46
+
47
+ Call `KasookooClickToCall.init()` **once**, anywhere early in your app —
48
+ before or after your `<kasookoo-dialer>` tags render, it doesn't matter which
49
+ comes first. Every tag on the page automatically shares the one connection
50
+ this creates, so tags themselves don't take a publishable key or a caller
51
+ name:
52
+
53
+ ```js
54
+ import { KasookooClickToCall } from "kasookoo-click-to-call-widget"
55
+
56
+ await KasookooClickToCall.init({
57
+ publishableKey: "pk_live_...",
58
+ subject: "Website visitor", // a display name for whoever is placing calls — see below
59
+ })
60
+ ```
61
+
62
+ - `publishableKey` — the key you were given for your website/app.
63
+ - `subject` — a display name for the caller. Any name is fine (e.g.
64
+ `"Website visitor"`, or a logged-in user's name) — it does not need to be
65
+ an email address or match an account.
66
+
67
+ Calling `init()` more than once is harmless — later calls just resolve to the
68
+ same connection the first call set up, so it's safe to call it from a
69
+ component that might mount more than once.
70
+
71
+ ## Quick start
72
+
73
+ ```html
74
+ <kasookoo-dialer></kasookoo-dialer>
75
+ ```
76
+
77
+ That's it — this renders a small floating call button. Clicking it opens a
78
+ popup with the full dial pad; when a visitor enters a number and presses
79
+ call, the widget places the call and shows a floating in-call card with
80
+ mute/hang-up controls, automatically.
81
+
82
+ ## Static "call this number" buttons
83
+
84
+ Set `fixed-number` on a tag to turn it into a button for one specific number
85
+ instead of a dial pad. Instead of a dots icon, it shows the number itself as
86
+ its label, calls that number directly on click (behind a confirmation modal
87
+ by default), and shows a small call icon beside the number on hover:
88
+
89
+ ```html
90
+ <!-- Dynamic (default): visitor dials any number -->
91
+ <kasookoo-dialer></kasookoo-dialer>
92
+
93
+ <!-- Static: shows "+1 800 555 1234", confirms, then calls it -->
94
+ <kasookoo-dialer fixed-number="+1 800 555 1234"></kasookoo-dialer>
95
+ ```
96
+
97
+ Two more attributes only apply to a static tag — both default to on:
98
+
99
+ ```html
100
+ <!-- No confirmation modal, no hover icon: one click, dials immediately -->
101
+ <kasookoo-dialer
102
+ fixed-number="+18005551234"
103
+ confirm-before-call="false"
104
+ show-call-icon-on-hover="false"
105
+ ></kasookoo-dialer>
106
+ ```
107
+
108
+ The number is dialed exactly as you write it in `fixed-number` (spaces and
109
+ formatting are stripped automatically before it's actually dialed) — so
110
+ format it however reads best to a visitor.
111
+
112
+ ## One call at a time
113
+
114
+ Every `<kasookoo-dialer>` tag on the page — dynamic or static — shares the
115
+ same active-call state. If a visitor is already on a call and tries to start
116
+ another one from any tag, the widget doesn't place a second call: it fires a
117
+ `call-error` event with `code: "call_in_progress"` on the tag they just
118
+ clicked, and every other tag's button disables itself for the duration of
119
+ the active call.
120
+
121
+ ## Placing a call programmatically
122
+
123
+ A static tag covers the common case — a button that always calls the same
124
+ number, no JavaScript required. For a number that isn't known until runtime
125
+ (looked up from an API, chosen per click, etc.), call the element's `dial`
126
+ method directly instead:
127
+
128
+ ```html
129
+ <kasookoo-dialer id="ctc"></kasookoo-dialer>
130
+ <button id="call-support">Call support for my region</button>
131
+
132
+ <script type="module">
133
+ document.getElementById("call-support").addEventListener("click", async () => {
134
+ const number = await resolveSupportNumberForVisitor() // your own logic
135
+ document.getElementById("ctc").dial(number)
136
+ })
137
+ </script>
138
+ ```
139
+
140
+ On a tag that has `fixed-number` set, `dial()` ignores whatever you pass it
141
+ and always calls that configured number instead — a static tag's whole point
142
+ is to only ever call the one number it's configured for.
143
+
144
+ The in-call window renders the same way regardless of how the call started —
145
+ `dial()`, a static button, and a number typed into the dial pad all go
146
+ through the same code path.
147
+
148
+ ## The widget instance
149
+
150
+ `init()` resolves to a `KasookooClickToCall` instance. It does **not** offer
151
+ any way to place a call from code — a real user clicking a rendered
152
+ `<kasookoo-dialer>` tag is the only way a call gets placed. This is
153
+ deliberate: if placing a call were just a normal method call, any script on
154
+ the page (a compromised dependency, an injected ad script) could silently
155
+ dial numbers with no user interaction and no confirmation. What the instance
156
+ does offer is read-only inspection and teardown:
157
+
158
+ ```js
159
+ const widget = await KasookooClickToCall.init({ publishableKey: "pk_live_...", subject: "Website visitor" })
160
+
161
+ widget.session // SessionInfo | null — sessionId, subject, organizationId, expiresAt
162
+ widget.activeCall // Call | null — the call currently connecting/connected, if any (see below)
163
+ widget.close() // tears the widget down: ends any active call, removes every mounted
164
+ // <kasookoo-dialer> trigger button, clears the session. Safe to call more
165
+ // than once; the instance can't be reused afterward — a later init() call
166
+ // creates a fresh one.
167
+ ```
168
+
169
+ ### The `Call` object
170
+
171
+ `activeCall` returns a `Call` — the same object that drives every
172
+ `<kasookoo-dialer>`'s built-in call window internally:
173
+
174
+ | Member | Type | Description |
175
+ |---|---|---|
176
+ | `roomName` | `string` | Call/room identifier. |
177
+ | `callTraceId` | `string` | Stable id for this call's lifecycle — useful for correlating logs/support requests. |
178
+ | `remote` | `{ name: string, type?: string, id?: string }` | The other side of the call — `name` is the dialed phone number. |
179
+ | `state` | `CallState` | `"connecting" \| "connected" \| "ended"`. |
180
+ | `muted` | `boolean` | Whether the local microphone is muted. |
181
+ | `setMuted(muted)` | `(muted: boolean) => Promise<void>` | Mutes/unmutes the local microphone. |
182
+ | `end()` | `() => Promise<void>` | Hangs up, whether or not the phone number has answered yet. |
183
+ | `on("state", cb)` | `(state: CallState) => void` | Fires on every state transition. |
184
+ | `on("error", cb)` | `(err: KasookooError) => void` | Fires if something goes wrong on this specific call. |
185
+
186
+ ## Configuration
187
+
188
+ Two kinds of settings:
189
+
190
+ - **Simple values** go on the element as plain HTML attributes (kebab-case,
191
+ since HTML attributes can only hold text):
192
+ - `fixed-number` — optional. See [Static "call this number" buttons](#static-call-this-number-buttons).
193
+ - `confirm-before-call` — static tags only, defaults to on. Set to `"false"` to skip the confirmation modal.
194
+ - `show-call-icon-on-hover` — static tags only, defaults to on. Set to `"false"` to hide the hover icon.
195
+
196
+ - **Everything else** (an object) goes on the element's `config` **property**
197
+ in JavaScript, since HTML attributes can't hold objects:
198
+
199
+ ```js
200
+ const dialer = document.querySelector("kasookoo-dialer")
201
+ dialer.config = {
202
+ participantName: "Jane (Sales Inquiry)", // shown as the caller's name on the call, defaults to the `subject` given to init()
203
+ defaultCountryCode: "+1", // prefilled in the dial pad's number field (dynamic tags only)
204
+ placeholder: "Enter phone number", // dial pad input placeholder (dynamic tags only)
205
+ customCss: ".kasookoo-ctc-trigger--static { background: #7c3aed; }", // see Styling below
206
+ }
207
+ ```
208
+
209
+ ## Styling
210
+
211
+ Set `config.customCss` to a raw CSS string to restyle a tag — it's injected
212
+ into that tag's own shadow root, so it only ever affects that one tag's
213
+ trigger button, dial pad, and (static tags) confirmation modal. It does not
214
+ reach the floating in-call window, since that's shared, page-level UI
215
+ independent of any single tag.
216
+
217
+ If you use Tailwind or another CSS build step, compile it and pass the
218
+ resulting CSS text here — this widget's shadow root isn't part of your page's
219
+ normal build output, so utility classes from your app's stylesheet can't
220
+ reach in on their own.
221
+
222
+ ```js
223
+ dialer.config = {
224
+ customCss: `
225
+ .kasookoo-ctc-trigger--static { background: #7c3aed; border-radius: 8px; }
226
+ .kasookoo-ctc-trigger--static:hover { background: #6d28d9; }
227
+ `,
228
+ }
229
+ ```
230
+
231
+ ## Listening for call activity
232
+
233
+ The element reports what's happening through standard browser
234
+ [`CustomEvent`s](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) —
235
+ listen with `addEventListener`, same as any other DOM event:
236
+
237
+ ```js
238
+ const dialer = document.querySelector("kasookoo-dialer")
239
+
240
+ dialer.addEventListener("call-state", (e) => {
241
+ // e.detail = { state: "connecting" | "connected" | "ended", phoneNumber: string }
242
+ console.log("call is now", e.detail.state)
243
+ })
244
+
245
+ dialer.addEventListener("call-error", (e) => {
246
+ // e.detail = { code: string, message: string }
247
+ console.warn("something went wrong:", e.detail.message)
248
+ })
249
+
250
+ dialer.addEventListener("session-expired", () => {
251
+ console.warn("the widget's session expired and needs a page refresh")
252
+ })
253
+ ```
254
+
255
+ `call-state` fires for every step of a call's life: `"connecting"` while it's
256
+ dialing, `"connected"` once answered, `"ended"` when it's over. Every call
257
+ placed by this widget is outbound — there is no "incoming call" state.
258
+
259
+ ## Framework usage
260
+
261
+ ### Plain HTML / vanilla JS
262
+
263
+ ```html
264
+ <script type="module" src="https://unpkg.com/kasookoo-click-to-call-widget/dist/kasookoo-dialer.js"></script>
265
+
266
+ <!-- Dynamic: dots icon, opens the dial pad -->
267
+ <kasookoo-dialer id="ctc"></kasookoo-dialer>
268
+
269
+ <!-- Static: shows the number, calls it on click -->
270
+ <kasookoo-dialer fixed-number="+18005551234"></kasookoo-dialer>
271
+
272
+ <script type="module">
273
+ import { KasookooClickToCall } from "https://unpkg.com/kasookoo-click-to-call-widget/dist/kasookoo-dialer.js"
274
+
275
+ await KasookooClickToCall.init({ publishableKey: "pk_live_...", subject: "Website visitor" })
276
+
277
+ const dialer = document.getElementById("ctc")
278
+ dialer.config = { defaultCountryCode: "+1" }
279
+ dialer.addEventListener("call-state", (e) => console.log("call state:", e.detail.state))
280
+ </script>
281
+ ```
282
+
283
+ ### React
284
+
285
+ ```tsx
286
+ import { useEffect, useState } from "react"
287
+ import { KasookooClickToCall } from "kasookoo-click-to-call-widget" // registers <kasookoo-dialer> once
288
+
289
+ // Call init() once, high in your app (e.g. a top-level effect) — every
290
+ // <kasookoo-dialer> tag rendered anywhere afterward shares that connection.
291
+ function App() {
292
+ const [ready, setReady] = useState(false)
293
+
294
+ useEffect(() => {
295
+ KasookooClickToCall.init({ publishableKey: "pk_live_...", subject: "Website visitor" }).then(() =>
296
+ setReady(true)
297
+ )
298
+ }, [])
299
+
300
+ if (!ready) return null
301
+ return (
302
+ <>
303
+ {/* @ts-expect-error -- custom element, not a typed JSX component */}
304
+ <kasookoo-dialer />
305
+ {/* Static button needs no ref or click handler — the tag is the whole button: */}
306
+ {/* @ts-expect-error -- custom element, not a typed JSX component */}
307
+ <kasookoo-dialer fixed-number="+18005551234" />
308
+ </>
309
+ )
310
+ }
311
+ ```
312
+
313
+ ### Vue
314
+
315
+ ```vue
316
+ <template>
317
+ <!-- Dynamic -->
318
+ <kasookoo-dialer ref="dialerEl" @call-state="onCallState" />
319
+ <!-- Static: shows the number, calls it on click -->
320
+ <kasookoo-dialer fixed-number="+18005551234" />
321
+ </template>
322
+
323
+ <script setup>
324
+ import { ref, onMounted } from "vue"
325
+ import { KasookooClickToCall } from "kasookoo-click-to-call-widget"
326
+
327
+ const dialerEl = ref(null)
328
+ const onCallState = (e) => console.log("call state:", e.detail.state)
329
+
330
+ onMounted(async () => {
331
+ await KasookooClickToCall.init({ publishableKey: "pk_live_...", subject: "Website visitor" })
332
+ dialerEl.value.config = { defaultCountryCode: "+1" }
333
+ })
334
+ </script>
335
+ ```
336
+
337
+ Vue treats unrecognized tags with a dash in the name as custom elements
338
+ automatically — no extra configuration needed.
339
+
340
+ ### Angular
341
+
342
+ ```html
343
+ <!-- Dynamic -->
344
+ <kasookoo-dialer #ctc (call-state)="onCallState($event)"></kasookoo-dialer>
345
+ <!-- Static: shows the number, calls it on click -->
346
+ <kasookoo-dialer fixed-number="+18005551234"></kasookoo-dialer>
347
+ ```
348
+
349
+ ```ts
350
+ // In the module bootstrapping this component:
351
+ // schemas: [CUSTOM_ELEMENTS_SCHEMA]
352
+ import { KasookooClickToCall } from "kasookoo-click-to-call-widget"
353
+
354
+ @Component({ /* ... */ })
355
+ export class ClickToCallComponent implements OnInit {
356
+ async ngOnInit() {
357
+ await KasookooClickToCall.init({ publishableKey: "pk_live_...", subject: "Website visitor" })
358
+ }
359
+ onCallState(e: CustomEvent) {
360
+ console.log("call state:", e.detail.state)
361
+ }
362
+ }
363
+ ```
364
+
365
+ ### Svelte
366
+
367
+ ```svelte
368
+ <script>
369
+ import { onMount } from "svelte"
370
+ import { KasookooClickToCall } from "kasookoo-click-to-call-widget"
371
+
372
+ onMount(() => {
373
+ KasookooClickToCall.init({ publishableKey: "pk_live_...", subject: "Website visitor" })
374
+ })
375
+ </script>
376
+
377
+ <!-- Dynamic -->
378
+ <kasookoo-dialer on:call-state={(e) => console.log("call state:", e.detail.state)} />
379
+ <!-- Static: shows the number, calls it on click -->
380
+ <kasookoo-dialer fixed-number="+18005551234" />
381
+ ```
382
+
383
+ The pattern is the same everywhere: `KasookooClickToCall.init()` is called
384
+ once per app, plain text values are HTML attributes, anything more complex
385
+ is a JS property set on the element (via `ref`, `:prop`, `[prop]`, or plain
386
+ binding, depending on your framework), and anything the widget needs to tell
387
+ you comes back as a `CustomEvent`.
388
+
389
+ ## Requirements
390
+
391
+ - A modern browser (Chrome, Edge, Firefox, Safari — recent versions).
392
+ - The page must be served over **HTTPS** (or `localhost` during development)
393
+ — browsers only allow microphone access on secure origins.
394
+ - The visitor will be prompted to allow microphone access the first time
395
+ `init()` runs; a call cannot be placed without it.
396
+
397
+ ## What this widget does not do
398
+
399
+ - It does not receive incoming calls — it only places outbound calls to
400
+ phone numbers.
401
+ - It does not offer a way to swap in your own dial pad or call window design
402
+ — both are fixed, built-in UI (though a static tag's trigger button and
403
+ dial pad can be restyled — see [Styling](#styling)).
404
+ - It does not support call hold, DTMF tones (in-call keypad input), or
405
+ choosing a specific microphone device.
406
+
407
+ ## API Reference
408
+
409
+ ### `KasookooClickToCall.init(config)`
410
+
411
+ Called once per app, before or after your `<kasookoo-dialer>` tags render.
412
+
413
+ | Option | Type | Default | Description |
414
+ |---|---|---|---|
415
+ | `publishableKey` | `string` | *required* | The key you were given for your website/app. |
416
+ | `subject` | `string` | *required* | Display name for whoever is placing calls. Any name works — not required to be an email or match an account. |
417
+ | `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. |
418
+ | `logging` | `KasookooLoggingConfig` | `-` | Structured JSON console logging. See below. |
419
+ | `telemetry` | `KasookooTelemetryConfig` | `-` | **Optional** OpenTelemetry trace/log export — off unless you turn it on. See below. |
420
+
421
+ `logging` options:
422
+
423
+ | Option | Type | Default | Description |
424
+ |---|---|---|---|
425
+ | `enabled` | `boolean` | `true` | Set to `false` to silence all console logging. |
426
+ | `level` | `"debug" \| "info" \| "warn" \| "error"` | `"info"` | Minimum level emitted. |
427
+ | `service` | `string` | `"kasookoo-click-to-call-widget"` | Logical service name attached to each log line. |
428
+ | `host` | `string` | `window.location.hostname` | Host label attached to each log line. |
429
+
430
+ Log lines are structured JSON emitted via `console.debug`/`info`/`warn`/`error`. Sensitive fields (tokens, passwords, secrets, etc.) are automatically redacted before logging.
431
+
432
+ ```js
433
+ await KasookooClickToCall.init({
434
+ publishableKey: "pk_live_...",
435
+ subject: "Website visitor",
436
+ logging: { enabled: false }, // or e.g. { level: "warn" }
437
+ })
438
+ ```
439
+
440
+ `telemetry` — **entirely optional, off by default.** The widget works
441
+ exactly the same with or without it; nothing here affects calling. Turning
442
+ it on sends OpenTelemetry traces (every `fetch()` the widget makes, tagged
443
+ with the same correlation ids as `logging`) and, optionally, the same
444
+ structured logs, to a collector you control.
445
+
446
+ | Option | Type | Default | Description |
447
+ |---|---|---|---|
448
+ | `enabled` | `boolean` | `false` | Turns telemetry on. Everything below only matters if this is `true`. |
449
+ | `endpoint` | `string` | `"https://monitoring-test.kasookoo.ai/v1/traces"` | OTLP/HTTP traces endpoint. Defaults to a shared test/demo collector — good enough to `{ enabled: true }` and immediately see traces, but point this at your own collector for anything real. |
450
+ | `logsEndpoint` | `string` | derived from `endpoint` | OTLP/HTTP logs endpoint. Defaults to `endpoint` with a trailing `/v1/traces` swapped for `/v1/logs`; set explicitly if your collector doesn't follow that convention. |
451
+ | `serviceName` | `string` | `"kasookoo-click-to-call-widget"` | OTel `service.name`. |
452
+ | `environment` | `string` | `"production"` | OTel `deployment.environment`. |
453
+
454
+ Two things make this safe to leave off, and safe to turn on without it ever
455
+ breaking the widget:
456
+
457
+ - **It requires extra packages you install yourself.** Traces need
458
+ `@opentelemetry/sdk-trace-web`, `@opentelemetry/sdk-trace-base`,
459
+ `@opentelemetry/exporter-trace-otlp-http`, `@opentelemetry/instrumentation`,
460
+ `@opentelemetry/instrumentation-fetch`, `@opentelemetry/resources`,
461
+ `@opentelemetry/semantic-conventions`, and `@opentelemetry/api`. Log export
462
+ additionally needs `@opentelemetry/sdk-logs` and
463
+ `@opentelemetry/exporter-logs-otlp-http`. They're declared as optional
464
+ `peerDependencies` — npm won't install them for you, and won't complain if
465
+ you don't:
466
+
467
+ ```bash
468
+ npm install @opentelemetry/api @opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base \
469
+ @opentelemetry/exporter-trace-otlp-http @opentelemetry/instrumentation \
470
+ @opentelemetry/instrumentation-fetch @opentelemetry/resources \
471
+ @opentelemetry/semantic-conventions @opentelemetry/sdk-logs \
472
+ @opentelemetry/exporter-logs-otlp-http
473
+ ```
474
+
475
+ If `telemetry.enabled` is `true` but these aren't installed, the widget
476
+ logs a console warning and continues working with console logging only —
477
+ it never throws or blocks a call.
478
+
479
+ - **It's bundler-only** — available when you `npm install` the widget, not
480
+ via the plain `<script type="module">`/CDN build. A script tag has no way
481
+ to resolve npm package names, so `dist/kasookoo-dialer.js` doesn't include
482
+ telemetry at all, on or off.
483
+
484
+ ```js
485
+ // Demo — traces go to the shared test collector, no setup required:
486
+ await KasookooClickToCall.init({
487
+ publishableKey: "pk_live_...",
488
+ subject: "Website visitor",
489
+ telemetry: { enabled: true },
490
+ })
491
+
492
+ // Production — point it at your own collector:
493
+ await KasookooClickToCall.init({
494
+ publishableKey: "pk_live_...",
495
+ subject: "Website visitor",
496
+ telemetry: {
497
+ enabled: true,
498
+ endpoint: "https://your-collector.example.com/v1/traces",
499
+ },
500
+ })
501
+ ```
502
+
503
+ ### `KasookooClickToCall` instance
504
+
505
+ The object `init()` resolves to. See [The widget instance](#the-widget-instance). There is deliberately no way to place a call from this object — only a real user clicking a rendered `<kasookoo-dialer>` tag can do that.
506
+
507
+ | Member | Type | Description |
508
+ |---|---|---|
509
+ | `session` | `SessionInfo \| null` | Current session info — `sessionId`, `subject`, `organizationId`, `expiresAt`. |
510
+ | `activeCall` | `Call \| null` | The call currently connecting/connected, if any. |
511
+ | `close()` | `() => void` | Tears the widget down: ends any active call, removes every mounted `<kasookoo-dialer>` trigger button, clears the session. Safe to call more than once. |
512
+
513
+ ### `<kasookoo-dialer>` attributes
514
+
515
+ Plain HTML attributes — values are always strings.
516
+
517
+ | Attribute | Type | Default | Description |
518
+ |---|---|---|---|
519
+ | `fixed-number` | `string` | `-` | Presence turns this tag into a static button showing and calling this one number instead of a dial pad. |
520
+ | `confirm-before-call` | `"false"` to disable | on | Static tags only. Shows a confirmation modal before dialing `fixed-number`. |
521
+ | `show-call-icon-on-hover` | `"false"` to disable | on | Static tags only. Shows a call icon beside the number on hover. |
522
+
523
+ ### `config` property
524
+
525
+ Set as a JS property (`element.config = {...}`), not an attribute — this is
526
+ where anything beyond a plain string goes.
527
+
528
+ | Prop | Type | Default | Description |
529
+ |---|---|---|---|
530
+ | `participantName` | `string` | value of `subject` given to `init()` | Caller name shown on the call itself. |
531
+ | `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. |
532
+ | `placeholder` | `string` | `"Enter phone number"` | Dynamic tags only. Placeholder text for the dial pad's number input. |
533
+ | `customCss` | `string` | `-` | Raw CSS injected into this tag's own shadow root. See [Styling](#styling). |
534
+
535
+ ### Events
536
+
537
+ Standard `CustomEvent`s — listen with `element.addEventListener(name, handler)`.
538
+
539
+ | Event | Detail (`event.detail`) | Description |
540
+ |---|---|---|
541
+ | `call-state` | `{ state: "connecting" \| "connected" \| "ended", phoneNumber: string }` | Fires on every step of a call's life, from dialing to hang-up. |
542
+ | `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 — contact your key provider/Kasookoo. |
543
+ | `session-expired` | `{ reason: string }` | Fires if the widget's session expires mid-use; reload the page to recover. |
544
+
545
+ ### Methods
546
+
547
+ | Method | Signature | Description |
548
+ |---|---|---|
549
+ | `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. |