# Getting started

This guide takes you from a new BitCall account to a metered consultation between two browsers. It uses the production API at `https://api.bitcall.stream`. All request and response bodies are JSON.

## What you need

- A BitCall account. Sign up at <https://bitcall.stream/portal/>.
- A sandbox API key (`ck_test_…`). Signup creates a sandbox application and shows its key once.
- A backend you control (any language). The examples use `curl` and Node 22 (`fetch` is built in).
- Two browsers, or one browser with two tabs, each with a microphone.

Where keys live:

| Key | Starts with | Who holds it | Used for |
|-----|-------------|--------------|----------|
| Sandbox API key | `ck_test_` | Your backend | Free development. Capped at 200 relayed minutes per month, never invoiced, events tagged `"mode":"test"`. |
| Live API key | `ck_live_` | Your backend | Production. Billed per relayed minute. Needs live approval (Step 6). |
| Peer token | JWT | One end user, one room | Opening the call WebSocket and fetching ICE servers. |
| Consultant presence token | JWT | One consultant's app | Going online and receiving consultation requests. |

Never ship an API key to a browser or mobile app. Your backend mints short-lived peer tokens and hands those to clients. Keys are stored hashed and cannot be recovered; if one leaks, rotate it in the portal.

## Step 1: Create an application and get a key

1. Sign up at <https://bitcall.stream/portal/>. The portal creates a sandbox app and shows its `ck_test_…` key once. Copy it into your secret store.

2. To add another app later, use **Create app** in the portal. Mode `Sandbox` is available immediately; mode `Live` is enabled after approval (Step 6). Each account can hold up to 10 apps. **Rotate key** on an app issues a new key and invalidates the old one at once.

3. Verify the key from your terminal. `GET /v1/me` returns the authenticated application.

```bash
curl https://api.bitcall.stream/v1/me \
  -H "Authorization: Bearer ck_test_…"
```

Response: `200 { "app": { "id": "app_…", "name": "My App", "mode": "test", "key_prefix": "ck_test_…", "settings": { "max_peers_per_room": 4, "max_rooms": 50, "token_ttl_seconds": 3600, "monthly_minutes": 200, ... } } }`. A wrong or missing key returns `401 {"error":{"code":401,"message":"invalid or missing API key"}}`.

## Step 2: Create a room and mint peer tokens from your backend

A room is a place two or more peers meet. A peer token lets one client join one room under one peer id.

1. Create a room.

```bash
curl -X POST https://api.bitcall.stream/v1/rooms \
  -H "Authorization: Bearer ck_test_…" -H "Content-Type: application/json" \
  -d '{"name":"demo-1","max_peers":2,"ttl_seconds":3600,"metadata":{"order":"o_1"}}'
```

| Field | Type | Notes |
|-------|------|-------|
| `name` | string | Optional label. |
| `max_peers` | int | Optional. Defaults to the app setting (`4` sandbox, `8` live) and cannot exceed it. |
| `ttl_seconds` | int | Optional. The room is removed after this time. |
| `metadata` | object of strings | Optional. Returned on `GET /v1/rooms/{id}`. |

Response: `201 { "room": { "id": "room_…", "app_id": "app_…", "name": "demo-1", "max_peers": 2, "created_at": "…", "expires_at": "…" } }`.

2. Mint one token per participant.

```bash
curl -X POST https://api.bitcall.stream/v1/tokens \
  -H "Authorization: Bearer ck_test_…" -H "Content-Type: application/json" \
  -d '{"room_id":"room_…","peer_id":"alice","name":"Alice","scope":"peer","ttl_seconds":900}'
```

| Field | Type | Notes |
|-------|------|-------|
| `room_id` | string | Required. Must belong to your app, otherwise `404`. |
| `peer_id` | string | Required. Must match `^[A-Za-z0-9_.:@-]{1,64}$` and be unique in the room. A second connection with the same id replaces the first. |
| `name` | string | Optional display name, shown to other peers. |
| `scope` | string | `peer` (default, can place calls) or `subscriber` (receive only). |
| `ttl_seconds` | int | Optional. Defaults to the app's `token_ttl_seconds` (3600). Values above that setting are ignored and the default applies. |

Response: `201 { "token": "<jwt>", "expires_at": "…", "ws_url": "wss://api.bitcall.stream/v1/ws?token=…", "room_id": "room_…", "peer_id": "alice" }`.

