Connect
Need YOUR_PHONER_API_KEY? Email esokullu@gmail.com for API access and more information. Once you receive a key, send it on every request:
Authorization: Bearer YOUR_PHONER_API_KEY
Dashboard calls require a $9 Stripe payment. These REST endpoints use only your bearer token and do not require or consume a paid call credit.
- On this Mac
http://127.0.0.1:3000/v1- From another machine
Your PUBLIC_BASE_URL + /v1
Use https://phonr.xyz/v1 for the hosted API. If you run your own instance, set PHONER_API_KEY and PUBLIC_BASE_URL in its private .env.
Your key can start and stop calls and read call history and recordings. Keep it in your client’s environment, send it in the header, and use HTTPS remotely. The public dashboard uses a separate browser session for your paid calls. Its cookie does not grant API access. The key is never shown on this page.
Try a preview first
This creates instructions only. It does not contact OpenAI or the phone provider.
In your terminal, set the client variables. Replace the example key with your saved phonr.xyz key:
export PHONER_API_URL='https://phonr.xyz/v1'
export PHONER_API_KEY='YOUR_PHONER_API_KEY'
curl --fail-with-body "$PHONER_API_URL/preview" \
-H "Authorization: Bearer $PHONER_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"to": "+14155550123",
"purpose": "Ask what time the shop closes today.",
"language": "English"
}'
The response contains brief and prompt. The number above is an example; replace it before starting a real call.
Start a call
POST /calls places a real phone call using the caller number in the server’s .env. Save the brief as call.json, then create and keep a unique request ID:
REQUEST_ID="$(uuidgen)"
curl --fail-with-body "$PHONER_API_URL/calls" \
-H "Authorization: Bearer $PHONER_API_KEY" \
-H "Idempotency-Key: $REQUEST_ID" \
-H 'Content-Type: application/json' \
--data-binary @call.json
A new call returns 201 with { "call": { "id": "…", "status": "…" } }. Repeating the same request ID and brief returns that call with 200. Keep the ID if the request times out; retry with the same ID and body, or look it up with GET /calls?requestId=…. A changed brief with the same ID returns 409.
| JSON field | What to send |
|---|---|
to | Required. Phone number with country code. Spaces and ordinary formatting are accepted. |
purpose | Required. 5–6,000 characters. Include the needed facts and any authority or spending limits. Replace template placeholders. |
language | Required. 2–80 characters, for example English, Turkish, or Mexican Spanish. |
systemMessage | Optional. 1–8,000 characters. Omit to use the default assistant instructions. |
The caller, recording mode, and duration limit come from server settings. Only one call can run at a time, across the API and dashboard. The normal provider country and account restrictions still apply.
Follow the conversation
Poll GET /calls/{id} every few seconds. It includes the transcript, result, recording status, and download paths. Replace CALL_ID with the returned call ID.
CALL_ID='your-call-id'
curl --fail-with-body "$PHONER_API_URL/calls/$CALL_ID" \
-H "Authorization: Bearer $PHONER_API_KEY"
# End the call
curl --fail-with-body "$PHONER_API_URL/calls/$CALL_ID/stop" \
-H "Authorization: Bearer $PHONER_API_KEY" \
-H 'Content-Type: application/json' --data '{}'
call.terminal tells you whether the phone call has ended. call.result.status describes the task: completed, partial, or needs_user. An ended call does not necessarily mean the task succeeded. Older rental calls retain their original result format.
Recordings and 10-second clips
With immediate recording enabled, poll GET /calls/{id}/clips for completed clips and download new ones in index order. They arrive roughly every 10 seconds while audio is being captured; the last can be shorter. Full audio becomes available after the recording finishes. Downloads also require the bearer header; all returned download paths begin with /v1 and are relative to the server origin.
curl --fail-with-body "$PHONER_API_URL/calls/$CALL_ID/clips" \
-H "Authorization: Bearer $PHONER_API_KEY"
curl --fail-with-body "$PHONER_API_URL/calls/$CALL_ID/clips/0000.wav" \
-H "Authorization: Bearer $PHONER_API_KEY" -o clip-0000.wav
curl --fail-with-body "$PHONER_API_URL/calls/$CALL_ID/recording" \
-H "Authorization: Bearer $PHONER_API_KEY" -o call.wav
Audio endpoints support a single HTTP byte range. A 404 means that call or audio file is not available yet. Consent-mode provider recording does not produce live clips.
Endpoint reference
All paths below are relative to /v1. All require bearer authentication. JSON requests use Content-Type: application/json.
| Method and path | Result |
|---|---|
GET /status | Readiness, caller number, recording mode, time limit, active call ID, and default system message. |
GET /templates | Purpose templates, available to your authorized API workspace. |
POST /preview | Normalized brief and full voice prompt. Incomplete drafts are allowed. |
POST /calls | Start a call. Requires Idempotency-Key: 16–80 letters, digits, or hyphens. |
GET /calls | Newest first. Optional limit (1–100, default 20), offset (default 0), and requestId. Returns calls, total, and nextOffset; list items omit transcripts. |
GET /calls/{id} | Call details, transcript, outcome, and audio links. |
POST /calls/{id}/stop | Stop the call. Send {}; inspect the returned status. |
POST /calls/{id}/reconcile | Check the provider’s actual call status. Send {}. Never redials. |
GET /calls/{id}/clips | clips with name, index, start time, duration, and download path. |
GET /calls/{id}/clips/{name} | WAV clip, such as 0000.wav. |
GET /calls/{id}/recording | Full WAV recording. |
Download the OpenAPI specification to import into Postman or another API client. Set the server URL to the local or remote address above.
Runnable examples
The project includes examples/api-client.mjs, examples/call.json, and examples/phoner.http. Run these from the phonr.xyz project folder. The Node client reads the key from .env.
# Check configuration, then preview. Neither places a call.
npm run api:example -- status
npm run api:example -- preview examples/call.json
# Edit examples/call.json before this command: it places a real call.
# Keep the request ID so you can reuse it if the response is lost.
REQUEST_ID="$(uuidgen)"
npm run api:example -- call examples/call.json "$REQUEST_ID"
npm run api:example -- get CALL_ID
npm run api:example -- stop CALL_ID
npm run api:example -- recording CALL_ID call.wav
# Simulated automated checks: no real calls, keys, or charges.
npm run test:api
npm test
Errors and retries
Errors have a consistent shape:
{ "error": { "code": "unauthorized", "message": "…" } }
400 invalid input; 401 missing or incorrect key; 404 missing call or audio; 405 wrong method; 409 active call or reused key with a changed brief; 413 body over 64 KiB; 415 missing JSON content type; 416 invalid audio range; 503 API disabled or missing configuration; 502 an operation could not be completed.
A successful HTTP response means a call record was returned, not that the conversation succeeded. Check call.status and call.problem: preflight-failed means startup failed; unknown means the provider’s state is uncertain. For unknown, reconcile that call and inspect the provider if needed. Do not generate another request ID to bypass the active-call lock.