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, or connect an agent over MCP.
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.
Requestcurl 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": "[email protected]",
"ownerPhone": "+18055550134"
}'
Response · 201
{
"business": {
"id": "biz_4f8k2m9q7x1c3v5n",
"name": "Hidden Valley Builders",
"timezone": "America/Los_Angeles",
"vertical": "remodeling",
"website": "https://hiddenvalleybuilders.example",
"ownerEmail": "[email protected]",
"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 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": "[email protected]" }'
{ "ok": true, "email": "[email protected]", "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, 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:
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: "[email protected]",
},
});
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);
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; llms-full.txt adds every route and field.
Concepts
Eight ideas cover everything the API does.
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 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 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 withwhenToUse, 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.
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
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
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.
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
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.
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), generate a typed client from /openapi.json, or connect an agent over MCP.
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.
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 Codeclaude 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): 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 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:
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.
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.
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.
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));
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.