3. The same two calls from Node, as a function your API can expose to a logged-in user.

```js
const BASE = 'https://api.bitcall.stream';
const KEY = process.env.BITCALL_API_KEY; // ck_test_… or ck_live_…, server side only

async function bitcall(method, path, body) {
  const res = await fetch(BASE + path, {
    method,
    headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + KEY },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const json = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(`${res.status} ${json.error?.message || ''}`);
  return json;
}

async function tokensForCall(userA, userB) {
  const { room } = await bitcall('POST', '/v1/rooms', { max_peers: 2, ttl_seconds: 3600 });
  const a = await bitcall('POST', '/v1/tokens', { room_id: room.id, peer_id: userA, ttl_seconds: 900 });
  const b = await bitcall('POST', '/v1/tokens', { room_id: room.id, peer_id: userB, ttl_seconds: 900 });
  return { roomId: room.id, tokens: { [userA]: a.token, [userB]: b.token } };
}
```

Return each token only to the user it was minted for.

## Step 3: Connect two browsers with the JS SDK

The SDK is one file served from the API host. It exposes `CallingClient` (rooms) and `PresenceClient` (consultants).

1. Load the SDK.

```html
<script src="https://api.bitcall.stream/sdk/calling-client.js"></script>
```

2. Callee page (Bob). Accept the first incoming call with the local microphone.

```html
<audio id="remote" autoplay></audio>
<script>
  const token = /* Bob's peer token from your backend */;
  const client = new CallingClient({ baseUrl: 'https://api.bitcall.stream', token });

  client.on('incoming', async ({ from, name, accept }) => {
    const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
    await accept(mic);
  });
  client.on('track', ({ peerId, stream }) => { document.getElementById('remote').srcObject = stream; });
  client.on('hangup', ({ peerId }) => console.log('call ended with', peerId));
  client.connect().then(info => console.log('joined', info.room_id, 'as', info.peer_id));
</script>
```

3. Caller page (Alice). Connect, then call Bob by his peer id.

```html
<audio id="remote" autoplay></audio>
<button id="call">Call Bob</button>
<button id="end">Hang up</button>
<script>
  const token = /* Alice's peer token from your backend */;
  const client = new CallingClient({ baseUrl: 'https://api.bitcall.stream', token });
  client.on('track', ({ stream }) => { document.getElementById('remote').srcObject = stream; });
  client.on('rejected', ({ peerId }) => console.log(peerId, 'rejected the call'));
  await client.connect();

  document.getElementById('call').onclick = async () => {
    const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
    await client.call('bob', mic);
  };
  document.getElementById('end').onclick = () => { client.hangup('bob'); client.disconnect(); };
</script>
```

Notes:

- `getUserMedia` needs a secure context (`https://` or `localhost`) and, in most browsers, a user gesture. Request media after a click, as the caller page does.
- `connect()` fetches ICE servers (STUN plus time-limited TURN credentials) with the peer token, opens the WebSocket, and resolves with the `welcome` payload `{ peer_id, room_id, call_id, peers }`. Pass `iceServers` in the constructor to skip the fetch.
- Constructor options: `baseUrl`, `token` (required), `iceServers`, `statsIntervalMs` (default 5000; call quality is reported to BitCall at this interval).
- Events: `connected`, `peer-joined`, `peer-left`, `incoming`, `track`, `connection-state`, `hangup`, `rejected`, `data`, `meter`, `consultation-ended`, `stats`, `chat`, `typing`, `read`, `error`, `disconnected`. Methods: `connect()`, `call(peerId, stream)`, `hangup(peerId)`, `disconnect()`, `sendData(payload, to)`, plus the chat methods in [features/chat.md](features/chat.md).
- A call is the lifetime of a room from its first peer joining to its last peer leaving. That is the time BitCall meters.

## Step 4: Metered consultations

Consultations are 1:1 sessions with a server-side time cap. Your backend converts a user's balance to `max_seconds`; BitCall warns, extends and hard-stops the call. BitCall never sees money.

### Consultant presence

1. Register the consultant from your backend (idempotent).

```bash
curl -X PUT https://api.bitcall.stream/v1/consultants/astro-1 \
  -H "Authorization: Bearer ck_test_…" -H "Content-Type: application/json" \
  -d '{"name":"Pandit Sharma","modes":["audio","video","chat"],"metadata":{"price_per_minute":"25"}}'
```

