> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apifycloud.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom code

> Inject your own HTML and JavaScript into the widget — safely, inside a sandbox

Every Click to Call widget has a **Custom code** section where you can
paste HTML and JavaScript snippets. The code runs inside a secure
sandbox, so it can react to widget events and call its own APIs
without putting your visitors or your account at risk.

## Where to configure it

In the console under **Call Studio → Settings → Custom code**, two
text areas:

* **Head HTML** — injected at the top of the sandbox document. Use for
  `<script>` tags that set up SDKs, `<link>` tags for CSS, meta tags.
* **Body end HTML** — injected at the bottom. Use for scripts that need
  the DOM ready or that fire on visibility.

Both areas accept arbitrary HTML. `<script>` tags execute. Inline
scripts, external scripts (via `src`), and `<link>` stylesheets all
work.

## The sandbox model

The code you paste runs inside an isolated iframe with a strict sandbox.

What the sandbox **can** do:

* Run any JavaScript
* Load external scripts and stylesheets
* Call third-party APIs (analytics, pixels, SDKs) over the network
* Subscribe to widget events
* Emit custom events back to the widget

What the sandbox **cannot** do:

* Read cookies or `localStorage` on the integrator's domain
* Read cookies or `localStorage` of `apifycloud.io`
* Access the widget's DOM or React state
* Call the integrator's APIs with the user's credentials
* Navigate the top-level window
* Read or modify the embedding page in any way

## The `window.c2c` API

Inside the sandbox, `window.c2c` is pre-populated with a small event
bridge to the widget. Same surface as if it were an uninstrumented
page — you just read events and emit events.

### Subscribe to events

```js theme={null}
window.c2c.on('call_started', (event) => {
  console.log('Call began', event.context);
});

window.c2c.on('call_ended', (event) => {
  // Send to your analytics
  ga4Track('call_ended', {
    duration: event.duration,
    reason: event.reason,
    order_id: event.context.orderId,
  });
});
```

All events from the [Events catalogue](/guides/click-to-call/events) are
available. The callback receives the full `data` object including
`context`.

### Unsubscribe

`on` returns an unsubscribe function. Call it to stop receiving
events:

```js theme={null}
const off = window.c2c.on('call_ended', handler);
// ...later
off();
```

Or use `window.c2c.off(name, handler)` explicitly.

### Emit custom events

```js theme={null}
window.c2c.emit('custom_action', { kind: 'chat_opened' });
```

Your custom events surface in the parent page and in
[Integrations](/guides/click-to-call/integrations) under the prefix
`custom:custom_action`.

### Read URL context

```js theme={null}
console.log(window.c2c.context.orderId);
console.log(window.c2c.appId);
```

The `context` object is frozen — you can read but not modify it.

### Await ready

The shim dispatches a `c2c:ready` event on its own `window` as soon as
the bridge is ready:

```js theme={null}
window.addEventListener('c2c:ready', () => {
  // window.c2c is fully initialised
});
```

In practice, `window.c2c` is defined synchronously before your scripts
execute, so you rarely need to wait.

## Common examples

### GA4 — track calls as events

```html theme={null}
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){ dataLayer.push(arguments); }
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXX');

  window.c2c.on('call_started', () => gtag('event', 'call_started'));
  window.c2c.on('call_ended', (e) => {
    gtag('event', 'call_ended', {
      duration: e.duration,
      reason: e.reason,
    });
  });
  window.c2c.on('survey_submitted', (e) => {
    gtag('event', 'survey_submitted', { rating: e.rating });
  });
</script>
```

### Meta Pixel — fire Lead on call start

```html theme={null}
<script>
  !function(f,b,e,v,n,t,s){/* Meta Pixel snippet */}(
    window, document,'script','https://connect.facebook.net/en_US/fbevents.js');
  fbq('init', 'YOUR_PIXEL_ID');
  fbq('track', 'PageView');

  window.c2c.on('call_started', () => fbq('track', 'Lead'));
</script>
```

### Google Tag Manager — push events to the dataLayer

```html theme={null}
<script>
  window.dataLayer = window.dataLayer || [];

  window.c2c.on('call_started', (e) => {
    window.dataLayer.push({ event: 'c2c_call_started', context: e.context });
  });
  window.c2c.on('call_ended', (e) => {
    window.dataLayer.push({
      event: 'c2c_call_ended',
      duration: e.duration,
      reason: e.reason,
      context: e.context,
    });
  });
</script>
```

### Custom events — emit your own signals

Use `window.c2c.emit` to surface custom signals from the sandbox. They
reach the parent page as `postMessage` and integrations as
`custom:<name>`.

```html theme={null}
<script>
  // Let your own logic fire a named event when, for example, a user
  // interacts with a chat widget you loaded inside the sandbox.
  function onChatOpened() {
    window.c2c.emit('chat_opened', { source: 'widget' });
  }
</script>
```

### Conditional tracking based on URL context

The `context` object is available on every event and on
`window.c2c.context`. Use it to only fire a tag for specific visitor
cohorts.

```html theme={null}
<script>
  window.c2c.on('call_ended', (e) => {
    if (e.context.customerTier === 'gold' && e.duration > 30) {
      // Your own tracking call here.
    }
  });
</script>
```

## Limitations to keep in mind

<AccordionGroup>
  <Accordion title="No access to cookies on your domain">
    Because the sandbox has an opaque origin, it has its own cookie
    jar. Any SDK that depends on first-party cookies on your domain
    will create them inside the sandbox instead, not on the embedding
    page.
  </Accordion>

  <Accordion title="No direct access to parent page">
    You can't read URL parameters from the embedding page, inspect its
    DOM, or modify it. Pass anything you need via
    [URL context](/guides/click-to-call/embedding#url-context) when
    loading the widget.
  </Accordion>

  <Accordion title="Synchronous postMessage only">
    Communication with the widget is event-based and one-way per direction
    (sandbox → widget and widget → sandbox). There's no request/response
    RPC pattern built in.
  </Accordion>

  <Accordion title="No access to call audio">
    WebRTC audio streams live in the widget, not in the sandbox. Custom
    code cannot tap into mic input or call audio.
  </Accordion>
</AccordionGroup>

## Debugging custom code

The sandbox is a normal iframe — open your browser devtools and select
it in the iframe dropdown to inspect its console, network, and
sources. Errors in your custom code appear there, not in the main page
console.

If a script fails silently:

1. Check the Network tab for blocked requests (CSP on your side, ad
   blockers, mixed-content warnings).
2. Check the Console for thrown exceptions inside handlers.
3. Verify the iframe's sandbox attributes are what you expect — by
   design they should be `allow-scripts` only.

## What's next

<CardGroup cols={2}>
  <Card title="Events" href="/guides/click-to-call/events">
    The full catalogue of events you can subscribe to.
  </Card>

  <Card title="Security" href="/guides/click-to-call/security">
    Why the sandbox is shaped this way and what it protects against.
  </Card>
</CardGroup>
