Ringkit docs

Build on the phone layer

Ringkit gives your app or agent a phone number, answers every inbound call, and routes it to a bot or the right tier of human. Every call comes back as a structured summary over webhooks. Bots for the easy calls, people for the ones that matter.

01 · Quickstart

Quickstart

Four steps from zero to a live, routed line. Most teams finish in under an hour with this quickstart. Grab a test key (rk_test_…) from the dashboard first; nothing below costs money in sandbox.

1. Install the SDK

Node
npm install ringkit
Python
pip install ringkit

2. Create a line with your brief

A line is a phone number plus everything the AI needs to know about your business, plus the rules for who handles what. Ringkit reads your site and syncs open jobs from your CRM to build the brief.

JavaScript
import Ringkit from "ringkit";
const ringkit = new Ringkit(process.env.RINGKIT_API_KEY);

const line = await ringkit.lines.create({
  business: "Hidden Valley Builders",
  brief: {
    site: "https://hiddenvalleybuilders.example",
    crm: { provider: "jobber", connection_id: "conn_8f2c" },
  },
  route: {
    simple: "ai",
    quotes: "ringkit-reps",
    new_jobs: "owner",
  },
});

console.log(line.number); // +18055550142
Python
import os
from ringkit import Ringkit

ringkit = Ringkit(os.environ["RINGKIT_API_KEY"])

line = ringkit.lines.create(
    business="Hidden Valley Builders",
    brief={
        "site": "https://hiddenvalleybuilders.example",
        "crm": {"provider": "jobber", "connection_id": "conn_8f2c"},
    },
    route={
        "simple": "ai",
        "quotes": "ringkit-reps",
        "new_jobs": "owner",
    },
)

print(line.number)  # +18055550142

3. Forward your number, or use the new one

Point your existing business number at line.number with your carrier's call forwarding, or publish the new number directly. To move the number over for good, use POST /numbers/port. Keep your existing number: porting is free and takes 3–10 business days with most US carriers; your old line keeps working until the port completes.

4. Receive webhooks

Register an HTTPS endpoint and Ringkit will post each call's routing decision, completion and summary to it.

await ringkit.webhooks.create({
  url: "https://hiddenvalleybuilders.example/hooks/ringkit",
  events: ["call.routed", "call.completed", "call.summarized"],
});

Call your new number from any phone. Within a minute of hanging up you'll get a call.summarized event with the intent, the details the caller gave, and suggested next actions.

02 · Concepts

Concepts

Four objects cover almost everything you'll build.

line

Lines

A phone number with a brief and a routing table attached. One business can have several lines, for example a main line and a separate after-hours or service line.

brief

Briefs

What the AI and reps know before they pick up:

  • Your site, services and service area (e.g. Ventura County)
  • Hours and holiday schedule
  • Pricing (e.g. $95/hr labor · free estimates)
  • Open jobs and customers, synced via your CRM
route

Routes

Map each call intent to a tier. Tiers, from first line to last:

  • ai: hours, directions, status of an existing job
  • ringkit-reps: live reps briefed on your business
  • your-team: rings your staff's phones in order
  • owner: rings you directly
summary

Summaries

Every call produces a summary: intent, extracted entities (name, address, job, budget), next actions and a recording link. Summaries are included at no extra cost and are delivered by webhook or fetched with GET /calls/:id/summary.

If a tier doesn't answer within its timeout (default 20s), the call falls through to the next tier you've listed. 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.

03 · REST API

REST API reference

All requests go to the base URL below over HTTPS, with JSON bodies. Authenticate with a secret key in the Authorization header. Live keys start with rk_live_, test keys with rk_test_.

Base URL   https://api.ringkit.ai/v1
Header     Authorization: Bearer rk_live_…
MethodPathDescription
POST/linesCreate a line with a brief and routes
GET/lines/:idRetrieve a line, its brief status and routes
PATCH/lines/:idUpdate the brief, routes or hours
POST/callsPlace an outbound call from a line
GET/calls/:idRetrieve call status, duration, tier and cost
GET/calls/:id/summaryRetrieve the structured summary for a call
POST/numbersBuy a new number ($1/mo)
POST/numbers/portStart a free port of an existing number
GET/reps/tiersList rep tiers available to your account
POST/webhooksRegister a webhook endpoint and events

