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

# Integrate

> The same call in cURL, Node and Python.

There is no SDK to install. The API is HTTP and JSON, and a working client is
about fifteen lines — so here it is rather than a dependency.

## Placing a call

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.voice.wixzel.com/v1/calls \
    -H "Authorization: Bearer $WIXZEL_API_KEY" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{
      "to": "+14155551234",
      "agent_id": "ag_01HXYZ"
    }'
  ```

  ```javascript Node theme={null}
  import { randomUUID } from 'node:crypto';

  const res = await fetch('https://api.voice.wixzel.com/v1/calls', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.WIXZEL_API_KEY}`,
      'Idempotency-Key': randomUUID(),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ to: '+14155551234', agent_id: 'ag_01HXYZ' }),
  });

  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${error.code}: ${error.message}`);
  }
  const call = await res.json();
  ```

  ```python Python theme={null}
  import os, uuid, httpx

  res = httpx.post(
      "https://api.voice.wixzel.com/v1/calls",
      headers={
          "Authorization": f"Bearer {os.environ['WIXZEL_API_KEY']}",
          "Idempotency-Key": str(uuid.uuid4()),
      },
      json={"to": "+14155551234", "agent_id": "ag_01HXYZ"},
  )

  if res.status_code >= 400:
      err = res.json()["error"]
      raise RuntimeError(f"{err['code']}: {err['message']}")
  call = res.json()
  ```
</CodeGroup>

## A client worth reusing

Three things separate a snippet from something you can put in production:
matching on `code` rather than message text, respecting `Retry-After`, and
sending an idempotency key on anything that spends money.

<CodeGroup>
  ```javascript Node theme={null}
  const BASE = 'https://api.voice.wixzel.com';

  export async function wixzel(path, { method = 'GET', body, idempotencyKey } = {}) {
    for (let attempt = 0; ; attempt++) {
      const res = await fetch(`${BASE}${path}`, {
        method,
        headers: {
          Authorization: `Bearer ${process.env.WIXZEL_API_KEY}`,
          'Content-Type': 'application/json',
          ...(idempotencyKey && { 'Idempotency-Key': idempotencyKey }),
        },
        body: body && JSON.stringify(body),
      });

      // Retry only what is safe to retry. A 429 did no work; a 4xx will fail
      // again with the same body, and retrying it just wastes everyone's time.
      if (res.status === 429 && attempt < 3) {
        const wait = Number(res.headers.get('Retry-After') ?? 1);
        await new Promise((r) => setTimeout(r, wait * 1000));
        continue;
      }

      if (res.status === 204) return null;
      const json = await res.json();
      if (!res.ok) {
        const e = new Error(json.error.message);
        // Match on code, never on the message — messages are prose and change.
        Object.assign(e, { code: json.error.code, requestId: json.error.request_id });
        throw e;
      }
      return json;
    }
  }
  ```

  ```python Python theme={null}
  import os, time, httpx

  BASE = "https://api.voice.wixzel.com"

  class WixzelError(RuntimeError):
      def __init__(self, code, message, request_id):
          super().__init__(f"{code}: {message}")
          self.code, self.request_id = code, request_id

  def wixzel(path, method="GET", body=None, idempotency_key=None, attempts=3):
      headers = {"Authorization": f"Bearer {os.environ['WIXZEL_API_KEY']}"}
      if idempotency_key:
          headers["Idempotency-Key"] = idempotency_key

      for attempt in range(attempts):
          res = httpx.request(method, f"{BASE}{path}", headers=headers, json=body)

          # A 429 did no work, so it is safe to repeat. A 4xx will fail the same
          # way with the same body.
          if res.status_code == 429 and attempt < attempts - 1:
              time.sleep(float(res.headers.get("Retry-After", 1)))
              continue

          if res.status_code == 204:
              return None
          data = res.json()
          if res.status_code >= 400:
              err = data["error"]
              raise WixzelError(err["code"], err["message"], err["request_id"])
          return data
  ```
</CodeGroup>

## Paging through usage

Every list endpoint uses the same cursor contract, so one loop covers all of
them. See [Pagination](/pagination).

<CodeGroup>
  ```javascript Node theme={null}
  let cursor = null;
  const events = [];

  do {
    const qs = new URLSearchParams({ limit: '100', ...(cursor && { starting_after: cursor }) });
    const page = await wixzel(`/v1/usage/events?${qs}`);
    events.push(...page.data);
    cursor = page.next_cursor;
  } while (cursor);
  ```

  ```python Python theme={null}
  cursor, events = None, []

  while True:
      params = {"limit": 100} | ({"starting_after": cursor} if cursor else {})
      page = wixzel("/v1/usage/events?" + httpx.QueryParams(params).__str__())
      events += page["data"]
      cursor = page["next_cursor"]
      if not cursor:
          break
  ```
</CodeGroup>

## Pin a version

Behavioural changes ship behind a dated version. A key keeps the behaviour it
was created with, but pinning explicitly means an upgrade is something you do
rather than something that happens to you:

```
Wixzel-Version: 2026-09-01
```
