# Connect your backend to HeyRik

> Base URL and authentication, the results webhook HeyRik sends after every call, the live-leads webhook that feeds a campaign from your own system, how lead fields reach the agent, and the campaign lifecycle.

Source: https://www.heyrik.com/docs/guides/integrate-backend

Everything your server needs: the base URL and auth, the results webhook for every finished call, the live-leads webhook that feeds a campaign from your own system, and how lead fields reach the agent.

8 minute read Practical examples included

Your server talks to HeyRik in two directions. It **calls the API** to create agents and campaigns and to push leads, and HeyRik **calls your server** with the result of every finished call. This page is the contract for both — copy the examples and you are integrated.

**Two webhooks, two directions**

The _results webhook_ is a URL you own that HeyRik POSTs to after each call. The _live-leads webhook_ is a URL HeyRik owns that your system POSTs leads to. They are independent — use either or both.

## Base URL, authentication, envelopes

The same three rules apply to every request.

### Base URL

https://server.heyrik.com/api/v1 — every path in the API reference is relative to this.

### Authentication

Authorization: Bearer <key> or X-API-Key: <key>. Keys are created in the dashboard under API keys and only ever see their own account.

### Envelopes

Success is { "data": … }. Errors are { "error": "<machine\_code>", "hint"?: "…" } with a matching HTTP status. Lists may add total, page, limit.

### Rate limits

API-key traffic is throttled per key (429 rate\_limited — back off and retry). The live-leads webhook allows 600 posts per minute per source IP.

**A minimal client (Node.js)**

```
const api = async (path, init = {}) => {
  const r = await fetch("https://server.heyrik.com/api/v1" + path, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.HEYRIK_API_KEY}`,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });
  const body = await r.json();
  if (!r.ok) throw new Error(`${r.status} ${body.error}: ${body.hint ?? ""}`);
  return body.data;
};
```

## Results webhook — every finished call, delivered to you

Set webhook\_url on a campaign. HeyRik POSTs JSON to it when a lead's call finishes and once more when the whole campaign completes.

**Set the URL (create or PATCH a campaign)**

```
curl -X PATCH https://server.heyrik.com/api/v1/campaigns/cmp_4a19 \
  -H "Authorization: Bearer $HEYRIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "webhook_url": "https://api.acme.com/heyrik/results" }'
```

**What arrives after each call — event: contact\_completed**

```
{
  "event": "contact_completed",
  "campaign_id": "cmp_4a19",
  "campaign_name": "July outreach",
  "contact_id": "cct_9b1e",
  "phone": "+919876543210",
  "status": "completed",
  "outcome": "completed",
  "attempts": 1,
  "category": "hot",
  "variables": { "name": "Ravi", "order_id": "AC-8842" },
  "call_id": "call_8eab93e5",
  "call": {
    "summary": "Ravi confirmed the order and asked for a callback on Monday.",
    "outcome": "completed",
    "duration_ms": 214000,
    "recording_url": "https://example.com/rec.mp3",
    "extracted_variables": { "callback_requested": "yes", "interest_level": "high" }
  },
  "ts": "2026-08-24T13:41:07.512Z"
}
```

**When the run finishes — event: campaign\_completed**

```
{
  "event": "campaign_completed",
  "campaign_id": "cmp_4a19",
  "campaign_name": "July outreach",
  "counts": { "pending": 0, "calling": 0, "completed": 231, "failed": 19, "retry": 0, "total": 250 },
  "ts": "2026-08-24T18:02:44.001Z"
}
```

### Respond 2xx quickly

HeyRik waits up to 12 seconds and does not retry a failed delivery. Acknowledge first, then do your work in a queue.

### Make it idempotent

Key your storage on contact\_id + call\_id. A lead with retries produces one event per attempt that ended; status is the final one for that attempt.

### Read the fields you asked for

call.extracted\_variables carries the agent's output variables and any campaign extraction\_schema fields; category is the classify bucket, if you set one.

### Keep the URL private

The webhook has no signature yet — use an unguessable path or a query token you validate, and only accept HTTPS.

**A handler that stays fast (Express)**

```
app.post("/heyrik/results", express.json(), async (req, res) => {
  const ev = req.body;
  res.sendStatus(200);                       // ack first — HeyRik times out at 12 s

  if (ev.event === "contact_completed") {
    await queue.add("heyrik-result", {
      key: `${ev.contact_id}:${ev.call_id}`,   // idempotency
      phone: ev.phone,
      outcome: ev.outcome,
      fields: ev.call?.extracted_variables ?? {},
      recording: ev.call?.recording_url ?? null,
    });
  } else if (ev.event === "campaign_completed") {
    await notifyOps(`Campaign ${ev.campaign_name} done: ${JSON.stringify(ev.counts)}`);
  }
});
```

## Live-leads webhook — feed a campaign from your own system

Every campaign has its own ingest URL and token. Your CRM, form, or ad platform posts leads to it as they arrive; HeyRik dials them inside the calling window.

### Copy the URL

Open the campaign in the dashboard → Live leads webhook. The URL carries the campaign's own token (cin\_…), so no account API key is needed — and a leaked token exposes only that one campaign.

### Send the token your way

?token=cin\_… in the URL, the X-Campaign-Token header, or token in the JSON body — whichever your tool supports.

### Rotate when in doubt

Rotate from the campaign page; the old token stops working immediately.

**Post one lead or many**

```
curl -X POST "https://server.heyrik.com/api/campaigns/cmp_4a19/ingest?token=cin_4ead0ca15938393bbb0eac35f6" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      { "phone": "+919876543210", "fullName": "Ravi Kumar", "company": "Acme", "priority": 5 },
      { "phone": "+919123456789", "fullName": "Asha Patel", "company": "Nimbus" }
    ]
  }'
