Developer API

API Reference

A live US phone number with an AI agent answering it, by API. Provision numbers, read calls, pull transcripts and recordings, and receive signed webhooks. Base URL https://kwickphone.com/v1.

Authentication

Every request authenticates with an API key as a Bearer token. Create keys in the developer console — the secret (kp_live_…) is shown once at creation; store it securely. Requests are HTTPS only and authenticate by Bearer token alone (no cookies).

curl https://kwickphone.com/v1/numbers \
  -H "Authorization: Bearer kp_live_xxxxxxxxxxxxxxxx"
  • Up to 5 active keys per account; revoke and rotate anytime in the console.
  • IP allowlist (optional): lock a key to specific IPs/CIDRs when you create it. A key used from a non-allowed IP is rejected exactly like an invalid key.
  • A missing or invalid key returns 401 unauthorized.

Balance & funding

The API runs on a prepaid balance. Numbers cost $5/number/month and the first month is charged when you provision. You must have at least $5 of balance to provision — otherwise POST /v1/numbers returns 402 insufficient_balance.

Fund your balance in the console under Billing → API balance: add funds manually, or turn on auto-top-up (when your balance drops below a threshold we charge your card on file, $5–$500 per top-up). Talk time is billed per the plan on the pricing page.

Quickstart

Prefer working examples? Three open-source (MIT) starters, basic → advanced: ai-front-desk-demo (provision + signed webhooks + transcripts/recordings) → chess-club-receptionist (extract intake → roster/leads → nightly digest) → ai-booking-line (+ Stripe deposit pay-link, reconciled by webhook). Each has a one-click Deploy to Render.

Fund your balance, create a key, then:

# 1 — provision a number that answers for your business line
curl -X POST https://kwickphone.com/v1/numbers \
  -H "Authorization: Bearer $KP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"business_phone":"+13465551234","area_code":"346","webhook_url":"https://yourapp.com/hooks/kp"}'

# 2 — your webhook URL now receives a signed call.completed event after each call
# 3 — or pull calls on demand:
curl https://kwickphone.com/v1/calls -H "Authorization: Bearer $KP_API_KEY"

Provision a number

POST /v1/numbers

Buys a local US number and assigns an AI agent that answers for your business_phone. Charges the first month ($5) on success.

Body fieldRequiredDescription
business_phoneyesE.164 US number the AI answers for (e.g. +13465551234). Keys the agent to your business line.
area_codenoPreferred area code for the new number (defaults to the business number's area).
webhook_urlnoHTTPS URL to receive signed call events. Must be public HTTPS (internal/private addresses are rejected).
curl -X POST https://kwickphone.com/v1/numbers \
  -H "Authorization: Bearer $KP_API_KEY" -H "Content-Type: application/json" \
  -d '{"business_phone":"+13465551234","area_code":"346","webhook_url":"https://yourapp.com/hooks/kp"}'

201 Created
{
  "id": "num_12",
  "phone_number": "+13465700511",
  "business_phone": "+13465551234",
  "status": "active",
  "webhook_url": "https://yourapp.com/hooks/kp",
  "webhook_secret": "whsec_8f3c…",   // shown once — store it to verify webhooks
  "created_at": "2026-06-20T08:00:00+00:00"
}
Save webhook_secret now. It's returned only in this response and is the key you use to verify webhook signatures (see Webhooks).

Failure modes: 402 insufficient_balance, 409 no_numbers (no stock in that area code — try another), 429 number_limit / provision_velocity (see limits), 503 provider_unavailable (transient — retry). A failed provision is never charged.

List numbers

GET /v1/numbers

Returns your account's numbers, active first. webhook_secret is never returned here — only at creation.

curl https://kwickphone.com/v1/numbers -H "Authorization: Bearer $KP_API_KEY"

200 OK
{ "data": [
  { "id": "num_12", "phone_number": "+13465700511", "business_phone": "+13465551234",
    "status": "active", "webhook_url": "https://yourapp.com/hooks/kp", "created_at": "2026-06-20T08:00:00+00:00" }
] }

Status is one of active, past_due (a monthly charge failed — paused until paid), or released.

Release a number

DELETE /v1/numbers/{id}

Releases the number and frees it. Stops future monthly billing. No refund of the current month.

curl -X DELETE https://kwickphone.com/v1/numbers/num_12 \
  -H "Authorization: Bearer $KP_API_KEY"

200 OK
{ "released": true }

An unknown or not-yours id returns 404 not_found.

List calls

GET /v1/calls

Calls across all your numbers, newest first.

curl https://kwickphone.com/v1/calls -H "Authorization: Bearer $KP_API_KEY"

200 OK
{ "data": [
  { "id": "CA9f…", "number_id": "num_12", "phone_number": "+13465700511",
    "caller": "+12145559876", "direction": "inbound", "status": "completed",
    "outcome": "order", "duration_sec": 132, "started_at": "2026-06-20T07:55:10Z",
    "has_recording": true, "has_transcript": true }
] }

Get a transcript

GET /v1/calls/{call_id}/transcript

Returns the turn-by-turn transcript for a call (the id from the calls list). Returns 404 not_found if the call isn't one of your numbers' or has no transcript.

curl https://kwickphone.com/v1/calls/CA9f.../transcript \
  -H "Authorization: Bearer $KP_API_KEY"

Get a recording

GET /v1/calls/{call_id}/recording

Streams the call audio as audio/mpeg. 404 no_recording if none.

curl https://kwickphone.com/v1/calls/CA9f.../recording \
  -H "Authorization: Bearer $KP_API_KEY" -o call.mp3

Webhooks — events & signing

If you set webhook_url on a number, we POST a JSON event to it after each completed call. Delivery is at-least-once with retries (exponential backoff, up to 5 attempts); dedupe on the call id. Your endpoint should return 2xx quickly.

Event payload

POST https://yourapp.com/hooks/kp
{
  "type": "call.completed",
  "number_id": "num_12",
  "occurred_at": "2026-06-20T07:57:25+00:00",
  "call": { "call_sid": "CA9f…", "caller": "+12145559876", "outcome": "order",
            "duration_sec": 132, "started_at": "2026-06-20T07:55:10Z",
            "has_recording": true, "has_transcript": true }
}

Headers

HeaderValue
X-KwickPhone-TimestampUnix epoch seconds when we signed the request.
X-KwickPhone-Signaturev1=<hex> — HMAC-SHA256 over "{timestamp}.{raw_body}" keyed by the number's webhook_secret.

Verify the signature

Compute the HMAC over timestamp + "." + rawBody with your stored webhook_secret and compare in constant time.

// PHP
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_KWICKPHONE_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_KWICKPHONE_SIGNATURE'] ?? '';
$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $raw, $WEBHOOK_SECRET);
if (!hash_equals($expected, $sig)) { http_response_code(401); exit; }
// Node.js (express, raw body)
const crypto = require('crypto');
const ts  = req.headers['x-kwickphone-timestamp'];
const sig = req.headers['x-kwickphone-signature'];
const expected = 'v1=' + crypto.createHmac('sha256', WEBHOOK_SECRET)
  .update(ts + '.' + rawBody).digest('hex');
