# Build on the phone layer

Ringkit gives a business a phone number and an AI receptionist that knows it. The receptionist answers every call, transfers callers to the business's own team when the rules say so, and takes a message otherwise. Everything the dashboard does is an HTTP call, so a developer or an AI agent can set a business up end to end, then hand it to the owner. There's no plan: $1.99/month per number and $0.20 a minute while the receptionist is on a call.

There are no SDKs yet. Use `curl` or `fetch`, generate a client from the [OpenAPI document](https://api.ringkit.com/openapi.json), or connect an agent over [MCP](#agents).

01 · Quickstart

## Quickstart

Ten calls from nothing to a funded business with a live number. Most people finish in **under an hour**. You don't need an account or a key to start: step 1 returns one. The examples use a made-up business, Hidden Valley Builders, a remodeling contractor in Ventura County.

### 1. Create the business

No key needed. The response carries the business and its API key. The key is shown once: store it now.

Request

```
curl https://api.ringkit.com/v1/businesses \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: hvb-create-1" \
  -d '{
    "name": "Hidden Valley Builders",
    "timezone": "America/Los_Angeles",
    "vertical": "remodeling",
    "website": "hiddenvalleybuilders.example",
    "ownerEmail": "mike@hiddenvalleybuilders.example",
    "ownerPhone": "+18055550134"
  }'
```

Response · 201

```
{
  "business": {
    "id": "biz_4f8k2m9q7x1c3v5n",
    "name": "Hidden Valley Builders",
    "timezone": "America/Los_Angeles",
    "vertical": "remodeling",
    "website": "https://hiddenvalleybuilders.example",
    "ownerEmail": "mike@hiddenvalleybuilders.example",
    "ownerPhone": "+18055550134",
    "notify": [],
    "status": "active",
    "knowledgeTier": "starter",
    "voice": "Ashley",
    "createdVia": "api",
    "createdAt": "2026-10-27T17:04:11.000Z",
    "updatedAt": "2026-10-27T17:04:11.000Z"
  },
  "apiKey": "rk_…"
}
```

```
export RINGKIT_API_KEY=rk_…
export BIZ=biz_4f8k2m9q7x1c3v5n
```

New businesses get a **$5 trial credit** for calls (one per owner email). It doesn't pay for the Pro plan, and pays for a number only during the 7-day trial on a business's first number once the owner has signed in.

### 2. Teach it the business

Pick any mix of four ways. The fastest is a background import of the website:

```
curl https://api.ringkit.com/v1/businesses/$BIZ/knowledge/imports \
  -H "Authorization: Bearer $RINGKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://hiddenvalleybuilders.example" }'
```

Response · 202

```
{
  "id": "kimp_9w2r5t8y1u4i7o3p",
  "url": "https://hiddenvalleybuilders.example",
  "status": "queued",
  "createdAt": "2026-10-27T17:04:20.000Z"
}
```

Poll `GET /v1/businesses/$BIZ/onboarding` until `latestImport.status` is `done` or `failed`. The import fills only fields that are still empty and never touches uploaded documents.

```
{
  "step": "number",
  "hasNumber": false,
  "knowledgeReady": true,
  "latestImport": {
    "id": "kimp_9w2r5t8y1u4i7o3p",
    "url": "https://hiddenvalleybuilders.example",
    "status": "done",
    "estimatedTokens": 3120,
    "found": { "hours": true, "services": 6, "faqs": 4, "policies": 2, "summary": true },
    "createdAt": "2026-10-27T17:04:20.000Z",
    "finishedAt": "2026-10-27T17:05:02.000Z"
  }
}
```

**Or write the profile yourself** with `PUT /v1/businesses/$BIZ/knowledge` (a full replace; `PATCH` merges):

```
{
  "summary": "Licensed general contractor in Ventura County: kitchen and bath remodels, additions and repairs.",
  "hours": {
    "regular": {
      "mon": [{ "open": "07:00", "close": "17:00" }],
      "tue": [{ "open": "07:00", "close": "17:00" }],
      "wed": [{ "open": "07:00", "close": "17:00" }],
      "thu": [{ "open": "07:00", "close": "17:00" }],
      "fri": [{ "open": "07:00", "close": "16:00" }],
      "sat": [],
      "sun": []
    }
  },
  "services": [
    { "name": "Kitchen remodel", "description": "Design, permits and build", "priceNote": "free estimates" },
    { "name": "Repairs", "priceUsd": 95, "priceNote": "per hour labor" }
  ],
  "faqs": [
    { "q": "Do you give free estimates?", "a": "Yes, on-site estimates are free anywhere in Ventura County." }
  ],
  "policies": [
    { "topic": "Deposits", "text": "Remodels start with a 10% deposit once the estimate is signed." }
  ]
}
```

**Or upload documents** (PDF, DOCX, TXT, Markdown, HTML; 10 MB each) with `POST /v1/businesses/$BIZ/knowledge/documents`. Text is extracted at upload and used on every call.

```
{ "fileName": "price-list.pdf", "contentBase64": "JVBERi0xLjcK…" }
```

**Or talk it through** with the setup chat. `POST /v1/businesses/$BIZ/setup-chat` starts a conversation and returns its `id` (`schat_…`) with the assistant's opening message. Answer with `POST /v1/businesses/$BIZ/setup-chat/:conversationId/messages`; the assistant saves hours, services, policies and transfers as you go and lists each write in `messages[].writes`.

```
{ "text": "We're closed weekends. Emergencies like a burst pipe go straight to Mike's cell." }
```

### 3. Pick a voice

`GET /v1/voices` lists the voices and the default. Set one on the business:

```
curl -X PATCH https://api.ringkit.com/v1/businesses/$BIZ \
  -H "Authorization: Bearer $RINGKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "voice": "Sarah" }'
```

### 4. Set the routing policy

Decide which calls the receptionist transfers to the business's own team, and to which number. `PUT` replaces the whole policy; fields you leave out take their defaults.

```
curl -X PUT https://api.ringkit.com/v1/businesses/$BIZ/routing \
  -H "Authorization: Bearer $RINGKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "ai_only",
    "transferTargets": [
      { "id": "mike", "label": "Mike (owner)", "number": "+18055550134", "whenToUse": "Emergencies and new jobs over $10k" }
    ]
  }'
```

### 5. Fund the wallet

The wallet is prepaid. A top-up returns a Stripe Checkout link for a person to open and pay; the minimum is $10. Paying is the only step that needs a person, and the number in the next step needs it, so ask early.

```
curl https://api.ringkit.com/v1/businesses/$BIZ/wallet/top-ups \
  -H "Authorization: Bearer $RINGKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: hvb-topup-1" \
  -d '{ "amountCents": 2000 }'
```

Response · 200

```
{
  "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_live_…",
  "balanceCents": 500
}
```

Once they've paid, `GET /v1/businesses/$BIZ/wallet` shows `balanceCents` (trial credit included) and `trialCreditCents` (the unspent trial part).

### 6. Get a number

```
curl "https://api.ringkit.com/v1/numbers/search?areaCode=805" \
  -H "Authorization: Bearer $RINGKIT_API_KEY"
```

```
{
  "numbers": [
    { "number": "+18055550142", "region": "Ventura, CA", "monthlyCents": 199, "provider": "livekit-telnyx" }
  ]
}
```

```
curl https://api.ringkit.com/v1/businesses/$BIZ/numbers \
  -H "Authorization: Bearer $RINGKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: hvb-number-1" \
  -d '{ "number": "+18055550142" }'
```

A number is $1.99/month from the wallet and needs **$2 of paid credit** (trial credit doesn't count, except that a business's first number can run a 7-day trial on it once the owner has signed in): without it you get `402 funding_required`, with `availableCents`, `requiredCents` and `trialCreditCents` in `details`. Nobody has to sign in first. Publish the new number, or keep the one customers already know and forward it to the Ringkit number with your carrier's call forwarding.

### 7. Try it

There is no sandbox. Test in text first: `POST /v1/businesses/$BIZ/simulate` runs the real receptionist with typed caller lines. An empty `callerText` gets the greeting; send the returned `callId` with each next line.

```
{ "callerText": "" }
{ "callId": "call_2h6j9k3l7z1x5c8v", "callerText": "Do you do free estimates in Thousand Oaks?" }
```

```
{
  "callId": "call_2h6j9k3l7z1x5c8v",
  "aiText": "We do. Estimates are free anywhere in Ventura County, Thousand Oaks included. Can I take your name and a good number to call you back?",
  "ended": false,
  "intent": "estimate_request",
  "escalated": false
}
```

Then a real call. Ringkit only rings numbers the business configured (`ownerPhone`, a transfer target or an SMS notify target) and has verified. Verify places a call that reads a 6-digit code to whoever answers (their consent to be called); confirm sends it back:

```
POST /v1/businesses/$BIZ/destinations/verify    { "number": "+18055550134" }
POST /v1/businesses/$BIZ/destinations/confirm   { "number": "+18055550134", "code": "482913" }
POST /v1/businesses/$BIZ/test-call              { "to": "+18055550134" }
```

Test calls ring from the business's Ringkit number, run on the AI and bill at the AI rate. They are capped at **5 per business per day** (and 3 outbound calls per number per day).

### 8. Read calls and summaries

`GET /v1/businesses/$BIZ/calls?limit=25` returns `{ items, nextCursor }`; pass `?cursor=` for the next page until `nextCursor` is `null`. One call, with its transcript and summary:

```
curl https://api.ringkit.com/v1/businesses/$BIZ/calls/call_7n2k9x4m1q8c5v3b \
  -H "Authorization: Bearer $RINGKIT_API_KEY"
```

```
{
  "call": {
    "id": "call_7n2k9x4m1q8c5v3b",
    "businessId": "biz_4f8k2m9q7x1c3v5n",
    "direction": "inbound",
    "from": "+18055550173",
    "to": "+18055550142",
    "status": "completed",
    "provider": "livekit-telnyx",
    "providerCallId": "SCL_4yQd9KpW2mVt",
    "startedAt": "2026-10-27T18:12:03.000Z",
    "answeredAt": "2026-10-27T18:12:04.000Z",
    "endedAt": "2026-10-27T18:15:37.000Z",
    "endReason": "caller_hangup",
    "segments": [
      { "handler": "ai", "startedAt": "2026-10-27T18:12:04.000Z", "endedAt": "2026-10-27T18:15:37.000Z" }
    ],
    "summary": {
      "intent": "estimate_request",
      "outcome": "message_taken",
      "callerName": "Dana Ruiz",
      "summary": "Dana wants an estimate to extend the Oakridge kitchen remodel into the dining room. Free after 3pm on weekdays.",
      "message": {
        "callerName": "Dana Ruiz",
        "callbackNumber": "+18055550173",
        "message": "Estimate for extending the Oakridge kitchen remodel into the dining room. Call after 3pm.",
        "urgency": "normal"
      },
      "followUpNeeded": true,
      "sentiment": "positive"
    },
    "billedCents": 51
  },
  "transcript": [
    { "seq": 0, "speaker": "ai", "text": "Thanks for calling Hidden Valley Builders. How can I help?", "at": "2026-10-27T18:12:04.000Z" },
    { "seq": 1, "speaker": "caller", "text": "Hi, I'd like to get an estimate on extending a kitchen remodel.", "at": "2026-10-27T18:12:09.000Z" }
  ]
}
```

### 9. Subscribe to webhooks

```
curl https://api.ringkit.com/v1/businesses/$BIZ/webhooks \
  -H "Authorization: Bearer $RINGKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hiddenvalleybuilders.example/hooks/ringkit",
    "events": ["call.completed", "message.taken", "balance.low"]
  }'
```

The response is `{ id, secret }`. The signing secret is shown once; see [Webhooks](#webhooks) to verify deliveries.

### 10. Hand it to the owner (optional)

The line already runs; nothing above needed the owner to sign in. When they want the dashboard, this emails them a sign-in link:

```
curl https://api.ringkit.com/v1/businesses/$BIZ/owners/invite \
  -H "Authorization: Bearer $RINGKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "mike@hiddenvalleybuilders.example" }'
```

```
{ "ok": true, "email": "mike@hiddenvalleybuilders.example", "magicLinkSent": true }
```

Leave out `email` to use the business's `ownerEmail`; 5 invites per business per hour. When the owner signs in at [app.ringkit.com](https://app.ringkit.com), the business gets `ownerSignedInAt` and the dashboard shows the business as **set up by an agent** and lets them rotate the agent's key. The agent can keep running the line either way.

### The same thing in JavaScript

No SDK needed. A small `fetch` helper covers every route:

JavaScript

```
const API = "https://api.ringkit.com/v1";

async function ringkit(method, path, { key, body, idempotencyKey } = {}) {
  const res = await fetch(API + path, {
    method,
    headers: {
      "Content-Type": "application/json",
      ...(key ? { Authorization: `Bearer ${key}` } : {}),
      ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json();
  if (!res.ok) throw new Error(`${json.error.code}: ${json.error.message} (${json.error.hint ?? json.error.docs})`);
  return json;
}

const { business, apiKey } = await ringkit("POST", "/businesses", {
  idempotencyKey: "hvb-create-1",
  body: {
    name: "Hidden Valley Builders",
    timezone: "America/Los_Angeles",
    ownerEmail: "mike@hiddenvalleybuilders.example",
  },
});

await ringkit("POST", `/businesses/${business.id}/knowledge/imports`, {
  key: apiKey,
  body: { url: "https://hiddenvalleybuilders.example" },
});

const { checkoutUrl } = await ringkit("POST", `/businesses/${business.id}/wallet/top-ups`, {
  key: apiKey,
  idempotencyKey: "hvb-topup-1",
  body: { amountCents: 2000 },
});
console.log("Open to fund the wallet:", checkoutUrl);
```

02 · Agent quickstart

## Agent quickstart

Written for an AI agent to follow as-is. Base URL `https://api.ringkit.com/v1`. Send `Content-Type: application/json` on every request, `Authorization: Bearer <apiKey>` from step 2 on, and a fresh `Idempotency-Key` on every POST (reuse it only when retrying that same request). On an error, read `error.hint` and do what it says. Each **Stop** means: wait for the human before going on.

```
1. Ask the human for: business name, timezone (IANA, e.g. America/Los_Angeles),
   owner email, owner mobile number (E.164), website (optional).

2. POST /businesses
   {"name": "...", "timezone": "...", "ownerEmail": "...", "website": "..."}
   Leave out ownerPhone: it must be a verified number (403 destination_unverified on create).
   Step 5 makes it a transfer target and step 9 verifies it.
   Save: business.id as BIZ, apiKey as KEY (shown once).
   Now ask the human to fund the wallet (step 7): it is the only thing they must do, and the number needs it.

3. POST /businesses/BIZ/knowledge/imports   {"url": "<website>"}
   Then GET /businesses/BIZ/onboarding every 10 s until latestImport.status is "done" or "failed".
   No website, or it failed: PUT /businesses/BIZ/knowledge with summary, hours, services, faqs, policies
   from what the human told you in step 1.

4. GET /voices. Pick one (or keep "default").
   PATCH /businesses/BIZ   {"voice": "<id>"}

5. PUT /businesses/BIZ/routing
   {"mode": "ai_only", "transferTargets": [{"id": "owner", "label": "Owner", "number": "<ownerPhone>",
    "whenToUse": "the caller asks for the owner or has an emergency"}]}

6. POST /businesses/BIZ/simulate   {"callerText": ""}
   Then {"callId": "<callId>", "callerText": "<a realistic caller question>"} two or three times.
   Billed at the AI rate; the trial credit covers it. Fix gaps with PATCH /businesses/BIZ/knowledge.

7. POST /businesses/BIZ/wallet/top-ups   {"amountCents": 2000}
   Save: checkoutUrl.
   Stop: ask the human to open checkoutUrl and pay. Then GET /businesses/BIZ/wallet and confirm
   balanceCents - trialCreditCents is at least 200.

8. GET /numbers/search?areaCode=<3 digits>. Pick one.
   POST /businesses/BIZ/numbers   {"number": "<picked>"}
   Save: number. No sign-in is needed. On 402 funding_required, the payment hasn't landed: back to step 7.

9. POST /businesses/BIZ/destinations/verify   {"number": "<ownerPhone>"}
   Stop: ask the human for the 6-digit code the call read (their consent to be called).
   POST /businesses/BIZ/destinations/confirm   {"number": "<ownerPhone>", "code": "<code>"}
   POST /businesses/BIZ/test-call   {"to": "<ownerPhone>"}
   Ask the human to answer and talk to the receptionist.

10. GET /businesses/BIZ/calls?limit=5. Show the human the newest call's summary.

11. Optional hand-over: POST /businesses/BIZ/owners/invite   {}
    Tell the human their Ringkit number and that the email signs them in to https://app.ringkit.com
    whenever they want to take over. You can keep running the line either way. Done.
```

The same steps are in [llms.txt](https://api.ringkit.com/llms.txt); [llms-full.txt](https://api.ringkit.com/llms-full.txt) adds every route and field.

03 · Concepts

## Concepts

Eight ideas cover everything the API does.

business

### Business

The account everything hangs off: name, timezone, owner email and phone, voice, status. Creating one needs no key and returns its API key. Ids are prefixed (`biz_`, `call_`, `num_`, `whk_`).

knowledge

### Knowledge profile

What the receptionist knows: summary, hours, address, services and prices, staff, FAQs, policies, things never to say, greeting. Plus uploaded documents:

- **Starter**: 5 documents, 50k tokens, free

- **Pro**: 100 documents, 500k tokens, $5/month from the wallet

routing

### Routing policy

Where calls go after the receptionist answers. `mode` is `ai_only`: the AI receptionist answers every call. Then:

- `transferTargets`: your own team's numbers, each with `whenToUse`, so the receptionist knows when to transfer

- No one free, or no rule matches: it takes a message for you

Ringkit doesn't staff calls itself: the `ai_first` and `human_first` modes, which brought in Ringkit's own live reps, aren't offered right now and run as `ai_only`.

number

### Numbers

US local numbers, searched by area code and ordered into the business for $1.99/month from the wallet (the first can run a 7-day trial). Calls to the number reach the receptionist; forward an existing line to it to keep the number customers know.

wallet

### Wallet

Prepaid, $10 minimum top-up, no plan or platform fee:

- AI receptionist $0.20/min of talk time

- First 10 seconds of every call free, then 6-second increments

- Number $1.99/month · $5 trial credit for calls on new businesses

call

### Calls & summaries

Every call keeps its transcript, who handled each part (`segments`), the cost (`billedCents`) and a summary: intent, outcome, caller name, a short write-up, any message taken with its urgency, and whether follow-up is needed. Summaries are included.

webhook

### Webhooks

Signed POSTs to your URL when a call completes, a message is taken, or the balance runs low. Included at no cost.

hand-over

### Hand-over

An agent can do the whole setup; the owner only pays. The business still belongs to them: the optional owner invite emails them a sign-in link whenever they want it, and the dashboard shows the business was set up by an agent and lets the owner rotate or revoke the agent's key.

Calls are recorded and transcribed to make the summary. Ringkit plays a consent notice on every call and follows two-party-consent rules by state. The receptionist always says it is an AI; that setting can't be turned off.

04 · REST API

## REST API reference

JSON over HTTPS. The full schema of every route is in the OpenAPI document, generated from the same contract the API runs on.

### Base URLs

| What | URL |
| --- | --- |
| REST API | `https://api.ringkit.com/v1` |
| MCP server | `https://api.ringkit.com/mcp` |
| OpenAPI 3.1 | `https://api.ringkit.com/openapi.json` |
| Docs for LLMs | `https://api.ringkit.com/llms.txt`, `https://api.ringkit.com/llms-full.txt` |
| MCP discovery | `https://api.ringkit.com/.well-known/mcp.json` |
| Dashboard | `https://app.ringkit.com` |

### Authentication and API keys

`POST /v1/businesses` is the only call that needs no key; it returns `{ business, apiKey }` once. Send the key on everything else:

```
Authorization: Bearer rk_…
```

An API key acts as the business owner. Each key has a scope: `manage` (the default, full access) or `read` (GET requests only; anything else returns `403 insufficient_scope`). Give a reporting tool a read key:

```
curl https://api.ringkit.com/v1/businesses/$BIZ/api-keys \
  -H "Authorization: Bearer $RINGKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Reporting", "scope": "read" }'
```

The response has the key's metadata (`key.id`, `key.prefix`) and the plaintext `apiKey`, once. `GET …/api-keys` lists keys; `DELETE …/api-keys/:keyId` revokes one.

### Idempotency

Every POST accepts an `Idempotency-Key` header. Retrying with the same key within 24 hours returns the stored response with `Idempotent-Replayed: true` instead of doing the work twice. A retry that arrives while the first is still running gets `409 idempotency_in_progress`. Always send one on top-ups and number orders.

### SDKs

No SDKs yet. Use `fetch` or `curl` (see the [quickstart](#quickstart)), generate a typed client from `/openapi.json`, or connect an agent over [MCP](#agents).

### Routes

Paths are relative to `https://api.ringkit.com`; `:id` is the business id.

| Method | Path | Description |
| --- | --- | --- |
| `POST` | `/v1/businesses` | Create a business; returns its API key once (no auth) |
| `GET` | `/v1/businesses/:id` | Read the business |
| `PATCH` | `/v1/businesses/:id` | Update name, timezone, owner phone, voice, notify targets |
| `GET` | `/v1/me` | The business behind the key |
| `POST` | `/v1/businesses/:id/owners/invite` | Email the owner a sign-in link `{ email? }` |
| `GET` `POST` | `/v1/businesses/:id/api-keys` | List keys; create one `{ name, scope }` |
| `DELETE` | `/v1/businesses/:id/api-keys/:keyId` | Revoke a key |
| `GET` `PUT` `PATCH` | `/v1/businesses/:id/knowledge` | Read, replace or merge the knowledge profile |
| `POST` | `/v1/businesses/:id/knowledge/imports` | Import a website in the background `{ url }` |
| `POST` | `/v1/businesses/:id/knowledge/import` | Draft a profile from a website now `{ url, apply }` |
| `GET` | `/v1/businesses/:id/onboarding` | Setup progress and the latest import |
| `POST` | `/v1/businesses/:id/setup-chat` | Start the setup chat `{ context?, restart? }` |
| `POST` | `/v1/businesses/:id/setup-chat/:conversationId/messages` | Send a message `{ text }` |
| `GET` `POST` | `/v1/businesses/:id/knowledge/documents` | List documents; upload one `{ fileName, contentBase64 }` |
| `DELETE` | `/v1/businesses/:id/knowledge/documents/:documentId` | Delete a document |
| `GET` | `/v1/businesses/:id/knowledge/usage` | Document and token usage against the tier |
| `POST` | `/v1/businesses/:id/knowledge/tier` | Switch tier `{ tier: starter \| pro }` |
| `GET` | `/v1/voices` | Available voices and the default |
| `GET` `PUT` | `/v1/businesses/:id/routing` | Read or replace the routing policy |
| `GET` | `/v1/numbers/search` | Find numbers `?areaCode=&contains=&limit=` |
| `GET` `POST` | `/v1/businesses/:id/numbers` | List numbers; order one `{ number }` ($1.99/mo) |
| `DELETE` | `/v1/businesses/:id/numbers/:numberId` | Release a number |
| `GET` | `/v1/businesses/:id/wallet` | Balance and unspent trial credit |
| `POST` | `/v1/businesses/:id/wallet/top-ups` | Get a checkout link `{ amountCents }` (min 1000) |
| `GET` | `/v1/businesses/:id/ledger` | Wallet ledger, cursor-paginated |
| `POST` | `/v1/businesses/:id/simulate` | Text-mode test call `{ callId?, callerText }` |
| `POST` | `/v1/businesses/:id/destinations/verify` | Call a number and read it a 6-digit code `{ number }` |
| `POST` | `/v1/businesses/:id/destinations/confirm` | Confirm the code `{ number, code }` |
| `POST` | `/v1/businesses/:id/test-call` | Ring a verified number with the AI `{ to }` |
| `GET` | `/v1/businesses/:id/calls` | List calls `?limit=&cursor=` |
| `GET` | `/v1/businesses/:id/calls/:callId` | One call with transcript and summary |
| `GET` | `/v1/businesses/:id/calls/:callId/recording` | Short-lived recording URL |
| `GET` `POST` | `/v1/businesses/:id/webhooks` | List webhooks; create one `{ url, events }` |

Pagination: list routes take `limit` and `cursor` and return `{ items, nextCursor }`; `nextCursor` is `null` on the last page.

05 · Agents

## Agent frameworks & MCP

The MCP server at `https://api.ringkit.com/mcp` exposes the setup lifecycle as tools. It speaks Streamable HTTP, is stateless and takes POST only. Put the API key in the `Authorization: Bearer rk_…` header of the connection.

### Add the server

Claude Code

```
claude mcp add --transport http ringkit https://api.ringkit.com/mcp --header "Authorization: Bearer rk_…"
```

Claude Desktop, Cursor and other MCP clients

```
{
  "mcpServers": {
    "ringkit": {
      "url": "https://api.ringkit.com/mcp",
      "headers": { "Authorization": "Bearer rk_…" }
    }
  }
}
```

**No key yet?** Connect without the header. `ringkit_create_business` works without one and returns the key; reconnect with it for every other tool. Every tool result carries a `next` field naming the tool that usually follows. Errors are the same JSON as REST, `{"error":{"code","message","hint","docs"}}` (see [errors](#errors)): follow the `hint`. Mutating tools take an optional `idempotencyKey` argument; an identical retry returns the stored result, so pass a new key to repeat a call on purpose.

### Tools by step

49 tools; [/.well-known/mcp.json](https://api.ringkit.com/.well-known/mcp.json) lists them with their step, the endpoint and how to authenticate.

| Step | Tools |
| --- | --- |
| 1. Create | `ringkit_create_business` (no key), `ringkit_whoami`, `ringkit_get_onboarding` |
| 2. Teach | `ringkit_start_knowledge_import`, `ringkit_get_latest_knowledge_import`, `ringkit_import_knowledge`, `ringkit_start_setup_chat`, `ringkit_send_setup_chat`, `ringkit_get_setup_chat`, `ringkit_close_setup_chat`, `ringkit_put_knowledge`, `ringkit_patch_knowledge`, `ringkit_get_knowledge`, `ringkit_upload_knowledge_document`, `ringkit_list_knowledge_documents`, `ringkit_get_knowledge_document`, `ringkit_delete_knowledge_document`, `ringkit_get_knowledge_usage`, `ringkit_set_knowledge_tier` |
| 3. Voice | `ringkit_list_voices`, `ringkit_update_business` `{ voice }` |
| 4. Routing | `ringkit_get_routing`, `ringkit_put_routing` |
| 5. Try it | `ringkit_simulate_call` (no number needed; billed like a call at the AI rate from the wallet, the free trial credit covers it) |
| 6. Fund | `ringkit_get_wallet`, `ringkit_top_up` (the human pays the `checkoutUrl`), `ringkit_get_auto_refill`, `ringkit_put_auto_refill`, `ringkit_list_ledger` |
| 7. Number | `ringkit_search_numbers`, `ringkit_provision_number`, `ringkit_list_numbers`, `ringkit_release_number` (needs step 6: $2 of paid credit) |
| 8. Test call | `ringkit_verify_destination`, `ringkit_confirm_destination`, `ringkit_list_destinations`, `ringkit_test_call` |
| 9. Watch | `ringkit_list_calls`, `ringkit_get_call`, `ringkit_get_recording` |
| 10. Webhooks | `ringkit_create_webhook`, `ringkit_list_webhooks` |
| 11. API keys | `ringkit_create_api_key`, `ringkit_list_api_keys`, `ringkit_revoke_api_key` |
| 12. Hand over (optional) | `ringkit_invite_owner`, `ringkit_get_business` (`ownerSignedInAt`), `ringkit_list_business_users`, `ringkit_complete_onboarding` |

The order is the order of `tools/list` and of the `Step N:` prefix on each tool description. Funding gates the number, so step 6 comes before it: a number needs $2 of paid credit (trial credit covers calls only), and `ringkit_top_up` returns a `checkoutUrl` the human pays. That is the only thing a person must do; nobody has to sign in. `ringkit_verify_destination` and `ringkit_test_call` ring a phone a person must answer and read back a code (their consent to be called). Step 12, `ringkit_invite_owner`, is an optional hand-over: the agent can keep running the line, and the owner can take over any time.

### OpenAI Agents, LangChain, Vercel AI SDK

Frameworks that build tools from OpenAPI can load `https://api.ringkit.com/openapi.json` directly. For a plain tool-calling loop, put `https://api.ringkit.com/llms-full.txt` in the system prompt and give the model one HTTP tool:

JavaScript

```
const [spec, guide] = await Promise.all([
  fetch("https://api.ringkit.com/openapi.json").then((r) => r.json()),
  fetch("https://api.ringkit.com/llms-full.txt").then((r) => r.text()),
]);
// spec.paths: one tool per operation; guide: the system prompt
```

Calls that need a person still reach one: list the business's own team numbers in `transferTargets` with a `whenToUse` for each, and the receptionist transfers those callers. Anything else becomes a message.

06 · Webhooks

## Webhooks

Create a webhook with `POST /v1/businesses/:id/webhooks` `{ url, events }`. The response holds the signing `secret`, once. Ringkit then POSTs JSON to the URL: `{ type, createdAt, data }`.

| Event | data | Sent when |
| --- | --- | --- |
| `call.completed` | `{ call, transcript }` | A call has ended and been billed, with its summary when ready |
| `message.taken` | `{ callId, message, summary }` | The receptionist took a message for the business |
| `balance.low` | `{ balanceCents }` | A call took the wallet below $2 |

### message.taken

```
{
  "type": "message.taken",
  "createdAt": "2026-10-27T18:15:52.000Z",
  "data": {
    "callId": "call_7n2k9x4m1q8c5v3b",
    "message": {
      "callerName": "Dana Ruiz",
      "callbackNumber": "+18055550173",
      "message": "Estimate for extending the Oakridge kitchen remodel into the dining room. Call after 3pm.",
      "urgency": "normal"
    },
    "summary": {
      "intent": "estimate_request",
      "outcome": "message_taken",
      "callerName": "Dana Ruiz",
      "summary": "Dana wants an estimate to extend the Oakridge kitchen remodel into the dining room.",
      "followUpNeeded": true
    }
  }
}
```

`call.completed` carries the same `call` and `transcript` as [GET /calls/:callId](#quickstart).

### Delivery

Respond with any `2xx` within 5 seconds. Other responses and timeouts are retried with exponential backoff, up to 10 attempts (about a day and a half). A `4xx` other than `408` or `429` is treated as permanent and not retried.

### Verify signatures

Every delivery carries `X-RingKit-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is HMAC-SHA256 of `"<t>.<raw body>"` keyed with the webhook's secret. The signature is made at send time, so reject anything more than five minutes old.

Node

```
import crypto from "node:crypto";

export function verifyRingkit(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(v1, "hex");
  return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}

// Express: app.post("/hooks/ringkit", express.raw({ type: "application/json" }), (req, res) =>
//   verifyRingkit(req.body.toString(), req.get("X-RingKit-Signature"), process.env.RINGKIT_WEBHOOK_SECRET)
//     ? res.sendStatus(204) : res.sendStatus(400));
```

07 · Limits

## Rate limits & errors

### Rate limits

- **Unauthenticated POSTs** (creating a business): 10 a minute per IP

- **Authenticated requests**: 600 a minute per API key

- **Owner invites**: 5 per business per hour

- **Setup chat**: 20 messages a minute per business

- **Outbound calls**: 5 test calls per business per day, 3 outbound calls per number per day

Over a limit you get `429 rate_limited` with a `Retry-After` header in seconds. Wait that long, then retry.

### Errors

Every error has the same body. `hint` is one concrete next step, written so an agent can act on it; `docs` links here.

```
{
  "error": {
    "code": "funding_required",
    "message": "ordering a number needs $2.00 of paid credit available; free trial credit covers calls only; top up first",
    "hint": "Top up the wallet: POST /v1/businesses/:id/wallet/top-ups returns a checkoutUrl for the human to pay; trial credit covers calls only.",
    "docs": "https://ringkit.com/docs.html#errors",
    "details": { "availableCents": 0, "requiredCents": 200, "trialCreditCents": 500 }
  }
}
```

| Status | Code | Meaning |
| --- | --- | --- |
| `400` | `validation_error` | A field is missing or malformed; `details` lists them |
| `401` | `unauthorized` | Missing, revoked or unknown API key |
| `402` | `funding_required` | Ordering a number needs $2 of paid credit; trial credit doesn't count |
| `402` | `insufficient_balance` | Not enough paid credit for this (for example the Pro plan) |
| `403` | `forbidden` | This key can't act on that business |
| `403` | `owner_required` | Owner-only route called by a team member's session |
| `403` | `insufficient_scope` | A `read` key tried to change something |
| `403` | `destination_unverified` | Verify the number before calling it |
| `404` | `not_found` | No such id for this business |
| `409` | `conflict` | The resource changed or already exists; read it and retry |
| `409` | `idempotency_in_progress` | A request with this `Idempotency-Key` is still running |
| `429` | `rate_limited` | Too many requests; wait `Retry-After` seconds |
| `503` | `llm_unavailable` | Website import or setup chat is unavailable; write the profile with `PUT …/knowledge` |
| `503` | `payments_unavailable` | In-page payments aren't configured; use the hosted checkout top-up |

Other codes are specific to one route (for example `file_too_large` on uploads or `invalid_code` on destination confirm) and almost all come with a `hint`.