```

**Response**

```
{
  "data": { "added": 2, "skipped": 0, "rounded": 0, "total": 252, "reopened": false }
}
```

### A single lead works too

Post { "phone": "+91…", "name": "…" } without the contacts wrapper. Add "fast": true to a lead (or the batch) to dial it before everything else.

### It reopens finished campaigns

A campaign is continuous: posting to a completed campaign sets it running again and the new leads are dialled. Only a cancelled campaign refuses leads (409 campaign\_closed).

### Numbers must be complete

Send phones as strings in E.164 (+919876543210). A value like 9.17993E+11 — Excel's rounded form — has lost digits; it is counted in rounded and skipped, never dialled.

## How lead fields reach the agent

Every field you send with a lead is available to the agent as a {{variable}}. You do not have to match the agent's names.

An agent declares **input variables** — say `prospect_name`,`prospect_company`, `industry_segment`. When a campaign dials a lead, HeyRik maps the lead's fields to those variables **by meaning**: a field called `fullName`, `Decision Maker` or `Contact person` all fill `prospect_name`. The mapping is worked out once per campaign (an AI reads the field names and a few sample values), stored on the campaign, and applied to every lead — uploaded, posted to the webhook, or added by hand. A field named exactly like the variable is used as-is.

**What you send**

```
{
  "phone": "+919876543210",
  "Decision Maker": "Aju Thomas",
  "Company": "CIRIL",
  "Segment": "Real Estate & Construction",
  "Role / Headline": "Corporate Real Estate Professional",
  "Location": "Hyderabad, Telangana, India"
}
```

**What the agent's prompt sees**

```
prospect_name      = Aju Thomas
prospect_company   = CIRIL
industry_segment   = Real Estate & Construction
prospect_role      = Corporate Real Estate Professional
prospect_location  = Hyderabad, Telangana, India
agent_name         = (the variable's default)
```

**01**A variable with no matching field falls back to its default; a variable with neither is left out of the sentence cleanly.**02**Send an empty string to drop a field; keys named priority, fast and token are control fields, not variables.**03**The agent's greeting is generated from its instructions with these values — it opens with the lead's real name in the agent's language.**04**Output variables (what the agent extracts on the call) come back to you in call.extracted\_variables on the results webhook and on GET /campaigns/{id}/contacts.

## Campaign lifecycle

What each status means for your integration.

[**draft** — Created; nothing dialled yet. — POST /campaigns/{id}/start to begin](https://www.heyrik.com/docs/api-reference#campaigns)[**scheduled** — Starts itself at schedule\_at. — PATCH schedule\_at: null to start now](https://www.heyrik.com/docs/api-reference#campaigns)[**running** — Dialling inside call\_window\_start–end on call\_days; retries per max\_retries. — Push leads any time — next tick picks them up](https://www.heyrik.com/docs/api-reference#campaigns)[**paused** — Holds; the in-flight call finishes. — POST /start resumes](https://www.heyrik.com/docs/api-reference#campaigns)[**completed** — Nothing left to dial — but not closed. — New leads reopen it automatically](https://www.heyrik.com/docs/api-reference#campaigns)[**cancelled** — Terminal. Results are kept; leads are refused. — Create a new campaign](https://www.heyrik.com/docs/api-reference#campaigns)

## Prefer polling? Read results instead

Everything the webhook delivers is also readable, so a batch job can reconcile without receiving anything.

**Progress, then the finished leads**

```
# progress + counts
curl https://server.heyrik.com/api/v1/campaigns/cmp_4a19 -H "Authorization: Bearer $HEYRIK_API_KEY"

# every finished lead with outcome, category, extracted fields
curl "https://server.heyrik.com/api/v1/campaigns/cmp_4a19/contacts?status=completed&limit=500" \
  -H "Authorization: Bearer $HEYRIK_API_KEY"

# one lead with its recording + transcript
curl https://server.heyrik.com/api/v1/campaigns/cmp_4a19/contacts/cct_9b1e -H "Authorization: Bearer $HEYRIK_API_KEY"

# or the whole run as a CSV
curl https://server.heyrik.com/api/v1/campaigns/cmp_4a19/export -H "Authorization: Bearer $HEYRIK_API_KEY" -o results.csv
```

401`api_key_required`

No key, or a revoked one. Create a new key in the dashboard.

402`insufficient_credits`

Top up before starting a run; \`need\` tells you the minimum.

404`campaign_not_found`

Wrong id, or a campaign owned by another account.

409`campaign_closed`

The campaign was cancelled — create a new one.

429`rate_limited`

Back off with jitter and retry.

## Go-live checklist

Ten minutes of checks that prevent a bad first campaign.

**01**The key lives on your server only; requests carry Authorization: Bearer.**02**webhook\_url is HTTPS, answers 2xx within a second, and stores by contact\_id + call\_id.**03**Your lead posts send phone as an E.164 string — never a spreadsheet number.**04**Lead fields cover the agent's input variables (compare a lead against GET /agents/{id}).**05**call\_window\_start / call\_window\_end / timezone are set so nobody is called at night.**06**You checked GET /balance — a run that runs out of credits stops mid-way.**07**You watched one real call end to end: results webhook received, recording playable, extracted fields present.

[Campaigns API — Every campaign endpoint with requests and responses. — Continue](https://www.heyrik.com/docs/api-reference#campaigns)

[CRM & leads API — Keep a pipeline in HeyRik and dial from it. — Continue](https://www.heyrik.com/docs/api-reference#crm)

[Design the agent — Declare the input and output variables your leads will fill. — Continue](https://www.heyrik.com/docs/guides/design-with-ai)