if (sig !== expected) return res.sendStatus(401);
Your endpoint must be public HTTPS. We refuse to deliver to private, loopback, or link-local addresses and never follow redirects — a webhook URL pointing at an internal host is rejected at send time. Also reject events whose X-KwickPhone-Timestamp is too old to prevent replay.

Lost & Found — report a found item

POST /v1/lostfound/found-items

Log an item your staff found (e.g. housekeeping, from your PMS app). Requires a location_id your account owns with Lost & Found enabled. Logging an item instantly reverse-matches waiting claims — matching claimants are texted automatically.

Body fieldRequiredDescription
location_idyesYour location with Lost & Found enabled (404 location_not_found otherwise).
descriptionyesWhat the item is — drives matching.
category / color / found_area / storage_ref / found_bynoCategory (e.g. wallet, umbrella), color, where it was found, bin/shelf, finder's name.
found_datenoYYYY-MM-DD; defaults to today (UTC).
image_urlnoPhoto URL, stored as-is.
curl -X POST https://kwickphone.com/v1/lostfound/found-items \
  -H "Authorization: Bearer $KP_API_KEY" -H "Content-Type: application/json" \
  -d '{"location_id":12,"category":"umbrella","description":"compact travel umbrella","color":"black","found_area":"room 214","storage_ref":"bin 3","found_by":"Maria (housekeeping)"}'

201 Created
{
  "id": 31, "category": "umbrella", "description": "compact travel umbrella",
  "color": "black", "found_by": "Maria (housekeeping)", "found_area": "room 214",
  "found_date": "2026-07-01", "storage_ref": "bin 3", "image_url": "",
  "status": "stored", "source": "api", "created_at": "2026-07-01T18:20:03+00:00"
}

Lost & Found — file a claim

POST /v1/lostfound/claims

File a lost-item claim on a guest's behalf. claimant_name and claimant_phone are required — the claimant is texted the moment a matching item is logged. Optional: claimant_email, category, description, color, lost_area, lost_date, proof_note, image_url.

curl -X POST https://kwickphone.com/v1/lostfound/claims \
  -H "Authorization: Bearer $KP_API_KEY" -H "Content-Type: application/json" \
  -d '{"location_id":12,"claimant_name":"Ana","claimant_phone":"+18325550101","category":"wallet","description":"brown leather wallet","lost_area":"hotel bar","proof_note":"library card + about $40 inside"}'