Response: `200 { "consultant": { "id": "astro-1", "status": "offline", "modes": [...], ... } }`.

2. Mint a presence token and send it to the consultant's app.

```bash
curl -X POST https://api.bitcall.stream/v1/consultants/astro-1/token \
  -H "Authorization: Bearer ck_test_…" -H "Content-Type: application/json" \
  -d '{"ttl_seconds":86400}'
```

Response: `201 { "token": "<jwt>", "expires_at": "…", "presence_url": "wss://api.bitcall.stream/v1/presence/ws?token=…" }`. The default TTL is 24 hours.

3. In the consultant's app, open the presence socket. Connecting sets the consultant `online`; disconnecting sets `offline` about 10 seconds later.

```js
const presence = new PresenceClient({ baseUrl: 'https://api.bitcall.stream', token: presenceToken });

presence.on('request', req => {
  // req is the consultation: { id, user_id, user_name, mode, max_seconds, ... }
  if (confirm(`${req.user_name} wants a ${req.mode} session. Accept?`)) presence.accept(req.id);
  else presence.decline(req.id);
});
presence.on('accepted', async ({ consultant_token, room_id, consultation }) => {
  const call = new CallingClient({ baseUrl: 'https://api.bitcall.stream', token: consultant_token });
  call.on('incoming', async ({ accept }) => accept(await navigator.mediaDevices.getUserMedia({ audio: true })));
  call.on('track', ({ stream }) => { remoteAudio.srcObject = stream; });
  call.on('meter', m => showRemaining(m.remaining_seconds));
  call.on('consultation-ended', m => console.log('ended:', m.reason, 'billed', m.billed_seconds, 's'));
  await call.connect();
});
presence.on('ended', m => console.log('consultation ended', m));
await presence.connect();
```

You can also set status from the backend with `POST /v1/consultants/{id}/status` `{"status":"online"|"offline"|"busy"}`, and list who is available with `GET /v1/consultants?status=online`.

### Request, accept, tokens

1. When a user taps "Call", your backend converts their balance to seconds and requests the consultation.

```bash
curl -X POST https://api.bitcall.stream/v1/consultations \
  -H "Authorization: Bearer ck_test_…" -H "Content-Type: application/json" \
  -d '{"consultant_id":"astro-1","user_id":"user-42","user_name":"Asha","mode":"audio",
       "max_seconds":600,"warn_at_seconds":60,"metadata":{"order_id":"o_123"}}'
```

| Field | Type | Notes |
|-------|------|-------|
| `consultant_id`, `user_id` | string | Required. Same pattern as `peer_id`. |
| `user_name` | string | Optional, shown to the consultant. |
| `mode` | string | `audio` (default), `video` or `chat`. The consultant must offer it. |
| `max_seconds` | int | Hard cap. `0` means uncapped. |
| `warn_at_seconds` | int | Remaining seconds at which the warning fires. Default 60. |
| `request_timeout_seconds` | int | How long the consultant has to answer. Default 45. |
| `connect_timeout_seconds` | int | How long both sides have to join after accept. Default 60. |
| `metadata` | object of strings | Optional, returned with the consultation. |

Response: `201 { "consultation": { "id": "cons_…", "status": "requested", ... } }`. You get `409` if the consultant is offline, busy, already has a pending request, or does not offer `mode`; `402` if the app's monthly quota is used up.

2. The consultant accepts on the presence socket (`presence.accept(id)`) or your backend accepts on their behalf.

```bash
curl -X POST https://api.bitcall.stream/v1/consultations/cons_…/accept \
  -H "Authorization: Bearer ck_test_…" -H "Content-Type: application/json" -d '{"by":"astro-1"}'
```

Response: `200 { "consultation", "room_id", "user_token", "consultant_token", "ws_url", "tokens_expire_at" }`. Tokens last 2 hours.

3. Deliver `user_token` to the user's app. Both the accept response and the `consultation.accepted` webhook (`data.user_token`, `data.consultant_token`, `data.room_id`, `data.ws_url`) carry it, so you can push it from your webhook handler. The consultant receives `consultant_token` on the presence socket. If a token is lost, `POST /v1/consultations/{id}/tokens` re-issues both.

