# Going live

**Availability: Available**

Before switching to a live key, check that your integration is secure, handles errors and retries correctly, and verifies webhooks. This checklist covers what to confirm before you place real, billed calls.

In live mode calls reach real people and are charged to your workspace balance. Everything below can be tested in test mode first; nothing on this list should be discovered in production.

## Before you begin

- Your integration works end to end in test mode: calls placed, webhooks received and verified, results written to your system, refusals handled.
- **API access for live use.** The API is in private preview and must be enabled for your workspace by RelationsAI. If any request answers [`api_access_not_enabled`](/errors/api_access_not_enabled), write to support@relationsai.com with your workspace name. Webhook delivery is a separate preview: check `capabilities.webhooks` on [`GET /workspace`](/api/workspace#retrieve-the-workspace).
- Your agent is published and ready in live mode, with a live outbound number, and the workspace has calling set up and a balance ([Creating and preparing agents](/guides/creating-and-preparing-agents)).

Run the preflight check with the exact key your server will use. It reports the key's mode, scopes and expiry, the limits that apply, the agent's readiness and your webhook endpoints, and exits with status 1 if a check fails:

Ask the API what the key can do:

```bash
curl -X GET https://voice-api.relationsai.com/v1/workspace \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/workspace", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const workspace = await response.json();
```
```python

response = requests.get(
    "https://voice-api.relationsai.com/v1/workspace",
    headers={
        "Authorization": "Bearer <token>",
    },
    timeout=30,
)
workspace = response.json()
```

Response `200` (a live key):

```json
{
  "id": "ws_7Hk2Lm9QxR4tB8vN3cZp6W",
  "object": "workspace",
  "livemode": true,
  "name": "Acme Lending",
  "timezone": "Asia/Kolkata",
  "currency": "INR",
  "api_key": {
    "id": "key_9Bv2Cx7Zl4Kj8Hg3Fd6Sa5",
    "name": "CRM sync",
    "environment": "live",
    "scopes": [
      "workspace:read",
      "calls:read",
      "calls:write"
    ],
    "created_at": "2026-10-21T06:00:00.000Z",
    "expires_at": "2027-01-19T06:00:00.000Z"
  },
  "capabilities": {
    "campaign_pause": true,
    "campaign_resume": true,
    "call_ringing_status": false,
    "live_transcript": false,
    "sentiment_analysis": false,
    "end_call": false,
    "agent_editing": false,
    "inbound_calls": false,
    "webhooks": false
  },
  "limits": {
    "rate_limits": {
      "read": {
        "per_key_per_minute": 600,
        "per_workspace_per_minute": 1200
      },
      "write": {
        "per_key_per_minute": 120,
        "per_workspace_per_minute": 240
      },
      "dial": {
        "per_key_per_minute": null,
        "per_workspace_per_minute": 30
      }
    },
    "daily_call_limit": 500,
    "campaign_max_concurrency": 10
  }
}
```

Check `api_key.environment`, `api_key.scopes` (only what this component needs), `api_key.expires_at`, `limits` and `capabilities.webhooks`. Then check the agent and your endpoints:

```bash
curl -X GET https://voice-api.relationsai.com/v1/agents/agent_1ZltiWzxrXmMprGD6q63/readiness \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/agents/agent_1ZltiWzxrXmMprGD6q63/readiness", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const readiness = await response.json();
```
```python

response = requests.get(
    "https://voice-api.relationsai.com/v1/agents/agent_1ZltiWzxrXmMprGD6q63/readiness",
    headers={
        "Authorization": "Bearer <token>",
    },
    timeout=30,
)
readiness = response.json()
```

```bash
curl -X GET "https://voice-api.relationsai.com/v1/webhook-endpoints?limit=100" \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/webhook-endpoints?limit=100", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const endpoints = await response.json();
```
```python

response = requests.get(
    "https://voice-api.relationsai.com/v1/webhook-endpoints",
    headers={
        "Authorization": "Bearer <token>",
    },
    params={
        "limit": 100,
    },
    timeout=30,
)
endpoints = response.json()
```

Complete runnable example: [cURL script](/examples/guides/going-live/preflight.sh) · [JavaScript](/examples/guides/going-live/preflight.mjs) · [Python](/examples/guides/going-live/preflight.py)

## Keys and scopes

- [ ] **A live key per component**, created by a workspace admin in the platform (**Settings → API Keys**), with only the scopes that component needs. A component that only reads results has no `:write` scope. Remember a key never does more than its creator's current role allows: create it from an account that will keep its role.
- [ ] **An expiry and an owner.** Live keys expire (30, 90, 180 or 365 days). Put the expiry date in your calendar and rotate before it: create a new key, deploy it, and let the old one expire after a short grace period.
- [ ] **Server-side only.** Store the key in a secret manager or your platform's secret configuration, never in source code, a mobile app, browser code, a URL, a log or a ticket. Live keys are refused from browsers with [`live_key_not_allowed_in_browser`](/errors/live_key_not_allowed_in_browser).
- [ ] **The IP allowlist**, if your workspace uses one, includes your servers' egress addresses (otherwise [`ip_not_allowed`](/errors/ip_not_allowed)).
- [ ] **A leak plan.** If a key leaks, an admin revokes it in the console immediately; requests with it then fail with [`api_key_revoked`](/errors/api_key_revoked).

See [Security practices](/fundamentals/security-practices).

## Error handling and retries

- [ ] Every response is checked for `res.ok`; errors branch on `error.code` and `error.retry`, never on `error.message`.
- [ ] **No blind retries of writes**: your own id in `metadata`, and after a timeout or `5xx` a state check before resending; optionally an `Idempotency-Key` per action, saved before the first attempt and reused on retries ([Retry and reliability practices](/guides/retry-and-reliability)).
- [ ] Retries use exponential backoff with jitter and a cap, honour `Retry-After`, and never retry `retry: "no"`.
- [ ] "May have started" errors (`500 internal_error`, timeouts, `campaign_outcome_unknown`, `idempotency_outcome_unknown`) lead to a state check, not to a fresh request.
- [ ] Refusals (`dnc_listed`, `consent_required`, `outside_calling_hours`, `agent_not_ready`, `insufficient_balance`, ...) are recorded as outcomes and surfaced to the people who can fix them, not retried.
- [ ] Open enums: unknown statuses, outcomes, event types and error codes are handled as "other", never as success.
- [ ] `PATCH` uses `If-Match` and handles `412 version_conflict` by re-reading.

## Webhooks

- [ ] A **live** webhook endpoint (its own secret, separate from the test one) on a public HTTPS URL, port 443, with a valid certificate.
- [ ] **Signature verification** on the raw body, constant-time compare, 5-minute tolerance, accepting any of several `v1` values ([Verifying webhook signatures](/guides/verifying-webhook-signatures)).
- [ ] **Deduplication** by event id in durable storage, before answering.
- [ ] **Answer `2xx` within 10 seconds**, process in the background, and sweep for acknowledged but unprocessed events.
- [ ] Out-of-order events cannot overwrite newer state.
- [ ] A reconciliation job lists recent calls (and campaigns) to catch anything missed ([Common integration patterns](/guides/common-integration-patterns#reconciliation)).
- [ ] A test event to the live endpoint `succeeded` in the delivery log.

## Compliance

- [ ] An admin has reviewed the workspace's compliance settings in the console: consent requirement, calling window and per-number daily limit, for the countries you call.
- [ ] Opt-outs from every channel reach the do-not-call list **immediately**, and do-not-call status is synchronised in both directions with your systems ([Consent and DNC](/guides/consent-and-dnc)).
- [ ] Consent is recorded through the API only from real records of consent.
- [ ] Nobody can route around a refusal: no retries with another number format, no automatic do-not-call removals, no consent set to unblock a call.
- [ ] Transcripts, recordings and analysis are stored and shared according to your data-protection obligations; recording links are never stored or logged ([Retrieving recordings and transcripts](/guides/retrieving-recordings-and-transcripts)).

## Limits and spending

- [ ] You know the limits that apply (from [`GET /workspace`](/api/workspace#retrieve-the-workspace)): rate limits per class, `limits.daily_call_limit` for calls placed through the API per UTC day, and `limits.campaign_max_concurrency`. If your volume needs a higher daily limit, ask support before launch, not on the day.
- [ ] Batch jobs pace themselves under the `write` and `dial` limits and read `RateLimit-Remaining`.
- [ ] The workspace balance covers your expected volume, and someone is responsible for topping it up. Calls are refused with [`insufficient_balance`](/errors/insufficient_balance) when it runs low, and running campaigns pause.
- [ ] Campaigns have a `budget_cap` (it pauses the campaign; the check runs about every 15 minutes, so allow for overshoot), and an admin has set a monthly spend limit if you want one ([`spend_limit_reached`](/errors/spend_limit_reached)).
- [ ] Campaign sizes fit the daily limit: a live start counts every recipient and is refused whole when they do not all fit.
- [ ] You handle automatic campaign pauses (`campaign.paused` with `data.reason`) and resume once the cause is fixed.

## Monitoring

- [ ] **Log `request_id`** (the `X-Request-Id` header, also in every error body) with every failed request, together with the operation and the time. Support needs it to find your request.
- [ ] Alert on: the rate of `4xx` and `5xx` per code, `429`s, webhook deliveries that are `failed` or `retrying` (from the delivery log or your own receiver), your inbox of unprocessed events, and calls without `call.settled` after an hour.
- [ ] Track business outcomes as well: refusals by code, `no_answer` and `failed` rates, and spend per day.
- [ ] Dashboards distinguish test from live (`livemode` on every object and event).

## Launch

1. Switch configuration to the live key and the live webhook secret. No code changes should be needed.
2. Start with a small set of real records, ideally people who expect the call.
3. Check the first results in both RelationsAI and your system: outcomes, analysis written to the right records, refusals recorded, charges as expected.
4. Increase volume gradually, watching the alerts above.

## What can go wrong

- **[`api_access_not_enabled`](/errors/api_access_not_enabled) (403)**: The workspace is not enabled for the API. Fix: Write to support@relationsai.com.
- **[`live_key_not_allowed_in_browser`](/errors/live_key_not_allowed_in_browser) (403)**: A live key used from browser code (a request with an `Origin` header). Fix: Move the call to your server.
- **[`resource_not_found`](/errors/resource_not_found) (404) for ids that worked yesterday**: Test-mode ids used with the live key. Fix: Use live ids; nothing is shared between modes except agents and numbers.
- **[`agent_not_ready`](/errors/agent_not_ready) (422)**: The live agent is not published, or has no outbound number. Fix: Fix it in the console and check readiness.
- **[`insufficient_balance`](/errors/insufficient_balance) (402)**: No balance. Fix: An admin tops up.
- **[`daily_call_limit_reached`](/errors/daily_call_limit_reached) (429)**: Volume above the daily limit, or a retry storm. Fix: Check for loops; ask support about the limit.
- **No live webhooks arrive**: The endpoint was created with the test key. Fix: Create a live endpoint with the live key.

## Next steps

- [Security practices](/fundamentals/security-practices)
- [Troubleshooting](/resources/troubleshooting)
- [Support](/resources/support)