201 Created — high-value category: has_match only, never the matched item's details
{
  "id": 9, "claimant_name": "Ana", "claimant_phone": "+18325550101",
  "category": "wallet", "description": "brown leather wallet",
  "status": "open", "source": "api", "created_at": "2026-07-01T18:22:41+00:00",
  "has_match": true, "verify_required": true
}
Discretion rules are enforced server-side. High-value categories (wallet, jewelry, phone, keys, bag, id, passport, electronics, laptop, tablet, watch, cash) return verify_required:true and never the matched item's details — ownership is proven at pickup, not by repeating a description. Low-value categories include candidates (description/color/area/date). Urgent categories (a lost child, person, medication) are refused with 422 urgent_category — connect the reporter to venue staff or 911; they are never queued via the API.

Lost & Found — list items & claims

GET /v1/lostfound/found-items
GET /v1/lostfound/claims

Paginated (25/page). Query params: location_id (required), status, category, area, from, to, q (free-text), page.

curl "https://kwickphone.com/v1/lostfound/found-items?location_id=12&status=stored&q=umbrella" \
  -H "Authorization: Bearer $KP_API_KEY"

200 OK
{ "data": [ { "id": 31, "category": "umbrella", "status": "stored",  } ],
  "total": 1, "page": 1, "pages": 1, "per": 25 }

Lost & Found — item / claim detail

GET /v1/lostfound/found-items/{id}
GET /v1/lostfound/claims/{id}

Pass ?location_id=. Adds photos (all photo URLs, primary first) and timeline — the chain-of-custody audit trail.

curl "https://kwickphone.com/v1/lostfound/found-items/31?location_id=12" \
  -H "Authorization: Bearer $KP_API_KEY"

200 OK
{
  "id": 31, "category": "umbrella", ,
  "photos": ["https://…/1.jpg"],
  "timeline": [
    { "created_at": "2026-07-01T18:20:03+00:00", "action": "logged", "staff_name": "API", "note": "" }
  ]
}

Lost & Found — reunite (release an item)

POST /v1/lostfound/claims/{id}/reunite

Marks the claim reunited with found_item_id and the item returned. For high-value categories the release is refused unless "verified": true — your staff confirmed proof of ownership against the item. Every release is written to the chain of custody.

curl -X POST https://kwickphone.com/v1/lostfound/claims/9/reunite \
  -H "Authorization: Bearer $KP_API_KEY" -H "Content-Type: application/json" \
  -d '{"location_id":12,"found_item_id":31,"verified":true}'

200 OK
{ "reunited": true, "claim_id": 9, "found_item_id": 31, "verified": true }

Failure modes: 409 verification_required (high-value without verified — nothing changes), 409 claim_not_open, 409 item_not_available, 404 not_found.

Webhooks: found / claim / reunite events also fan out to your configured outbound integrations (webhook/Slack/Teams) as lostfound.found_logged, lostfound.claim_filed and lostfound.reunited — claimant phone numbers and proof notes never leave the system on those events.

Errors

Errors use a consistent envelope and a matching HTTP status:

{ "error": { "code": "insufficient_balance", "message": "Add funds to your API balance before provisioning a number." } }
StatusCodeMeaning
400invalid_requestMalformed body / bad business_phone / non-HTTPS webhook URL.
401unauthorizedMissing/invalid key, or key used from a non-allowed IP.
402insufficient_balanceBalance below $5 — top up first.
404not_found / no_recording / location_not_foundUnknown/foreign id, no recording for that call, or a location that isn't yours / doesn't run Lost & Found.
409no_numbersNo stock in that area code — try another.
409verification_required / claim_not_open / item_not_availableLost & Found release refused: high-value without verified, claim already closed, or item not in storage.
422urgent_categoryLost child/person/medication — route the reporter to staff or 911, never the API queue.
429number_limit / provision_velocity / rate_limitedA limit was hit (see below).
503provider_unavailableTransient provisioning issue — retry shortly.

Limits & billing

  • Request rate: 60 requests/minute per key → 429 rate_limited.
  • Active numbers: up to 5 active numbers per account → 429 number_limit. Need more? Contact us to raise it.
  • Provisioning velocity: up to 3 provisions/hour per account → 429 provision_velocity.
  • API keys: up to 5 active keys per account.
  • Numbers: $5/number/month, charged on provision and monthly thereafter. If a monthly charge fails, the number goes past_due (paused) with a 7-day grace before release — keep your balance funded or auto-top-up on.
  • Talk time: per the pricing tiers; payments on calls are PCI SAQ-A pass-through.
Building a sandbox integration? Create a key, fund a small balance, and provision a test number — call logs, transcripts and recordings are all available. Questions or higher limits: see the overview.