4. The user joins the room with `CallingClient` and calls the consultant. Room peer ids are fixed: the user is `u:<user_id>` and the consultant is `c:<consultant_id>`.

```js
const call = new CallingClient({ baseUrl: 'https://api.bitcall.stream', token: userToken });
call.on('track', ({ stream }) => { remoteAudio.srcObject = stream; });
call.on('meter', m => showRemaining(m.remaining_seconds));
call.on('consultation-ended', m => showSummary(m.reason, m.billed_seconds));
await call.connect();
const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
await call.call('c:astro-1', mic);
```

The meter starts when both peers are connected, not when the consultation is accepted.

### The meter message

Both peers receive a WebSocket frame of type `meter` when the session starts, every 15 seconds, at the warning threshold, after an extension, and at the end. The SDK emits it as the `meter` event; `state: "ended"` also fires `consultation-ended`.

```json
{ "type": "meter", "payload": {
    "consultation_id": "cons_…", "state": "warning",
    "elapsed_seconds": 540, "remaining_seconds": 60, "max_seconds": 600 } }
```

| Field | Notes |
|-------|-------|
| `state` | `started`, `active`, `warning`, `extended` or `ended`. |
| `elapsed_seconds` | Seconds since both peers connected. |
| `remaining_seconds` | Seconds left. `-1` when `max_seconds` is `0`. |
| `max_seconds` | Current cap, including extensions. |
| `billed_seconds` | Present on `ended`. Whole seconds between start and end, capped at `max_seconds`. Bill from this, not from your own clock. |
| `reason` | Present on `ended`. See below. |

To add time after a top-up: `POST /v1/consultations/{id}/extend` `{"add_seconds":120,"by":"user-42"}`. Both peers get a `meter` with `state: "extended"`.

### What ends a call

| End | Status | `reason` | Notes |
|-----|--------|----------|-------|
| Cap reached | `ended` | `balance_exhausted` | BitCall disconnects both peers. |
| User's socket closes | `ended` | `user_hangup` | Includes `hangup()`, tab close and network loss. |
| Consultant's socket closes | `ended` | `consultant_hangup` | |
| `POST /v1/consultations/{id}/end` | `ended` | `closed_by_api` | Works in any non-terminal state. |
| Nobody joined within `connect_timeout_seconds` | `ended` | `connect_timeout` | `billed_seconds` is `0`. |
| Consultant did not answer within `request_timeout_seconds` | `expired` | `request_timeout` | |
| Consultant declined / user cancelled | `declined` / `cancelled` | `declined` / `cancelled` | `POST …/decline`, `POST …/cancel`. |

Read the final state any time with `GET /v1/consultations/{id}` (`status`, `end_reason`, `ended_by`, `billed_seconds`, `duration_ms`).

## Step 5: Receive webhooks

Webhooks tell your backend what happened without polling. Every delivery is signed and retried.

1. In the portal, set the app's webhook URL (saved with `PATCH /v1/account/apps/{id}` `{"webhook_url":"https://your.app/webhooks/bitcall"}`).

2. Open <https://bitcall.stream/portal/webhooks.html>, select the app and click **Rotate secret**. The secret is shown once; store it next to your API key. Use **Send test** to deliver a `webhook.test` event.

3. Verify the signature over the raw body, check the timestamp, and deduplicate by `X-Webhook-Id`. Plain Node, no framework:

```js
import http from 'node:http';
import crypto from 'node:crypto';

const SECRET = process.env.BITCALL_WEBHOOK_SECRET;
const seen = new Set(); // use a unique index on webhook_id in your database

http.createServer(async (req, res) => {
  if (req.method !== 'POST' || req.url !== '/webhooks/bitcall') { res.writeHead(404).end(); return; }
  const chunks = []; for await (const c of req) chunks.push(c);
  const raw = Buffer.concat(chunks);

  const ts = req.headers['x-webhook-timestamp'] || '';
  const sig = req.headers['x-webhook-signature'] || '';
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) { res.writeHead(400).end('stale'); return; }
  const expected = 'v1=' + crypto.createHmac('sha256', SECRET).update(ts + '.').update(raw).digest('hex');
  const a = Buffer.from(expected), b = Buffer.from(sig);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { res.writeHead(401).end('bad signature'); return; }

  const id = req.headers['x-webhook-id'];
  if (!seen.has(id)) { seen.add(id); handle(JSON.parse(raw)); }
  res.writeHead(200).end();
}).listen(4000);
```

