# Quickstart: your first call

**Availability: Test mode**

In this quickstart you check an API key, pick an agent, place a call in test mode, follow it until it is final, and read its transcript, recording and analysis: eight requests, about five minutes.

> **Test mode never places a real call:** Everything on this page uses a test key (`rai_test_...`). Test mode never dials a phone, never reaches a phone network and never charges your workspace. The call, its transcript, its recording and its analysis are simulated, and every object says `"livemode": false`.

## Before you begin

- A RelationsAI workspace with API access. The API is in private preview: a workspace that has not been enabled answers every authenticated request with [`api_access_not_enabled`](/errors/api_access_not_enabled). Write to [support@relationsai.com](mailto:support@relationsai.com) to request access.
- A workspace owner or admin to create the key (only they can create keys).
- One of: `curl`; Node.js 20 or newer (built-in `fetch`); Python 3.9 or newer with `requests` (`pip install requests`).
- No agent of your own is needed. Test mode has a built-in **Sample agent** (`agent_1ZltiWzxrXmMprGD6q63`) that is always ready.

### Step 1: Create a test API key
In the RelationsAI platform ([voice.relationsai.com](https://voice.relationsai.com)), open **Settings → API Keys** and select **Create API key** (step by step: [Get an API key](/getting-started/api-keys#get-an-api-key)):

1. Name it after what will use it, for example `Quickstart`.
2. Choose the **Test** environment.
3. Keep the default read permissions and also tick **Place calls** (`calls:write`).
4. Choose an expiry. Test keys may also be set to never expire.
5. Select **Create API key**, copy the key and confirm that you have stored it. The key is shown once.

In the requests on this page, replace `<token>` with that key. The ids shown (`call_6Vb3Nm8Qw2Er5Ty9Ui4Op7` and so on) are examples: use the ids from your own responses. Keep the key in a secret manager, never in source code. See [API keys](/getting-started/api-keys) for scopes, expiry and rotation.

### Step 2: Check the key
Retrieve the workspace. It is the quickest way to check that a key works, which mode it is in and which scopes it has, and it never changes anything.

[`GET /workspace`](/api/workspace#retrieve-the-workspace)

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

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

```json
{
  "id": "ws_7Hk2Lm9QxR4tB8vN3cZp6W",
  "object": "workspace",
  "livemode": false,
  "name": "Acme Lending",
  "timezone": "Asia/Kolkata",
  "currency": "INR",
  "api_key": {
    "id": "key_4Nf8Gt2Hx6Jq9Lm3Pv7Rs1",
    "name": "CRM sync (test)",
    "environment": "test",
    "scopes": [
      "workspace:read",
      "agents:read",
      "numbers:read",
      "calls:read",
      "calls:write",
      "transcripts:read",
      "recordings:read",
      "analysis:read"
    ],
    "created_at": "2026-10-20T08:15:00.000Z",
    "expires_at": null
  },
  "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": true
  },
  "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": null,
    "campaign_max_concurrency": 10
  }
}
```

**Next:** check that `livemode` is `false` (a test key) and that `api_key.scopes` contains `calls:write`.

### Step 3: List agents
List the agents your key can use. Test mode lists your workspace's real agents plus the built-in Sample agent, marked `"sample": true`.

[`GET /agents`](/api/agents#list-agents)

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

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

```json
{
  "object": "list",
  "data": [
    {
      "id": "agent_3hT9kQ2mVx7RbN4pLc8Wz1",
      "object": "agent",
      "livemode": false,
      "name": "Loan renewal reminder",
      "description": "Reminds customers that their loan is due for renewal and books a callback.",
      "status": "active",
      "published": true,
      "published_version": 4,
      "is_default": true,
      "sample": false,
      "created_at": "2026-09-02T10:12:44.000Z",
      "updated_at": "2026-10-18T14:03:10.000Z"
    },
    {
      "id": "agent_1ZltiWzxrXmMprGD6q63",
      "object": "agent",
      "livemode": false,
      "name": "Sample agent",
      "description": "A built-in agent for trying the API in test mode.",
      "status": "active",
      "published": true,
      "published_version": 1,
      "is_default": false,
      "sample": true,
      "created_at": "2026-10-21T00:00:00.000Z",
      "updated_at": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

**Next:** use the Sample agent's `id`, `agent_1ZltiWzxrXmMprGD6q63`. It has the same id in every workspace, exists only in test mode, and calls from the sample number `+12025550150`. If `has_more` is `true`, see [Pagination](/fundamentals/pagination).

### Step 4: Check that the agent is ready
Ask whether the agent can place calls right now. The Sample agent is always ready; one of your own agents reports every problem it has, such as being switched off in the console.

[`GET /agents/{agent_id}/readiness`](/api/agents#check-whether-an-agent-can-place-calls)

```bash
curl 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", {
  headers: {
    Authorization: "Bearer <token>",
  },
});
console.log(response.status, await response.json());
```
```python

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

```json
{
  "object": "agent_readiness",
  "livemode": true,
  "agent_id": "agent_3hT9kQ2mVx7RbN4pLc8Wz1",
  "ready": true,
  "reasons": [],
  "checked_at": "2026-10-21T09:30:00.000Z"
}
```

This example from the API reference is a live agent; for the Sample agent in test mode, `agent_id` is `agent_1ZltiWzxrXmMprGD6q63` and `livemode` is `false`.

**Next:** continue when `ready` is `true`. Otherwise each entry in `reasons` says what to fix in the console.

### Step 5: Place a test call
Send the agent and a destination number. In test mode the destination chooses the simulated result: `+12025550100` answers and completes a conversation. `variables` are values the agent can use in the conversation; `metadata` is your own data, returned as sent. Each `POST /v1/calls` places a call: if a request times out, list your calls before sending it again, or send an optional `Idempotency-Key` so that a retry returns the same call (see [Idempotency](/fundamentals/idempotency)).

[`POST /calls`](/api/calls#place-a-call)

```bash
curl -X POST https://voice-api.relationsai.com/v1/calls \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{
  "agent_id": "agent_1ZltiWzxrXmMprGD6q63",
  "to": "+12025550100",
  "variables": {
    "first_name": "Asha",
    "amount_due": "4,200"
  },
  "metadata": {
    "crm_lead_id": "L-20931"
  }
}'
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/calls", {
  method: "POST",
  headers: {
    Authorization: "Bearer <token>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    agent_id: "agent_1ZltiWzxrXmMprGD6q63",
    to: "+12025550100",
    variables: {
      first_name: "Asha",
      amount_due: "4,200",
    },
    metadata: {
      crm_lead_id: "L-20931",
    },
  }),
});
console.log(response.status, await response.json());
```
```python

response = requests.post(
    "https://voice-api.relationsai.com/v1/calls",
    headers={
        "Authorization": "Bearer <token>",
    },
    json={
        "agent_id": "agent_1ZltiWzxrXmMprGD6q63",
        "to": "+12025550100",
        "variables": {
            "first_name": "Asha",
            "amount_due": "4,200",
        },
        "metadata": {
            "crm_lead_id": "L-20931",
        },
    },
    timeout=30,
)
print(response.status_code, response.json())
```

```json
{
  "id": "call_6Vb3Nm8Qw2Er5Ty9Ui4Op7",
  "object": "call",
  "livemode": false,
  "status": "queued",
  "outcome": null,
  "end_reason": null,
  "direction": "outbound",
  "source": "api",
  "agent_id": "agent_1ZltiWzxrXmMprGD6q63",
  "campaign_id": null,
  "phone_number_id": "pn_6Vhm3sFrB5UpmXAnsvmDu",
  "from": "+12025550150",
  "to": "+12025550100",
  "metadata": {
    "crm_lead_id": "L-20931"
  },
  "created_at": "2026-10-21T09:30:00.000Z",
  "ended_at": null,
  "duration_seconds": null,
  "billable_minutes": null,
  "cost": null,
  "artifacts": {
    "transcript": {
      "state": "pending"
    },
    "recording": {
      "state": "pending"
    },
    "analysis": {
      "state": "pending"
    }
  },
  "settled": false,
  "settled_at": null
}
```

`202` means the call passed every check and was accepted; it does not mean anyone answered. Other test numbers simulate other results:

| Destination | Simulated result | `outcome` |
| --- | --- | --- |
| `+12025550100` | Answered and completed, with transcript, recording and analysis | `completed` |
| `+12025550101` | No answer | `no_answer` |
| `+12025550102` | Busy | `busy` |
| `+12025550103` | Failed: the call could not be connected | `failed` |
| `+12025550104` | Answered by voicemail; nobody spoke | `no_response` |
| `+12025550105` | Completed after a longer conversation | `completed` |
| `+12025550106` | The person hangs up early; short duration | `completed` |
| Any other valid number | Completed | `completed` |

**Next:** keep the call's `id` and read the call until it is settled.

### Step 6: Retrieve the call
Read the call. A test call reaches `status: ended` within about 20 seconds and `settled: true` within about 30 seconds; `settled` means everything about the call is final: outcome, duration, artifacts and charge.

[`GET /calls/{call_id}`](/api/calls#retrieve-a-call)

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

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

```json
{
  "id": "call_2Hd8Ks4Lq7Mw3Nx9Pz5Rc1",
  "object": "call",
  "livemode": true,
  "status": "ended",
  "outcome": "completed",
  "end_reason": "hangup",
  "direction": "outbound",
  "source": "api",
  "agent_id": "agent_3hT9kQ2mVx7RbN4pLc8Wz1",
  "campaign_id": null,
  "phone_number_id": "pn_5Lq8Xc2Rk9TzW3mBv7Np4d",
  "from": "+918000000001",
  "to": "+919000000001",
  "metadata": {
    "crm_lead_id": "L-20931"
  },
  "created_at": "2026-10-21T09:30:00.000Z",
  "ended_at": "2026-10-21T09:32:14.000Z",
  "duration_seconds": 118,
  "billable_minutes": 2,
  "cost": {
    "amount": 600,
    "currency": "INR"
  },
  "artifacts": {
    "transcript": {
      "state": "ready"
    },
    "recording": {
      "state": "ready"
    },
    "analysis": {
      "state": "ready"
    }
  },
  "settled": true,
  "settled_at": "2026-10-21T09:41:02.000Z"
}
```

This example from the API reference is a settled live call; a settled test call looks the same except that `livemode` is `false` and `cost.amount` is always `0`.

**Next:** repeat this request every few seconds until `settled` is `true`. In production, let RelationsAI tell you instead (last step).

### Step 7: Read the transcript
Once `artifacts.transcript.state` is `ready`, read the conversation turn by turn, verbatim. This needs `transcripts:read`, which is in the default scopes of a new key.

[`GET /calls/{call_id}/transcript`](/api/transcripts#retrieve-a-call-transcript)

```bash
curl https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7/transcript \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7/transcript", {
  headers: {
    Authorization: "Bearer <token>",
  },
});
console.log(response.status, await response.json());
```
```python

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

```json
{
  "object": "call_transcript",
  "livemode": true,
  "call_id": "call_2Hd8Ks4Lq7Mw3Nx9Pz5Rc1",
  "state": "ready",
  "simulated": false,
  "turns": [
    {
      "index": 0,
      "speaker": "agent",
      "text": "Hello, this is an AI assistant calling from Acme Lending. Am I speaking with Asha?",
      "start_ms": 400,
      "end_ms": 4900
    },
    {
      "index": 1,
      "speaker": "caller",
      "text": "Yes, speaking.",
      "start_ms": 5600,
      "end_ms": 6500
    }
  ]
}
```

In test mode the transcript is a fixture with `"simulated": true` and `"livemode": false`; this example from the API reference comes from a live call.

**Next:** read the recording.

### Step 8: Get the recording link
Ask for a short-lived link to the audio (about 15 minutes, see `expires_at`). This needs `recordings:read`.

[`GET /calls/{call_id}/recording`](/api/recordings#retrieve-a-call-recording-link)

```bash
curl https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7/recording \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7/recording", {
  headers: {
    Authorization: "Bearer <token>",
  },
});
console.log(response.status, await response.json());
```
```python

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

```json
{
  "object": "call_recording",
  "livemode": true,
  "call_id": "call_2Hd8Ks4Lq7Mw3Nx9Pz5Rc1",
  "state": "ready",
  "simulated": false,
  "url": "https://voice-api.relationsai.com/v1/recordings/rec_tok_Y2FsbF8ySGQ4S3M0THE3TXczTng5UHo1UmMx",
  "expires_at": "2026-10-21T10:00:00.000Z",
  "content_type": "audio/ogg"
}
```

The link works without an API key, so treat it like a password: use it right away, do not log or store it, and ask for a new one when you need it. It also stops working as soon as the key that obtained it is revoked or expires, loses `recordings:read`, or its creator leaves the workspace. In test mode the audio is a short synthetic `audio/wav` file and the recording has `"simulated": true`.

**Next:** download the audio with a plain `GET` of `url` (no `Authorization` header), or go on to the analysis.

### Step 9: Read the analysis
Read the summary, the fields the agent extracts (configured in the console) and whether the call met its goal. This needs `analysis:read`.

[`GET /calls/{call_id}/analysis`](/api/analysis#retrieve-a-call-analysis)

```bash
curl https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7/analysis \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7/analysis", {
  headers: {
    Authorization: "Bearer <token>",
  },
});
console.log(response.status, await response.json());
```
```python

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

```json
{
  "object": "call_analysis",
  "livemode": true,
  "call_id": "call_2Hd8Ks4Lq7Mw3Nx9Pz5Rc1",
  "state": "ready",
  "simulated": false,
  "summary": {
    "state": "ready",
    "text": "The customer confirmed the renewal and asked for a callback on Friday afternoon."
  },
  "extracted_fields": [
    {
      "name": "wants_renewal",
      "type": "boolean",
      "state": "ready",
      "value": true
    },
    {
      "name": "callback_day",
      "type": "string",
      "state": "ready",
      "value": "Friday"
    }
  ],
  "goal_met": true
}
```

In test mode the analysis is a fixture with `"simulated": true`. More in [Retrieving recordings and transcripts](/guides/retrieving-recordings-and-transcripts) and [Working with analysis results](/guides/working-with-analysis-results).

**Next:** stop polling and receive events instead.

### Step 10: Get notified instead of polling
Register a webhook endpoint and RelationsAI sends your server a signed `call.ended` event when the call ends and `call.settled` when everything is final, in test mode too. Webhook delivery is a separate private preview: check that `capabilities.webhooks` is `true` in the workspace response from step 2 before you build on it. Follow [Receiving webhooks](/guides/receiving-webhooks), then [Verifying webhook signatures](/guides/verifying-webhook-signatures).

## Run the whole flow

The same flow as one tested program in cURL, JavaScript and Python: it checks the key (and refuses anything but a test key), finds the Sample agent, checks readiness, places a call to `+12025550100`, polls until the call settles and prints the transcript, the recording state and the analysis.

Complete runnable example: [cURL script](/examples/getting-started/quickstart.sh) · [JavaScript](/examples/getting-started/quickstart.mjs) · [Python](/examples/getting-started/quickstart.py)

## What can go wrong

- [`authentication_required`](/errors/authentication_required): No `Authorization` header, or not in the form `Bearer <key>`. Check that the `Authorization` header carries your key in place of `<token>`.
- [`invalid_api_key`](/errors/invalid_api_key): The key is mistyped, truncated or not an API key. Copy it again from where you stored it. If it is lost, create a new key: secrets are shown only once.
- [`legacy_api_key_not_supported`](/errors/legacy_api_key_not_supported): An older `sk_live_` key. Create a scoped key under **Settings → API Keys**.
- [`api_access_not_enabled`](/errors/api_access_not_enabled): The workspace is not in the API preview. Write to [support@relationsai.com](mailto:support@relationsai.com) to request access.
- [`insufficient_scope`](/errors/insufficient_scope) on `POST /calls`: The key has no `calls:write`. Create a test key with **Place calls** ticked. `details.required_scopes` lists what is missing.
- [`live_key_not_allowed_in_browser`](/errors/live_key_not_allowed_in_browser): A live key was sent from browser code. Use a test key, and call the API from a server.
- [`resource_not_found`](/errors/resource_not_found): The id is wrong, from another workspace or from the other mode. The Sample agent exists only in test mode; copy ids from your own responses.
- [`agent_not_ready`](/errors/agent_not_ready): One of your own agents cannot call yet. Read its readiness `reasons` and fix them in the console, or use the Sample agent.
- [`dnc_listed`](/errors/dnc_listed) or [`consent_required`](/errors/consent_required): Your test-mode do-not-call list or consent rules refuse the number. Test mode applies these exactly as live mode. Use another test number, or see [Consent and do-not-call](/guides/consent-and-dnc).
- [`rate_limited`](/errors/rate_limited): Too many requests in a minute. Wait for `Retry-After` seconds. See [Rate limits](/fundamentals/rate-limits).

Every error has the same shape and a `request_id`; see [Error handling](/fundamentals/error-handling).

## Check that it worked

- The call's `settled` is `true`, `outcome` is `completed` and all three `artifacts` are `ready`.
- Every object you received has `"livemode": false`, and the call's `cost.amount` is `0`. Nothing was charged.
- Test-mode calls are kept apart from live data: they do not appear in the console's call history or to live keys. Read them with your test key.

## Next steps

- [Authentication](/getting-started/authentication) and [API keys](/getting-started/api-keys): scopes, expiry, rotation.
- [Test mode](/getting-started/test-mode): every simulated outcome and what is enforced.
- [Making an outbound call](/guides/making-an-outbound-call) and [Tracking call progress](/guides/tracking-call-progress) in depth.
- [Receiving webhooks](/guides/receiving-webhooks) instead of polling.
- [Going live](/guides/going-live) when you are ready for real calls.
