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.
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
Nodenpm 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.
JavaScriptimport 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.
Concepts
Four objects cover almost everything you'll build.
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.
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
Routes
Map each call intent to a tier. Tiers, from first line to last:
ai: hours, directions, status of an existing jobringkit-reps: live reps briefed on your businessyour-team: rings your staff's phones in orderowner: rings you directly
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.
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_…
| Method | Path | Description |
|---|---|---|
POST | /lines | Create a line with a brief and routes |
GET | /lines/:id | Retrieve a line, its brief status and routes |
PATCH | /lines/:id | Update the brief, routes or hours |
POST | /calls | Place an outbound call from a line |
GET | /calls/:id | Retrieve call status, duration, tier and cost |
GET | /calls/:id/summary | Retrieve the structured summary for a call |
POST | /numbers | Buy a new number ($1/mo) |
POST | /numbers/port | Start a free port of an existing number |
GET | /reps/tiers | List rep tiers available to your account |
POST | /webhooks | Register a webhook endpoint and events |
POST /lines
Requestcurl 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.
{
"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" }
}
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
JavaScriptconst 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
JavaScriptconst 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
JavaScriptconst 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)
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.
| Event | Sent when |
|---|---|
call.routed | The AI has classified the call and picked a tier |
call.completed | The call has ended; includes duration and cost |
call.summarized | The 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).
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 /numberswith a test key returns a number in the reserved+1 555 01xxrange. - Simulated callers. Trigger a scripted inbound call with
POST /sandbox/callsand a scenario:quote,new_job,existing_job_status,after_hoursorwrong_number. - Real webhooks. Sandbox calls fire the same
call.routed,call.completedandcall.summarizedevents, with"livemode": false.
curl https://api.ringkit.ai/v1/sandbox/calls \
-H "Authorization: Bearer rk_test_4eC39HqLyjWDarjt" \
-d line=line_01JB7Q2X9M \
-d scenario=quote
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_…" }
}
}
}
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" } }.
| Status | Code | Meaning |
|---|---|---|
400 | invalid_request | Missing or malformed parameter; param names it |
401 | unauthorized | Missing, revoked or wrong-mode API key |
402 | payment_required | No payment method on file for live usage |
404 | not_found | No object with that id in this mode |
409 | number_unavailable | Number already taken or a port is in progress |
422 | route_invalid | A route points to a tier your account can't use |
429 | rate_limited | Too many requests; retry after RateLimit-Reset |
5xx | api_error | Ringkit-side failure; safe to retry with an Idempotency-Key |