# Authentication
Source: https://docs.reachedapp.com/api-reference/authentification
Learn how to authenticate your API requests using Bearer tokens.
# Authentication
All API requests require a valid API key passed in the `Authorization` header as a Bearer token.
## Generating an API Key
In the Reached dashboard, go to **Settings > API**.
Click **Create Key**, give it a descriptive name (e.g., "Clay Integration" or "Zapier"), and confirm.
Your API key will be displayed once. Copy it and store it securely. It starts with `rchd_live_`.
## Header Format
Include your API key in every request using the `Authorization` header:
```
Authorization: Bearer rchd_live_a1b2c3d4e5f6789012345678901234567890abcd
```
## Example Request
```bash cURL theme={null}
curl -X GET \
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/leads" \
-H "Authorization: Bearer rchd_live_a1b2c3d4e5f6789012345678901234567890abcd"
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/leads",
{
headers: {
"Authorization": "Bearer rchd_live_a1b2c3d4e5f6789012345678901234567890abcd"
}
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/leads",
headers={
"Authorization": "Bearer rchd_live_a1b2c3d4e5f6789012345678901234567890abcd"
}
)
data = response.json()
```
## Authentication Errors
If your API key is missing, invalid, or revoked, the API returns a `401 Unauthorized` response:
```json theme={null}
{
"error": {
"code": "unauthorized",
"message": "Invalid API key"
}
}
```
**Keep your API keys secret.** Do not share API keys in public repositories, client-side code, or insecure channels. If a key is compromised, revoke it immediately in **Settings > API**.
## Key Management
| Action | How |
| ---------- | -------------------------------------------------------- |
| **Create** | Settings > API > Create Key |
| **Revoke** | Settings > API > click the revoke button next to the key |
| **Rotate** | Revoke the old key and create a new one |
API keys are scoped to your company workspace. All data accessed through a key belongs to the workspace that created it.
# Overview
Source: https://docs.reachedapp.com/api-reference/overview
Complete reference for all Reached API endpoints.
# API Reference
The Reached API provides a set of RESTful endpoints to manage your outbound calling workflow. All endpoints accept and return JSON.
## Base URL
```text theme={null}
https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1
```
## Authentication
All requests must include a valid API key in the `Authorization` header:
```text theme={null}
Authorization: Bearer rchd_live_xxxxxxxxxxxx
```
See the full authentication guide for details on generating and managing API keys.
## Available Resources
Create, list, get, and update leads in your workspace.
Access calling campaigns and add leads for parallel dialing.
Retrieve call history with recordings, transcripts, and dispositions.
Create and manage follow-up tasks linked to leads.
## Endpoint Summary
### Leads
| Method | Endpoint | Description |
| ------ | --------------- | ------------- |
| `POST` | `/v1/leads` | Create a lead |
| `GET` | `/v1/leads` | List leads |
| `GET` | `/v1/leads/:id` | Get a lead |
| `PUT` | `/v1/leads/:id` | Update a lead |
### Campaigns
| Method | Endpoint | Description |
| -------- | ---------------------------------- | ----------------------------- |
| `GET` | `/v1/campaigns` | List campaigns |
| `GET` | `/v1/campaigns/:id` | Get a campaign |
| `POST` | `/v1/campaigns/:id/leads` | Add leads to a campaign |
| `DELETE` | `/v1/campaigns/:id/leads/:lead_id` | Remove a lead from a campaign |
### Call Logs
| Method | Endpoint | Description |
| ------ | ------------------- | -------------- |
| `GET` | `/v1/call-logs` | List call logs |
| `GET` | `/v1/call-logs/:id` | Get a call log |
### Tasks
| Method | Endpoint | Description |
| ------ | ----------- | ------------- |
| `POST` | `/v1/tasks` | Create a task |
| `GET` | `/v1/tasks` | List tasks |
# Task
Source: https://docs.reachedapp.com/create-task
POST /plantsleads
Creates a new task, optionally linked to a lead.
# Create a Task
Creates a new task. Optionally link it to a lead or assign it to a specific user.
## Request
`POST /v1/tasks`
### Body Parameters
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------------------------------------- |
| `title` | string | Yes | Task title |
| `description` | string | No | Task description |
| `notes` | string | No | Additional notes |
| `lead_id` | uuid | No | Link task to a lead |
| `user_id` | uuid | No | Assign task to a specific user |
| `due_date` | string | No | Due date (YYYY-MM-DD) |
| `due_time` | string | No | Due time (HH:MM) |
| `priority` | string | No | Priority: `low`, `medium`, or `high` (default: `medium`) |
## Example
```bash cURL theme={null}
curl -X POST \
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/tasks" \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "Follow up with John Doe",
"lead_id": "a1b2c3d4-...",
"due_date": "2026-03-25",
"priority": "high"
}'
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/tasks",
{
method: "POST",
headers: {
"Authorization": "Bearer rchd_live_xxxxxxxxxxxx",
"Content-Type": "application/json"
},
body: JSON.stringify({
title: "Follow up with John Doe",
lead_id: "a1b2c3d4-...",
due_date: "2026-03-25",
priority: "high"
})
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/tasks",
headers={
"Authorization": "Bearer rchd_live_xxxxxxxxxxxx",
"Content-Type": "application/json"
},
json={
"title": "Follow up with John Doe",
"lead_id": "a1b2c3d4-...",
"due_date": "2026-03-25",
"priority": "high"
}
)
data = response.json()
```
## Response
```json theme={null}
{
"data": {
"id": "task-uuid-1",
"title": "Follow up with John Doe",
"lead_id": "a1b2c3d4-...",
"status": "pending",
"priority": "high",
"due_date": "2026-03-25",
"created_at": "2026-03-21T10:00:00.000Z"
}
}
```
```json theme={null}
{
"error": {
"code": "bad_request",
"message": "title is required"
}
}
```
# Errors
Source: https://docs.reachedapp.com/errors
Understand error responses and HTTP status codes returned by the API.
# Errors
The API returns consistent error responses with an HTTP status code and a JSON body containing an error code and human-readable message.
## Error Response Format
All errors follow this structure:
```json theme={null}
{
"error": {
"code": "not_found",
"message": "Lead not found"
}
}
```
| Field | Type | Description |
| --------------- | ------ | ----------------------------------------- |
| `error.code` | string | A machine-readable error code |
| `error.message` | string | A human-readable description of the error |
## HTTP Status Codes
| Code | Status | Description |
| ----- | ----------------- | --------------------------------------------------- |
| `200` | OK | Request succeeded |
| `201` | Created | Resource successfully created |
| `400` | Bad Request | Invalid request body or missing required parameters |
| `401` | Unauthorized | Missing, invalid, or revoked API key |
| `403` | Forbidden | API key does not have permission for this action |
| `404` | Not Found | The requested resource does not exist |
| `429` | Too Many Requests | Rate limit exceeded |
| `500` | Internal Error | An unexpected error occurred on the server |
## Error Code Reference
| Error Code | HTTP Status | Description |
| -------------- | ----------- | -------------------------- |
| `unauthorized` | 401 | Invalid or missing API key |
| `bad_request` | 400 | Invalid request parameters |
| `not_found` | 404 | Resource not found |
| `rate_limited` | 429 | Rate limit exceeded |
| `internal` | 500 | Server error |
## Common Error Scenarios
### Missing Authorization Header
```bash theme={null}
curl -X GET "https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/leads"
```
```json theme={null}
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid Authorization header. Use: Bearer rchd_live_xxx"
}
}
```
### Invalid JSON Body
```bash theme={null}
curl -X POST \
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/leads" \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d 'invalid json'
```
```json theme={null}
{
"error": {
"code": "bad_request",
"message": "Invalid JSON body"
}
}
```
### Resource Not Found
```json theme={null}
{
"error": {
"code": "not_found",
"message": "Lead not found"
}
}
```
Always check the `error.code` field programmatically rather than parsing the `message` string, as messages may change over time.
# Task
Source: https://docs.reachedapp.com/get-a-task
GET /plantsleads
Returns a paginated list of tasks with optional filters.
# List Tasks
Returns a paginated list of tasks with optional filters.
## Request
`GET /v1/tasks`
### Query Parameters
| Parameter | Type | Required | Description |
| ---------- | ------- | -------- | ------------------------------------------------------ |
| `page` | integer | No | Page number (default: 1) |
| `per_page` | integer | No | Results per page (default: 25) |
| `status` | string | No | Filter by status (`pending`, `completed`, `cancelled`) |
| `lead_id` | uuid | No | Filter by lead ID |
## Example
```bash cURL theme={null}
curl -X GET \
"https://YOUR_PROJECT.supabase.co/functions/v1/api-gateway/v1/tasks?status=pending" \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx"
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/tasks?status=pending",
{
headers: {
"Authorization": "Bearer rchd_live_xxxxxxxxxxxx"
}
}
);
const { data, meta } = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/tasks",
headers={"Authorization": "Bearer rchd_live_xxxxxxxxxxxx"},
params={"status": "pending"}
)
result = response.json()
```
## Response
```json theme={null}
{
"data": [
{
"id": "task-uuid-1",
"title": "Follow up with John Doe",
"status": "pending",
"priority": "high",
"due_date": "2026-03-25",
"lead_id": "a1b2c3d4-...",
"created_at": "2026-03-21T10:00:00.000Z"
}
],
"meta": {
"page": 1,
"per_page": 25,
"total": 1
}
}
```
### Response Fields
| Field | Type | Description |
| ------------ | -------- | ------------------------------------------------- |
| `id` | uuid | Task unique identifier |
| `title` | string | Task title |
| `status` | string | Task status (`pending`, `completed`, `cancelled`) |
| `priority` | string | Priority level (`low`, `medium`, `high`) |
| `due_date` | string | Due date (YYYY-MM-DD) |
| `lead_id` | uuid | Associated lead ID (if linked) |
| `created_at` | datetime | Creation timestamp |
# Get a Call Log
Source: https://docs.reachedapp.com/get-call-log
GET /v1/call-logs/:id
Retrieves the full details of a specific call.
# Get a Call Log
Retrieves the full details of a specific call, including recording URL, transcript, and disposition.
## Request
`GET /v1/call-logs/:id`
### Path Parameters
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ------------------------------------- |
| `id` | uuid | Yes | The unique identifier of the call log |
## Example
```bash cURL theme={null}
curl -X GET \
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/call-logs/log-uuid-1" \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx"
```
```javascript JavaScript theme={null}
const callId = "log-uuid-1";
const response = await fetch(
`https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/call-logs/${callId}`,
{
headers: {
"Authorization": "Bearer rchd_live_xxxxxxxxxxxx"
}
}
);
const { data } = await response.json();
```
```python Python theme={null}
import requests
call_id = "log-uuid-1"
response = requests.get(
f"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/call-logs/{call_id}",
headers={"Authorization": "Bearer rchd_live_xxxxxxxxxxxx"}
)
data = response.json()
```
## Response
```json theme={null}
{
"data": {
"id": "log-uuid-1",
"lead_id": "a1b2c3d4-...",
"campaign_id": "c1d2e3f4-...",
"agent_id": "user-uuid-1",
"from_number": "+33140000000",
"to_number": "+33612345678",
"duration": 145,
"status": "completed",
"disposition": "meeting_booked",
"recording_url": "https://...",
"transcript": "...",
"notes": "Demo booked for next Thursday",
"started_at": "2026-03-21T14:30:00.000Z",
"ended_at": "2026-03-21T14:32:25.000Z"
}
}
```
```json theme={null}
{
"error": {
"code": "not_found",
"message": "Call log not found"
}
}
```
### Additional Fields (vs. List endpoint)
| Field | Type | Description |
| ------------ | ------ | --------------------------------------- |
| `agent_id` | uuid | ID of the agent who handled the call |
| `transcript` | string | Call transcript (if available) |
| `notes` | string | Notes added by the agent after the call |
# Get a Campaign
Source: https://docs.reachedapp.com/get-campaign
GET /v1/campaigns/:id
Retrieves the full details of a specific campaign.
# Get a Campaign
Retrieves the full details of a specific campaign by its ID.
## Request
`GET /v1/campaigns/:id`
### Path Parameters
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ------------------------------------- |
| `id` | uuid | Yes | The unique identifier of the campaign |
## Example
```bash cURL theme={null}
curl -X GET \
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/campaigns/c1d2e3f4-..." \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx"
```
```javascript JavaScript theme={null}
const campaignId = "c1d2e3f4-...";
const response = await fetch(
`https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/campaigns/${campaignId}`,
{
headers: {
"Authorization": "Bearer rchd_live_xxxxxxxxxxxx"
}
}
);
const { data } = await response.json();
```
```python Python theme={null}
import requests
campaign_id = "c1d2e3f4-..."
response = requests.get(
f"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/campaigns/{campaign_id}",
headers={"Authorization": "Bearer rchd_live_xxxxxxxxxxxx"}
)
data = response.json()
```
## Response
```json theme={null}
{
"data": {
"id": "c1d2e3f4-...",
"name": "Q1 Outbound France",
"status": "active",
"parallel_calls": 3,
"script_template": "Hi {first_name}, ...",
"tags": ["france", "q1"],
"created_at": "2026-03-01T09:00:00.000Z"
}
}
```
```json theme={null}
{
"error": {
"code": "not_found",
"message": "Campaign not found"
}
}
```
### Response Fields
| Field | Type | Description |
| ----------------- | -------- | -------------------------------------- |
| `id` | uuid | Campaign unique identifier |
| `name` | string | Campaign name |
| `status` | string | Current status |
| `parallel_calls` | integer | Number of parallel calls configured |
| `script_template` | string | Call script template with placeholders |
| `tags` | array | Tags associated with the campaign |
| `created_at` | datetime | Creation timestamp |
# Get campaign statistics
Source: https://docs.reachedapp.com/get-stats
GET /v1/campaigns/:id/stats
Returns aggregated statistics for a campaign, including call volumes, connection & conversion rates, a full dispositions breakdown, and a per-agent performance report.
## Request
```text theme={null}
GET /v1/campaigns/:id/stats
```
### Path parameters
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | --------------- |
| `id` | `uuid` | Yes | The campaign ID |
### Example
```bash cURL theme={null}
curl -X GET \
https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/campaigns/c1d2e3f4-.../stats \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx"
```
```javascript JavaScript theme={null}
const campaignId = 'c1d2e3f4-...';
const response = await fetch(
`https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/campaigns/${campaignId}/stats`,
{
headers: {
'Authorization': 'Bearer rchd_live_xxxxxxxxxxxx'
}
}
);
const { data } = await response.json();
```
```python Python theme={null}
import requests
campaign_id = 'c1d2e3f4-...'
response = requests.get(
f'https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/campaigns/{campaign_id}/stats',
headers={'Authorization': 'Bearer rchd_live_xxxxxxxxxxxx'}
)
data = response.json()['data']
```
***
## Response
### Top-level fields
| Field | Type | Description |
| ------------------------- | --------- | ----------------------------------------------- |
| `total_leads` | `integer` | Total leads enrolled in the campaign |
| `called_leads` | `integer` | Leads that have been called at least once |
| `total_calls` | `integer` | Total outbound call attempts |
| `answered_calls` | `integer` | Calls answered by a person (excludes voicemail) |
| `conversations` | `integer` | Calls classified as conversations (≥ 60 s) |
| `total_talk_time_seconds` | `integer` | Total talk time in seconds for all calls |
| `appointments` | `integer` | Leads with an appointment-scheduled outcome |
| `connection_rate` | `float` | `answered_calls / total_calls × 100` (%) |
| `conversion_rate` | `float` | `appointments / conversations × 100` (%) |
| `dispositions` | `array` | Breakdown of call outcomes — see below |
| `agents` | `array` | Per-agent performance — see below |
### `dispositions` array
| Field | Type | Description |
| ------------- | --------- | ------------------------------------------------------------- |
| `disposition` | `string` | The outcome label (e.g. `no-answer`, `appointment-scheduled`) |
| `count` | `integer` | Number of outbound calls with that outcome |
### `agents` array
| Field | Type | Description |
| ------------------------- | --------- | ---------------------------------------------- |
| `agent_id` | `uuid` | User ID of the agent |
| `first_name` | `string` | Agent first name |
| `last_name` | `string` | Agent last name |
| `email` | `string` | Agent email address |
| `total_calls` | `integer` | Total calls made by this agent in the campaign |
| `answered_calls` | `integer` | Answered calls (non-voicemail) |
| `conversations` | `integer` | Conversations (≥ 60 s) |
| `total_talk_time_seconds` | `integer` | Cumulative talk time in seconds |
| `appointments` | `integer` | Appointments booked by this agent |
### Example response
```json theme={null}
{
"data": {
"total_leads": 350,
"called_leads": 280,
"total_calls": 412,
"answered_calls": 198,
"conversations": 87,
"total_talk_time_seconds": 18540,
"appointments": 14,
"connection_rate": 48.1,
"conversion_rate": 16.1,
"dispositions": [
{ "disposition": "no-answer", "count": 214 },
{ "disposition": "conversation", "count": 87 },
{ "disposition": "voicemail-left", "count": 62 },
{ "disposition": "not-interested", "count": 35 },
{ "disposition": "appointment-scheduled", "count": 14 }
],
"agents": [
{
"agent_id": "u1a2b3c4-...",
"first_name": "Sophie",
"last_name": "Bernard",
"email": "sophie@company.com",
"total_calls": 210,
"answered_calls": 105,
"conversations": 48,
"total_talk_time_seconds": 9800,
"appointments": 8
},
{
"agent_id": "u9x8y7z6-...",
"first_name": "Thomas",
"last_name": "Petit",
"email": "thomas@company.com",
"total_calls": 202,
"answered_calls": 93,
"conversations": 39,
"total_talk_time_seconds": 8740,
"appointments": 6
}
]
}
}
```
***
## Errors
| Status | Code | Description |
| ------ | -------------- | ----------------------------------------------------- |
| `401` | `unauthorized` | Missing or invalid API key |
| `404` | `not_found` | Campaign not found or does not belong to your account |
| `500` | `internal` | Unexpected server error |
# Overview
Source: https://docs.reachedapp.com/index
The Reached API lets you programmatically manage leads, campaigns, calls, and tasks.
# Welcome to the Reached API
The Reached API is a RESTful API that exchanges JSON over HTTPS. It allows you to programmatically manage your entire outbound calling workflow -- from creating leads and campaigns to retrieving call logs and triggering AI enrichment.
JSON request and response bodies over HTTPS.
Secure Bearer token authentication with scoped API keys.
300 requests per minute per API key with clear rate limit headers.
## Base URL
All API requests should be made to:
```text theme={null}
https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1
```
You can find your API key in **Settings > API** within the Reached dashboard.
## What can you do with the API?
| Resource | Description |
| ------------- | -------------------------------------------------------------------- |
| **Leads** | Create, list, get, and update leads in your workspace |
| **Campaigns** | List campaigns and add leads to them for parallel dialing |
| **Call Logs** | Retrieve call history with recordings, transcripts, and dispositions |
| **Tasks** | Create and manage follow-up tasks linked to leads |
## Quick Start
Go to **Settings > API** in the Reached dashboard and create a new API key. Your key will start with `rchd_live_`.
```bash theme={null}
curl -X GET \
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/leads?per_page=5" \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx"
```
```json theme={null}
{
"data": [...],
"meta": {
"page": 1,
"per_page": 5,
"total": 142
}
}
```
## Next Steps
Learn how to authenticate your API requests.
Explore the full endpoint reference.
Push leads from Clay directly into Reached campaigns.
Understand request limits and best practices.
# List Call Logs
Source: https://docs.reachedapp.com/list-call-logs
GET /v1/call-logs
Returns a paginated list of call logs with optional filters.
# List Call Logs
Returns a paginated list of call logs with optional filters. Use this endpoint to retrieve call results, dispositions, and recordings.
## Request
`GET /v1/call-logs`
### Query Parameters
| Parameter | Type | Required | Description |
| ------------- | ------- | -------- | ---------------------------------------------------------------- |
| `page` | integer | No | Page number (default: 1) |
| `per_page` | integer | No | Results per page, max 100 (default: 25) |
| `lead_id` | uuid | No | Filter by lead ID |
| `campaign_id` | uuid | No | Filter by campaign ID |
| `disposition` | string | No | Filter by disposition (e.g., `meeting_booked`, `not_interested`) |
| `date_from` | string | No | Start date in ISO 8601 format (e.g., `2026-03-01T00:00:00Z`) |
| `date_to` | string | No | End date in ISO 8601 format |
## Example
```bash cURL theme={null}
curl -X GET \
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/call-logs?campaign_id=c1d2e3f4-...&date_from=2026-03-01T00:00:00Z" \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx"
```
```javascript JavaScript theme={null}
const params = new URLSearchParams({
campaign_id: "c1d2e3f4-...",
date_from: "2026-03-01T00:00:00Z"
});
const response = await fetch(
`https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/call-logs?${params}`,
{
headers: {
"Authorization": "Bearer rchd_live_xxxxxxxxxxxx"
}
}
);
const { data, meta } = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/call-logs",
headers={"Authorization": "Bearer rchd_live_xxxxxxxxxxxx"},
params={
"campaign_id": "c1d2e3f4-...",
"date_from": "2026-03-01T00:00:00Z"
}
)
result = response.json()
```
## Response
```json theme={null}
{
"data": [
{
"id": "log-uuid-1",
"lead_id": "a1b2c3d4-...",
"campaign_id": "c1d2e3f4-...",
"from_number": "+33140000000",
"to_number": "+33612345678",
"duration": 145,
"status": "completed",
"disposition": "meeting_booked",
"recording_url": "https://...",
"started_at": "2026-03-21T14:30:00.000Z",
"ended_at": "2026-03-21T14:32:25.000Z"
}
],
"meta": {
"page": 1,
"per_page": 25,
"total": 1
}
}
```
### Response Fields
| Field | Type | Description |
| --------------- | -------- | -------------------------------------------------------- |
| `id` | uuid | Call log unique identifier |
| `lead_id` | uuid | Associated lead ID |
| `campaign_id` | uuid | Associated campaign ID |
| `from_number` | string | Caller phone number |
| `to_number` | string | Called phone number |
| `duration` | integer | Call duration in seconds |
| `status` | string | Call status (`completed`, `no-answer`, `busy`, `failed`) |
| `disposition` | string | Call outcome disposition |
| `recording_url` | string | URL to the call recording (if available) |
| `started_at` | datetime | Call start timestamp |
| `ended_at` | datetime | Call end timestamp |
### Disposition Values
| Disposition | Description |
| -------------------------- | --------------------------- |
| `meeting_booked` | Meeting or demo scheduled |
| `appointment_scheduled` | Appointment confirmed |
| `callback_requested` | Lead requested a callback |
| `not_interested` | Lead not interested |
| `no_answer` | No one answered |
| `voicemail` | Reached voicemail |
| `voicemail_left` | Voicemail message left |
| `wrong_number` | Wrong number |
| `wrong_person` | Wrong contact |
| `gatekeeper` | Blocked by gatekeeper |
| `no_longer_at_company` | Contact left the company |
| `abandoned_call` | Call abandoned |
| `voice_assistant_filtered` | Filtered by voice assistant |
# Pagination
Source: https://docs.reachedapp.com/pagination
Learn how to paginate through list endpoints.
# Pagination
All list endpoints return paginated results. Use the `page` and `per_page` query parameters to control pagination.
## Query Parameters
| Parameter | Type | Default | Description |
| ---------- | ------- | ------- | ------------------------------------ |
| `page` | integer | 1 | Page number (starts at 1) |
| `per_page` | integer | 25 | Number of results per page (max 100) |
## Response Format
Every paginated response includes a `meta` object with pagination information:
```json theme={null}
{
"data": [...],
"meta": {
"page": 1,
"per_page": 25,
"total": 142
}
}
```
| Field | Type | Description |
| --------------- | ------- | -------------------------------- |
| `meta.page` | integer | Current page number |
| `meta.per_page` | integer | Number of items per page |
| `meta.total` | integer | Total number of matching records |
## Example
Fetch the second page of leads with 10 results per page:
```bash cURL theme={null}
curl -X GET \
"https://api.reachedapp.com/v1/leads?page=2&per_page=10" \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx"
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.reachedapp.com/v1/leads?page=2&per_page=10",
{
headers: {
"Authorization": "Bearer rchd_live_xxxxxxxxxxxx"
}
}
);
const { data, meta } = await response.json();
console.log(`Page ${meta.page} of ${Math.ceil(meta.total / meta.per_page)}`);
```
## Iterating Through All Pages
```javascript JavaScript theme={null}
async function fetchAllLeads(apiKey) {
const baseUrl = "https://api.reachedapp.com/v1/leads";
const allLeads = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}&per_page=100`, {
headers: { "Authorization": `Bearer ${apiKey}` }
});
const { data, meta } = await response.json();
allLeads.push(...data);
hasMore = page * meta.per_page < meta.total;
page++;
}
return allLeads;
}
```
```python Python theme={null}
import requests
def fetch_all_leads(api_key):
base_url = "https://api.reachedapp.com/v1/leads"
all_leads = []
page = 1
while True:
response = requests.get(
f"{base_url}?page={page}&per_page=100",
headers={"Authorization": f"Bearer {api_key}"}
)
result = response.json()
all_leads.extend(result["data"])
if page * result["meta"]["per_page"] >= result["meta"]["total"]:
break
page += 1
return all_leads
```
To minimize the number of requests, use `per_page=100` (the maximum) when fetching all records.
# Add Leads to Campaign
Source: https://docs.reachedapp.com/post-campaign
POST /v1/campaigns/:id/leads
Adds one or more leads to a campaign for calling.
# Add Leads to a Campaign
Adds one or more leads to a campaign for calling. If a lead with the same phone or email already exists, it will be linked to the campaign. Otherwise, a new lead is created automatically.
This is the primary endpoint for **Clay**, **Zapier**, and other automation tool integrations.
## Request
`POST /v1/campaigns/:id/leads`
### Path Parameters
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ------------------------------- |
| `id` | uuid | Yes | The campaign ID to add leads to |
### Body Parameters
| Parameter | Type | Required | Description |
| ---------------------- | ------ | ----------- | -------------------------------------------------------- |
| `leads` | array | Yes | Array of lead objects to add |
| `leads[].phone` | string | Conditional | Phone in E.164 format (required if no email or lead\_id) |
| `leads[].email` | string | Conditional | Email address (required if no phone or lead\_id) |
| `leads[].lead_id` | uuid | Conditional | Existing lead ID (use if lead already exists) |
| `leads[].first_name` | string | No | First name |
| `leads[].last_name` | string | No | Last name |
| `leads[].company_name` | string | No | Company name |
| `leads[].title` | string | No | Job title |
| `leads[].linkedin_url` | string | No | LinkedIn profile URL |
Each lead in the array must include at least one of: `phone`, `email`, or `lead_id`. The API automatically deduplicates leads based on phone number or email within your workspace.
## Example
```bash cURL theme={null}
curl -X POST \
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/campaigns/c1d2e3f4-.../leads" \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"leads": [
{
"first_name": "Marie",
"last_name": "Dupont",
"phone": "+33698765432",
"company_name": "TechCo",
"title": "Head of Sales"
},
{
"first_name": "Pierre",
"last_name": "Martin",
"phone": "+33612345678",
"email": "pierre@startup.io"
}
]
}'
```
```javascript JavaScript theme={null}
const campaignId = "c1d2e3f4-...";
const response = await fetch(
`https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/campaigns/${campaignId}/leads`,
{
method: "POST",
headers: {
"Authorization": "Bearer rchd_live_xxxxxxxxxxxx",
"Content-Type": "application/json"
},
body: JSON.stringify({
leads: [
{
first_name: "Marie",
last_name: "Dupont",
phone: "+33698765432",
company_name: "TechCo",
title: "Head of Sales"
},
{
first_name: "Pierre",
last_name: "Martin",
phone: "+33612345678",
email: "pierre@startup.io"
}
]
})
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
campaign_id = "c1d2e3f4-..."
response = requests.post(
f"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/campaigns/{campaign_id}/leads",
headers={
"Authorization": "Bearer rchd_live_xxxxxxxxxxxx",
"Content-Type": "application/json"
},
json={
"leads": [
{
"first_name": "Marie",
"last_name": "Dupont",
"phone": "+33698765432",
"company_name": "TechCo",
"title": "Head of Sales"
},
{
"first_name": "Pierre",
"last_name": "Martin",
"phone": "+33612345678",
"email": "pierre@startup.io"
}
]
}
)
data = response.json()
```
## Response
```json theme={null}
{
"data": {
"campaign_id": "c1d2e3f4-...",
"results": [
{
"lead_id": "new-lead-uuid-1",
"status": "added"
},
{
"lead_id": "existing-lead-uuid",
"status": "already_in_campaign"
}
]
}
}
```
### Result Statuses
| Status | Description |
| --------------------- | ------------------------------------------------------- |
| `added` | Lead was successfully added to the campaign |
| `already_in_campaign` | Lead was already in the campaign (no duplicate created) |
If a lead object is missing all three identifiers (`phone`, `email`, `lead_id`), it will be skipped and an error will be returned in the results array for that entry.
# Lead
Source: https://docs.reachedapp.com/post-lead
POST /plantsleads
Creates a new lead in your workspace.
# Create a Lead
Creates a new lead in your workspace. At least a phone number or email address is required.
## Request
`POST /v1/leads`
### Body Parameters
| Parameter | Type | Required | Description |
| ------------------ | ------ | ----------- | --------------------------------------------------- |
| `first_name` | string | No | First name of the lead |
| `last_name` | string | No | Last name of the lead |
| `email` | string | Conditional | Email address (required if no phone) |
| `phone` | string | Conditional | Phone number in E.164 format (required if no email) |
| `company_name` | string | No | Company or organization name |
| `title` | string | No | Job title |
| `linkedin_url` | string | No | LinkedIn profile URL |
| `company_website` | string | No | Company website URL |
| `job_description` | string | No | Job description or role details |
| `company_size` | string | No | Company size range (e.g., "11-50") |
| `custom_variables` | object | No | Key-value pairs for custom data |
| `metadata` | object | No | Additional metadata as JSON |
At least one of `phone` or `email` must be provided. Phone numbers should be in E.164 format (e.g., `+33612345678`).
## Example
```bash cURL theme={null}
curl -X POST \
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/leads" \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"first_name": "John",
"last_name": "Doe",
"email": "john@acme.com",
"phone": "+33612345678",
"company_name": "Acme Corp",
"title": "VP Sales"
}'
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/leads",
{
method: "POST",
headers: {
"Authorization": "Bearer rchd_live_xxxxxxxxxxxx",
"Content-Type": "application/json"
},
body: JSON.stringify({
first_name: "John",
last_name: "Doe",
email: "john@acme.com",
phone: "+33612345678",
company_name: "Acme Corp",
title: "VP Sales"
})
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
"https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/leads",
headers={
"Authorization": "Bearer rchd_live_xxxxxxxxxxxx",
"Content-Type": "application/json"
},
json={
"first_name": "John",
"last_name": "Doe",
"email": "john@acme.com",
"phone": "+33612345678",
"company_name": "Acme Corp",
"title": "VP Sales"
}
)
data = response.json()
```
## Response
```json theme={null}
{
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"first_name": "John",
"last_name": "Doe",
"email": "john@acme.com",
"phone": "+33612345678",
"company_name": "Acme Corp",
"title": "VP Sales",
"status": "pending",
"source": "api",
"created_at": "2026-03-21T10:00:00.000Z"
}
}
```
```json theme={null}
{
"error": {
"code": "bad_request",
"message": "At least phone or email is required"
}
}
```
# Rate Limiting
Source: https://docs.reachedapp.com/rate-limiting
Understand API rate limits and how to handle them.
# Rate Limiting
The API enforces a rate limit of **300 requests per minute** per API key. Rate limit information is included in every response via HTTP headers.
## Rate Limit Headers
Every API response includes the following headers:
| Header | Description |
| ----------------------- | ------------------------------------- |
| `X-RateLimit-Limit` | Maximum requests per window (300) |
| `X-RateLimit-Remaining` | Requests remaining in current window |
| `X-RateLimit-Reset` | Unix timestamp when the window resets |
## Handling Rate Limits
When you exceed the rate limit, the API returns a `429 Too Many Requests` response:
```json theme={null}
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Max 300 requests per minute."
}
}
```
## Best Practices
Check `X-RateLimit-Remaining` in responses to track your usage before hitting the limit.
When you receive a 429, wait until the `X-RateLimit-Reset` timestamp before retrying.
Use the bulk add leads endpoint to add multiple leads in a single request instead of one-by-one.
Cache GET responses locally when data does not change frequently.
## Retry Strategy Example
```javascript JavaScript theme={null}
async function apiCallWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const resetAt = response.headers.get("X-RateLimit-Reset");
const waitMs = resetAt
? (parseInt(resetAt) * 1000) - Date.now()
: 60000;
await new Promise(resolve => setTimeout(resolve, Math.max(waitMs, 1000)));
continue;
}
return response;
}
throw new Error("Max retries exceeded");
}
```
```python Python theme={null}
import time
import requests
def api_call_with_retry(url, headers, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 429:
reset_at = int(response.headers.get("X-RateLimit-Reset", 0))
wait_seconds = max(reset_at - time.time(), 1)
time.sleep(wait_seconds)
continue
return response
raise Exception("Max retries exceeded")
```
## Webhooks vs Polling
If you are polling the API repeatedly to detect new calls, updated lead statuses, or completed campaigns, consider using **webhooks** instead. Webhooks push events to your server the moment they happen — no quota consumed, no delay.
Register a webhook endpoint once and receive real-time `POST` payloads for every event you care about:
```bash theme={null}
# Register a webhook
curl -X POST \
https://kdjmltmhxvvmiuehafgl.supabase.co/functions/v1/api-gateway/v1/webhooks \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/reached-webhook",
"events": ["call.completed", "lead.updated", "campaign.completed"],
"secret": "your_signing_secret"
}'
```
Each delivery includes an `X-Reached-Signature` header (HMAC-SHA256) so you can verify the payload is genuine.
| Approach | Requests consumed | Latency |
| ------------------ | ------------------------- | ------------- |
| Polling every 10 s | \~6 req/min per resource | Up to 10 s |
| Polling every 1 s | \~60 req/min per resource | Up to 1 s |
| **Webhooks** | **0 req/min** | **Real-time** |
Use the REST API for on-demand reads and writes; use webhooks for everything event-driven.
# Lead
Source: https://docs.reachedapp.com/rest-api/leads/get-lead
GET /plantsleads
Retrieves the full details of a specific lead.
# Get a Lead
Retrieves the full details of a specific lead by its ID.
## Request
`GET /v1/leads/:id`
### Path Parameters
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | --------------------------------- |
| `id` | uuid | Yes | The unique identifier of the lead |
## Example
```bash cURL theme={null}
curl -X GET \
"https://YOUR_PROJECT.supabase.co/functions/v1/api-gateway/v1/leads/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx"
```
```javascript JavaScript theme={null}
const leadId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
const response = await fetch(
`https://YOUR_PROJECT.supabase.co/functions/v1/api-gateway/v1/leads/${leadId}`,
{
headers: {
"Authorization": "Bearer rchd_live_xxxxxxxxxxxx"
}
}
);
const { data } = await response.json();
```
```python Python theme={null}
import requests
lead_id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
response = requests.get(
f"https://YOUR_PROJECT.supabase.co/functions/v1/api-gateway/v1/leads/{lead_id}",
headers={"Authorization": "Bearer rchd_live_xxxxxxxxxxxx"}
)
data = response.json()
```
## Response
```json theme={null}
{
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"first_name": "John",
"last_name": "Doe",
"email": "john@acme.com",
"phone": "+33612345678",
"company_name": "Acme Corp",
"title": "VP Sales",
"status": "pending",
"call_outcome": null,
"total_calls": 0,
"ai_enrichment": null,
"custom_variables": {},
"created_at": "2026-03-21T10:00:00.000Z",
"updated_at": "2026-03-21T10:00:00.000Z"
}
}
```
```json theme={null}
{
"error": {
"code": "not_found",
"message": "Lead not found"
}
}
```
### Response Fields
| Field | Type | Description |
| ------------------ | -------- | --------------------------------- |
| `id` | uuid | Unique identifier |
| `first_name` | string | First name |
| `last_name` | string | Last name |
| `email` | string | Email address |
| `phone` | string | Phone number (E.164) |
| `company_name` | string | Company name |
| `title` | string | Job title |
| `status` | string | Current status |
| `call_outcome` | string | Last call outcome disposition |
| `total_calls` | integer | Number of calls made to this lead |
| `ai_enrichment` | string | AI enrichment data (if enriched) |
| `custom_variables` | object | Custom key-value data |
| `created_at` | datetime | Creation timestamp |
| `updated_at` | datetime | Last update timestamp |
# Clay Integration
Source: https://docs.reachedapp.com/tools/clay
Push leads from Clay tables directly into Reached campaigns for parallel dialing.
# Clay Integration
Push leads from Clay tables directly into Reached campaigns for parallel dialing. This guide walks you through the full setup.
In Reached, go to **Settings > API** and click **Create Key**. Name it "Clay" and copy the generated key.
Your key starts with `rchd_live_` -- store it securely.
Use the List Campaigns endpoint to find the ID of the campaign you want to push leads into:
```bash theme={null}
curl -X GET \
"https://api.reachedapp.com/v1/campaigns?status=active" \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx"
```
Copy the `id` field from the campaign you want to target.
In your Clay table, add an **HTTP Action** step with the following configuration:
| Setting | Value |
| ---------------- | ----------------------------------------------------------- |
| **Method** | POST |
| **URL** | `https://api.reachedapp.com/v1/campaigns/CAMPAIGN_ID/leads` |
| **Header** | `Authorization: Bearer rchd_live_xxxxxxxxxxxx` |
| **Content-Type** | `application/json` |
Map your Clay columns to the request body:
```json theme={null}
{
"leads": [
{
"first_name": "{{first_name}}",
"last_name": "{{last_name}}",
"phone": "{{phone}}",
"email": "{{email}}",
"company_name": "{{company}}",
"title": "{{job_title}}",
"linkedin_url": "{{linkedin_url}}"
}
]
}
```
After calls are made, use the Call Logs endpoint to pull results back into Clay:
```bash theme={null}
curl -X GET \
"https://api.reachedapp.com/v1/call-logs?campaign_id=CAMPAIGN_ID&date_from=2026-03-21T00:00:00Z" \
-H "Authorization: Bearer rchd_live_xxxxxxxxxxxx"
```
Each call log includes the `disposition`, `duration`, `recording_url`, and timestamps.
## Key Endpoints for Clay
| Method | Endpoint | Use Case |
| ------ | ------------------------- | --------------------------------------- |
| `POST` | `/v1/campaigns/:id/leads` | Push leads into a campaign |
| `GET` | `/v1/call-logs` | Pull call results and dispositions |
| `GET` | `/v1/leads/:id` | Get full lead details with call outcome |
| `POST` | `/v1/leads/:id/enrich` | Trigger AI enrichment for a lead |
## Deduplication
The API automatically handles deduplication:
* If a lead with the same **phone number** already exists in your workspace, the existing lead is linked to the campaign
* If a lead with the same **email** already exists, the existing lead is linked to the campaign
* If the lead is already in the campaign, it will be returned with status `already_in_campaign`
This means you can safely push the same Clay table multiple times without creating duplicates.
## Zapier & Make
The same API endpoints work with any HTTP-capable automation tool:
* **Zapier**: Use the "Webhooks by Zapier" action with a POST request
* **Make (Integromat)**: Use the "HTTP > Make a request" module
* **n8n**: Use the "HTTP Request" node
The configuration is identical -- just use the same URL, headers, and body format shown above.