Respond within 5 seconds. Non-2xx responses other than 4xx are retried at 2 s, 10 s, 1 m, 5 m and 30 m; a 4xx other than 429 stops retries.

4. Payload shape and the events most integrations handle:

```json
{ "type": "consultation.ended", "app_id": "app_…", "mode": "test", "room_id": "room_…", "call_id": "call_…",
  "consultation_id": "cons_…", "timestamp": "2026-10-27T10:00:01Z",
  "data": { "reason": "balance_exhausted", "ended_by": "system", "billed_seconds": 600, "duration_ms": 600412,
            "max_seconds": 600, "mode": "audio", "consultant_id": "astro-1", "user_id": "user-42", "status": "ended" } }
```

| Event | Use it to |
|-------|-----------|
| `consultation.accepted` | Push `data.user_token` and `data.room_id` to the user's device. |
| `consultation.started` | Mark the session active; the meter is running. |
| `consultation.warning` | Prompt the user to top up (`data.remaining_seconds`). |
| `consultation.extended` | Confirm the top-up (`data.add_seconds`, `data.max_seconds`). |
| `consultation.ended` | Debit `data.billed_seconds`. The billing truth. |
| `consultation.declined`, `.cancelled`, `.expired` | Release the hold, nothing charged. |
| `consultant.status` | Update your directory (`data.consultant_id`, `data.status`, `data.source`). |
| `call.started`, `peer.joined`, `peer.left`, `call.ended` | Plain room calls; `call.ended` carries `data.duration_seconds`. |
| `usage.quota_reached` | Fires once when the app's monthly minutes are used up. New rooms and consultations return `402`. |

The delivery log, retries and replay are in the portal and in [features/webhooks.md](features/webhooks.md).

## Step 6: Go live

1. Request live approval. New accounts are sandbox only. On the portal home page click **Request live access**, describe your app and expected call volume, and send it. The page shows *Live access requested* while BitCall reviews it (usually within one business day) and *live enabled* once approved; if a request is declined, the reason is shown and you can request again. Creating a live app before approval returns `403 "live mode is not enabled for this account yet; contact support for approval"`.

2. Once approved, create an app with mode **Live** in the portal and store its `ck_live_…` key. Live apps default to `max_peers_per_room: 8`, `max_rooms: 1000`, `monthly_minutes: 0` (no cap) and your account's `price_per_minute`, `included_minutes` and `currency`. Set the live app's webhook URL and rotate its secret; secrets are per app.

3. Quotas. When an app with a `monthly_minutes` cap reaches it, `POST /v1/rooms` and `POST /v1/consultations` return `402 "monthly minute quota exhausted for this application"` and `usage.quota_reached` fires once. Calls in progress are not cut. Sandbox apps are capped at 200 minutes. Watch usage with `GET /v1/account` (`this_month` per app) or the portal's Usage card.

4. Invoices. Each live app gets a monthly statement: `billed_minutes = ceil(used_seconds / 60)`, `billable = max(0, billed − included_minutes)`, `amount = billable × price_per_minute`. Read it in the portal or with a portal session token:

```bash
curl "https://api.bitcall.stream/v1/account/apps/app_…/invoice?month=2026-10" \
  -H "Authorization: Bearer <session_token from POST /v1/login>"
```

`GET /v1/account/apps/{id}/usage?month=YYYY-MM` returns the usage summary together with the invoice. Sandbox apps always show `amount: 0`.

## Next steps

- [API reference](API.md): every endpoint, field and WebSocket frame.
- [Errors, limits and conventions](errors-and-limits.md): status codes, rate limits, quotas, TTLs.
- [Webhooks](features/webhooks.md): headers, retries, delivery log, replay.
- [Chat](features/chat.md): history, typing, read receipts on the same socket.
- [Call recording](features/recording.md): consent-gated server-side recording.
- [Scheduled consultations](features/scheduling.md): availability, bookings, auto-start.
- [Group calls](features/group-calls.md): rooms with more than a few participants.
- [Mobile SDKs](features/mobile-sdks.md): Kotlin and Swift clients with the same events.
- Sample integration: `examples/astro-platform/` in the BitCall repository is a complete wallet-to-meter backend in one Node file.