POST /lines

Request
curl https://api.ringkit.ai/v1/lines \
  -H "Authorization: Bearer rk_test_4eC39HqLyjWDarjt" \
  -H "Content-Type: application/json" \
  -d '{
    "business": "Hidden Valley Builders",
    "area_code": "805",
    "brief": { "site": "https://hiddenvalleybuilders.example" },
    "route": { "simple": "ai", "quotes": "ringkit-reps", "new_jobs": "owner" }
  }'
Response · 201
{
  "id": "line_01JB7Q2X9M",
  "object": "line",
  "number": "+18055550142",
  "business": "Hidden Valley Builders",
  "brief": { "status": "building", "sources": ["site"] },
  "route": { "simple": "ai", "quotes": "ringkit-reps", "new_jobs": "owner" },
  "created": "2026-10-25T17:04:11Z"
}

POST /calls

Place an outbound call. Set handled_by to "ai" for an agent-run call or "rep" to have a briefed Ringkit rep make it.

Request
{
  "line": "line_01JB7Q2X9M",
  "to": "+18055550199",
  "handled_by": "ai",
  "goal": "Confirm Thursday 9am site visit for the Oakridge kitchen remodel",
  "metadata": { "job_id": "J-2291" }
}
Response · 201
{
  "id": "call_01JB7R5KDT",
  "object": "call",
  "direction": "outbound",
  "status": "queued",
  "line": "line_01JB7Q2X9M",
  "to": "+18055550199",
  "handled_by": "ai",
  "metadata": { "job_id": "J-2291" }
}

GET /calls/:id/summary

Response · 200
{
  "call": "call_01JB7S0P4A",
  "summary": "Homeowner in Thousand Oaks wants a quote to extend the Oakridge kitchen remodel to the dining room. Agreed to an $18,400 estimate pending a site visit.",
  "intent": "quote",
  "entities": {
    "name": "Dana Ruiz",
    "address": "1840 Oakridge Ln, Thousand Oaks, CA",
    "job_id": "J-2291",
    "amount": 18400
  },
  "next_actions": [
    { "type": "proposal", "title": "Send revised $18,400 estimate" },
    { "type": "task", "title": "Book site visit", "due": "2026-10-30" }
  ],
  "handled_by": "rep",
  "tier": "ringkit-reps",
  "duration_seconds": 352,
  "cost": { "billable_seconds": 342, "amount": "5.42", "currency": "usd" }
}
04 · SDKs

SDKs

Official libraries for Node (18+) and Python (3.9+). Both are typed, retry idempotent requests automatically, and read RINGKIT_API_KEY from the environment if you don't pass a key.

Create a line

JavaScript
const line = await ringkit.lines.create({
  business: "Hidden Valley Builders",
  brief: { site: "https://hiddenvalleybuilders.example" },
  route: { simple: "ai", quotes: "ringkit-reps", new_jobs: "owner" },
});
Python
line = ringkit.lines.create(
    business="Hidden Valley Builders",
    brief={"site": "https://hiddenvalleybuilders.example"},
    route={"simple": "ai", "quotes": "ringkit-reps", "new_jobs": "owner"},
)

Place a call

JavaScript
const call = await ringkit.calls.create({
  line: line.id,
  to: "+18055550199",
  handled_by: "rep",
  goal: "Walk the customer through the $18,400 estimate",
});
Python
call = ringkit.calls.create(
    line=line.id,
    to="+18055550199",
    handled_by="rep",
    goal="Walk the customer through the $18,400 estimate",
)

Fetch a summary

JavaScript
const summary = await ringkit.calls.summary("call_01JB7S0P4A");
for (const action of summary.next_actions) console.log(action.title);
Python
summary = ringkit.calls.summary("call_01JB7S0P4A")
for action in summary.next_actions:
    print(action.title)
05 · Webhooks

Webhooks

Ringkit posts JSON to your endpoint for each event. Respond with any 2xx within 10 seconds; failed deliveries retry with exponential backoff for up to 72 hours.

EventSent when
call.routedThe AI has classified the call and picked a tier
call.completedThe call has ended; includes duration and cost
call.summarizedThe summary, entities and next actions are ready

