Everything here works against the v0 API. Anything marked Planned doesn't exist yet.
export K=sk_... B=https://sipcraft.dev
curl -s $B/v0/numbers/search?area_code=415 -H "Authorization: Bearer $K"
curl -s $B/v0/numbers -H "Authorization: Bearer $K" -d '{"e164":"+14155550123","confirm":true}'
curl -s -X PATCH $B/v0/numbers/+14155550123 -H "Authorization: Bearer $K" \
-d '{"route_type":"websocket","route_target":"wss://agent.example.com/media"}'
curl -s $B/v0/webhooks -H "Authorization: Bearer $K" -d '{"url":"https://agent.example.com/hooks"}'
curl -s $B/v0/verification -H "Authorization: Bearer $K" -d '{"kind":"individual"}' # Get Verified: a human opens verification_urlGet Verified is powered by Didit. /v0/kyc and the MCP tools start_kyc and get_kyc_status remain as aliases.
sign_up (no Authorization header) or POST /v0/signup with email, optional agent_name and use_case. The API key is returned once. A 6-digit code and a link go to that email; confirm with verify_email or POST /v0/email/verify.get_verified with owner_email (and notify_human: true to email them). It returns verify_url, message_for_human and instructions. Pass the message to your human. They set a password or use an emailed sign-in link at /owner/, then complete Get Verified (powered by Didit). The owner becomes the responsible person on the account. One verified owner can own several agents, and each new agent they accept is unlocked straight away.{"name":"sign_up","arguments":{"email":"agent@yourco.com","agent_name":"Front desk"}}
{"name":"verify_email","arguments":{"code":"123456"}}
{"name":"get_verified","arguments":{"owner_email":"you@yourco.com","notify_human":true}}
# place_call to a phone number before then → 403 with verify_url + message_for_humanLimits: 5 sign-ups per IP per hour and 20 per day, 3 per email per day, 5 code attempts. Disposable email domains are rejected. Until the email is verified the key can only read and verify. Events: account.signed_up, account.email_verified, kyc.owner_invited, kyc.link_sent, kyc.owner_linked, kyc.started, kyc.approved / kyc.declined.
Every endpoint gets a SIP address like sip:front-desk@harbor.sipcraft.dev and a short extension. Calls between SIPCraft addresses never touch the phone network, so they're free and need no verification. Types: agent (routes to your wss:// stream, SIP URI or webhook), human_app (an invite link; the person signs in on the SIPCraft app or the NT Connect App, Planned), and sip_device (registrar credentials for a softphone).
curl -s $B/v0/endpoints -H "Authorization: Bearer $K" -d '{"name":"front-desk","route_target":"wss://agent.example.com/media"}'
curl -s $B/v0/endpoints -H "Authorization: Bearer $K" -d '{"type":"human_app","name":"nick"}' # returns invite_url to share
curl -s $B/v0/calls -H "Authorization: Bearer $K" -d '{"from":"front-desk","to":"nick"}' # or to: "sip:…" · network: onnetFair use on the free plan: 1,000 on-net minutes a month, 3 simultaneous calls, 10 endpoints. The audio stream to your agent is included. Past a cap the API returns 429 with upgrade: "buy_number". Cross-account calls reach only endpoints that set accept_from: "anyone". A human endpoint rings only while its app is registered (presence online); otherwise the call ends no_answer. MCP: create_endpoint, list_endpoints, call_endpoint, or place_call with a sip: target.
The framing is JSON, compatible with the Twilio Media Streams message shapes so existing agent code ports over. Audio is base64 G.711 µ-law at 8 kHz in 20 ms frames.
{"event":"start","start":{"call_id":"call_…","from":"+1…","to":"+1…",
"media_format":{"encoding":"audio/x-mulaw","sample_rate":8000,"channels":1}}}
{"event":"media","media":{"payload":"<base64 160 bytes>","timestamp":"20"}}
{"event":"dtmf","dtmf":{"digit":"5"}}
{"event":"mark","mark":{"name":"greeting-done"}}
{"event":"stop"}{"event":"media","media":{"payload":"<base64 µ-law>"}}
{"event":"mark","mark":{"name":"greeting-done"}} # echoed when played
{"event":"clear"} # barge-in: flush queued audioEvery delivery carries SIPCraft-Signature: t=<unix>,v1=<hex>, where v1 = HMAC-SHA256(secret, "<t>.<raw body>"). Reject timestamps older than five minutes. Missed something? Replay from GET /v0/events?since=.
def verify(secret, header, body): t, v1 = (kv.split("=", 1)[1] for kv in header.split(",")) mac = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest() return hmac.compare_digest(mac, v1) and abs(time.time() - int(t)) < 300
Event types: number.purchased number.released call.ringing call.answered call.busy call.completed recording.ready endpoint.created kyc.approved (Get Verified) kyc.declined webhook.test
Usage is pay-as-you-go, drawn from prepaid credit. Every grant, pack purchase, call and monthly number fee is a row in the credit ledger, and your balance is their sum. Packs: $5, $10, $20, $50, $100. Payments coming soon
GET /v0/billing # balance, packs, payments_enabled, auto_topup, stop_outbound_at_zero GET /v0/billing/ledger # grant | purchase | usage | number_fee entries PUT /v0/billing/settings {"auto_topup":{"enabled":true,"threshold_usd":5,"pack_usd":20},"stop_outbound_at_zero":true} POST /v0/billing/checkout {"pack_usd":20} # Stripe Checkout URL; 503 until payments are enabled
Key scopes: numbers:read numbers:write calls:read calls:write calls:outbound webhooks:write account:read billing:write. Send Idempotency-Key on writes and a retry returns the original response.