Documentation

workrr Phone API

Real US phone numbers for AI agents. Calls ring your agent's endpoint first. If it doesn't answer, the caller gets voicemail and you get two webhooks: the recording right away, and the transcript when it's ready.

Building with an AI agent? Point it at /llms.txt. It has the same information in plain text, written for models.

Quickstart

Base URL: https://phone.workrr.ai. An agent can do all of these steps itself.

1. Sign up

Sign up on the website, or have your agent call the API. Either way you get an API key and a claim link.

curl -X POST https://phone.workrr.ai/v1/signup \
  -H "content-type: application/json" \
  -d '{"agent_name": "Support agent", "contact_email": "you@company.com"}'

A human opens the claim_url, adds a card, and accepts the terms. Until then the key returns 403 account_pending. Poll GET /v1/account until status is "active".

2. Create a line

A line is where calls go: the endpoint to ring, the webhook to notify, and the rules in between.

curl -X POST https://phone.workrr.ai/v1/lines \
  -H "authorization: Bearer $WORKRR_KEY" \
  -H "content-type: application/json" \
  -d '{"name": "Support", "endpoint": "sip:support@agent.example",
       "webhook_url": "https://agent.example/hooks/phone"}'

The response includes webhook_secret. It's shown only once, so save it.

3. Get a number

curl -X POST https://phone.workrr.ai/v1/numbers \
  -H "authorization: Bearer $WORKRR_KEY" \
  -H "content-type: application/json" \
  -d '{"kind": "local", "area_code": "415", "line_id": "line_..."}'

This charges your card $8 now. To choose a specific number first, search GET /v1/numbers/available?area_code=415 and send its e164.

4. Call it

Call the number. If your endpoint doesn't answer, leave a voicemail and watch your webhook.

Authentication

Send your API key as a bearer token on every request except signup:

Authorization: Bearer wp_...

Keys start with wp_. Errors come back as {"error": {"code": "...", "message": "..."}}.

How calls are routed

  1. Extensions. If the number is shared, the caller is asked for an extension (one retry).
  2. Business hours. Inside hours, or if the line has none, we ring the line's endpoint for ring_timeout_secs (default 20).
  3. Forward. If nobody answers and forward_to is set, we try it next.
  4. Voicemail. The caller hears your greeting and a beep, then records up to 3 minutes. Recording stops after 10 seconds of silence.
  5. After hours. Callers go straight to voicemail, or to forward_to if after_hours is "forward".

Lines

GET/v1/lines
POST/v1/lines
PATCH/v1/lines/{id}

FieldDescription
nameRequired. Up to 80 characters.
endpointWhat to ring first: a SIP URI (sip:user@host) or a US number (+1XXXXXXXXXX). Leave empty to send every call to voicemail.
forward_toFallback SIP URI or number, tried if the endpoint doesn't answer.
ring_timeout_secs5 to 60. Default 20.
business_hours{"tz": "America/New_York", "days": {"mon": [["09:00", "17:00"]]}}
after_hours"voicemail" (default) or "forward".
greeting_textVoicemail greeting, up to 500 characters, read aloud.
transcription_enabledDefault true.
webhook_urlWhere voicemail events are sent.

Numbers

GET/v1/numbers/available?kind=local&area_code=415
GET/v1/numbers
POST/v1/numbers
PATCH/v1/numbers/{id}
DELETE/v1/numbers/{id}

Search, then order the one you want with {"e164": "+14155550142", "line_id": "..."}, or send just {"area_code": "415"} to take the first available. Toll-free needs a Business account. Without line_id the number routes to your first line.

Ordering charges the card on file $8 right away, then $8 every month. If the card is declined you get 402 payment_failed and no number is bought; if the carrier can't supply the number, the charge is refunded.

To share one number across several lines, switch it to extensions:

PATCH /v1/numbers/{id}
{"mode": "extensions", "extensions": {"101": "line_sales", "102": "line_support"}}

Deleting a number releases it and stops its billing. The current month isn't refunded.

Billing

POST/v1/billing/portal returns {"url": "..."}, a short-lived Stripe page where the account owner can update their card and download invoices. People can also sign in at phone.workrr.ai/account with their email to manage numbers, billing and API keys.

Calls and voicemail

GET/v1/calls?limit=25&before=<iso>&line_id=
GET/v1/voicemails?limit=25&before=<iso>&line_id=
GET/v1/voicemails/{id}

A single voicemail includes a signed audio_url that's good for 24 hours. Recordings and transcripts are deleted after 90 days.

Webhooks

Each voicemail sends two POSTs to the line's webhook_url, with a JSON body of {id, type, created_at, data}.

TypeWhendata
voicemail.receivedRight after the caller hangs upvoicemail_id, call_id, from, to, duration_secs, audio_url, transcript_status
voicemail.transcribedWhen the transcript is readyvoicemail_id, call_id, from, transcript

Verifying signatures

Every webhook has a Workrr-Signature: t=<unix>,v1=<hex> header, where v1 is HMAC-SHA256 of "<t>.<raw body>" keyed with the line's webhook_secret.

import crypto from "node:crypto";

function verify(header, rawBody, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

Failed deliveries retry with backoff. Respond with any 2xx to acknowledge.

MCP server

Connect any MCP client over Streamable HTTP at https://phone.workrr.ai/mcp with the same bearer key.

{
  "mcpServers": {
    "workrr-phone": {
      "url": "https://phone.workrr.ai/mcp",
      "headers": { "Authorization": "Bearer wp_..." }
    }
  }
}

Tools: get_account, search_numbers, order_number (charges $8), release_number, list_numbers, list_lines, update_routing, list_voicemails, get_voicemail, list_calls.

Limits