call.routed

{
  "id": "evt_01JB7S0R2C",
  "type": "call.routed",
  "data": {
    "call": "call_01JB7S0P4A",
    "line": "line_01JB7Q2X9M",
    "from": "+18055550173",
    "intent": "quote",
    "tier": "ringkit-reps",
    "handled_by": "rep"
  }
}

call.completed

{
  "id": "evt_01JB7S6V8N",
  "type": "call.completed",
  "data": {
    "call": "call_01JB7S0P4A",
    "duration_seconds": 352,
    "handled_by": "rep",
    "tier": "ringkit-reps",
    "cost": { "billable_seconds": 342, "amount": "5.42", "currency": "usd" }
  }
}

call.summarized

{
  "id": "evt_01JB7S7F1Q",
  "type": "call.summarized",
  "data": {
    "call": "call_01JB7S0P4A",
    "summary": "Wants a quote to extend the Oakridge kitchen remodel to the dining room.",
    "intent": "quote",
    "entities": { "name": "Dana Ruiz", "job_id": "J-2291", "amount": 18400 },
    "next_actions": [
      { "type": "proposal", "title": "Send revised $18,400 estimate" },
      { "type": "task", "title": "Book site visit", "due": "2026-10-30" }
    ],
    "recording_url": "https://api.ringkit.ai/v1/recordings/rec_01JB7S6W",
    "consent": true,
    "handled_by": "rep",
    "tier": "ringkit-reps"
  }
}

Verify signatures

Every request carries a Ringkit-Signature header: t=<timestamp>,v1=<hex>, where v1 is an HMAC-SHA256 of timestamp + "." + raw_body keyed with your endpoint's signing secret (whsec_…). Reject requests older than five minutes, and compare with a constant-time function.

import crypto from "node:crypto";

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Date.now() / 1000 - Number(parts.t) > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(parts.t + "." + rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

The SDKs wrap this as ringkit.webhooks.verify(rawBody, header, secret).

06 · Sandbox

Sandbox

Every account gets a sandbox. Use an rk_test_ key and every request runs against it; no real calls are placed and nothing is billed.

  • Sandbox numbers are free. POST /numbers with a test key returns a number in the reserved +1 555 01xx range.
  • Simulated callers. Trigger a scripted inbound call with POST /sandbox/calls and a scenario: quote, new_job, existing_job_status, after_hours or wrong_number.
  • Real webhooks. Sandbox calls fire the same call.routed, call.completed and call.summarized events, with "livemode": false.
curl https://api.ringkit.ai/v1/sandbox/calls \
  -H "Authorization: Bearer rk_test_4eC39HqLyjWDarjt" \
  -d line=line_01JB7Q2X9M \
  -d scenario=quote
07 · Agents

Agent frameworks & MCP

Ringkit is built so agents can place and take calls without a telecom project. Each API operation is exposed as a tool with a typed schema, so it drops into OpenAI Agents, LangChain, Vercel AI SDK and MCP. An agent can create a line, place a call with a goal, wait for call.summarized, and act on next_actions. Calls that need a person still go to a person: set handled_by: "rep" or route to your-team. To use Ringkit from any MCP client, add the server to its config:

{
  "mcpServers": {
    "ringkit": {
      "command": "npx",
      "args": ["-y", "@ringkit/mcp"],
      "env": { "RINGKIT_API_KEY": "rk_test_…" }
    }
  }
}
08 · Limits

Rate limits & errors

Default limits are 100 requests per second per key and 50 concurrent outbound calls per account. Responses include RateLimit-Remaining and RateLimit-Reset headers. Errors return a JSON body: { "error": { "type", "code", "message" } }.

StatusCodeMeaning
400invalid_requestMissing or malformed parameter; param names it
401unauthorizedMissing, revoked or wrong-mode API key
402payment_requiredNo payment method on file for live usage
404not_foundNo object with that id in this mode
409number_unavailableNumber already taken or a port is in progress
422route_invalidA route points to a tier your account can't use
429rate_limitedToo many requests; retry after RateLimit-Reset
5xxapi_errorRingkit-side failure; safe to retry with an Idempotency-Key