Integrate Pine voice calls directly into your application using the REST API or SDK. This API is for direct voice calls (Pine Voice). For the Pine Assistant application API (sessions, tasks, Socket.IO), see the Pine Assistant API Reference.
The API uses two hosts. Authentication goes through the Pine AI main API; voice call endpoints go through the gateway.
All endpoints accept and return JSON. Include Content-Type: application/json in all requests.
Pine Voice uses a two-step email verification flow to issue access tokens. Tokens are then passed as Bearer tokens in subsequent API calls.
Send your Pine AI account email to receive a one-time verification code via email.
Submit the code from your email along with the request_token you received. You get back an access_token and user_id.
Include both the access token and user ID as headers in all voice call requests.
After completing authentication, include these headers on every voice call API request:
Authorization: Bearer <access_token>
X-Pine-User-Id: <user_id>
Content-Type: application/json
Prerequisite: You need an active Pine AI Pro subscription and a registered phone number on your Pine account.
Sends a one-time verification code to the provided email address. The email must be associated with an existing Pine AI account. Uses the Auth API host (https://www.19pine.ai).
| Parameter | Type | Description |
|---|---|---|
| emailrequired | string | Your Pine AI account email address |
curl -X POST https://www.19pine.ai/api/v2/auth/email/request \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]"
}'
{
"status": "success",
"data": {
"request_token": "eyJhbGciOiJIUzI1NiIs..."
}
}
| Field | Type | Description |
|---|---|---|
| status | string | Always "success" |
| data.request_token | string | Temporary token to use in the verify step. Expires after a few minutes. |
| Status | Cause |
|---|---|
| 400 | Missing or invalid email address |
| 422 | Email not registered with Pine AI |
Verifies the one-time code from your email and returns your access credentials. Store both access_token and id securely. Uses the Auth API host (https://www.19pine.ai).
| Parameter | Type | Description |
|---|---|---|
| emailrequired | string | Same email used in the request step |
| request_tokenrequired | string | The request_token from the previous step |
| coderequired | string | The verification code received via email |
curl -X POST https://www.19pine.ai/api/v2/auth/email/verify \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"request_token": "eyJhbGciOiJIUzI1NiIs...",
"code": "1234"
}'
{
"status": "success",
"data": {
"id": "1234567890",
"access_token": "eyJhbGciOiJSUzI1NiIs..."
}
}
| Field | Type | Description |
|---|---|---|
| data.id | string | Your Pine user ID. Use as the X-Pine-User-Id header. |
| data.access_token | string | Bearer token for API authentication. Expires periodically. |
| Status | Cause |
|---|---|
| 400 | Invalid verification code |
| 401 | Code verification failed |
| 410 | Request token expired — restart the flow |
Security: Store your access_token securely. Do not commit it to version control or log it in plaintext. Tokens expire and will need to be refreshed by repeating this flow.
Validates your request, runs safety and billing checks, and initiates a phone call using Pine's AI voice agent. The request blocks while the call is being set up (~2–5 seconds). Once the call is successfully initiated, it returns a call_id that you use to poll for progress and results.
Async results. This endpoint waits for the call to be initiated, then returns a call_id. The call itself runs in the background. To get the result, use the SSE stream endpoint (recommended) to wait for the final transcript, or poll the GET endpoint periodically. If you need a fully synchronous flow that blocks until the call finishes, use the MCP protocol with mode: "sync". Note: no real-time intermediate updates are available during the call.
| Header | Value | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <access_token> |
| X-Pine-User-Idrequired | string | Your Pine user ID from authentication |
| Content-Typerequired | string | application/json |
| Parameter | Type | Description |
|---|---|---|
| dialed_numberrequired | string | Phone number to call in E.164 format (e.g. +14155551234) |
| callee_namerequired | string | Name of the person or business being called |
| callee_contextrequired | string | Background context about the callee (e.g. "Local dentist office", "My internet service provider") |
| call_objectiverequired | string | Clear, specific goal for the call (e.g. "Schedule an appointment for next Tuesday") |
| detailed_instructionsoptional | string | Additional strategy or instructions for the voice agent |
| calleroptional | string | "negotiator" or "communicator". Default: "negotiator". See Caller Types below for detailed guidance on when to use each and what information to provide. |
| voiceoptional | string | "male" or "female". Default: "female" |
| max_duration_minutesoptional | integer | Maximum call duration in minutes (1–120). Default: 120 |
| enable_summaryoptional | boolean | Request an LLM-generated summary after the call. Default: false. Most AI agents can process the full transcript directly, so the summary is opt-in to save latency and cost. |
curl -X POST https://agent3-api-gateway-staging.19pine.ai/api/v2/voice/call \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "X-Pine-User-Id: 1234567890" \
-H "Content-Type: application/json" \
-d '{
"dialed_number": "+14155551234",
"callee_name": "Dr. Smith Office",
"callee_context": "Local dentist office, open Mon-Fri 9am-5pm",
"call_objective": "Schedule a dental cleaning appointment for next Tuesday afternoon",
"caller": "communicator",
"voice": "female",
"max_duration_minutes": 10
}'
{
"call_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "in_progress"
}
| Field | Type | Description |
|---|---|---|
| call_id | string | Unique identifier for the call. Use this to poll for status. |
| status | string | Initial status, always "in_progress" |
Caller ID: The call will appear to the recipient as coming from your registered Pine phone number.
The caller parameter selects which Pine voice agent personality handles the call. Choosing the right type — and providing the right level of detail — is critical to call success.
"negotiator"
Purpose-trained for complex negotiations, bill reductions, account disputes, contract discussions, and any call where the agent needs to persuade, counter-offer, or hold firm on your behalf.
Best for:
callee_context and detailed_instructions. Include: target outcome, acceptable range, leverage points, current terms, fallback positions, and any constraints (e.g. "don't change plan tier").
"communicator"
Handles general-purpose routine calls — scheduling, reservations, inquiries, confirmations, and straightforward requests where no persuasion or negotiation is needed.
Best for:
Negotiation calls require strategy details. If you use "negotiator", calls without a clear negotiation strategy (target price, current terms, acceptable range, fallback positions) are likely to fail or produce poor results. The voice agent cannot ask you for missing details mid-call — all strategy must be provided upfront in callee_context and detailed_instructions.
{
"dialed_number": "+18005551234",
"callee_name": "Internet Provider Support",
"callee_context": "My internet service provider. Account #987654321. Current plan: 200 Mbps at $89.99/mo, subscribed for 3 years. Competitor offer: same speed at $49.99/mo from a local provider.",
"call_objective": "Negotiate the monthly bill down from $89.99 to $60/mo or less",
"detailed_instructions": "Target: $60/mo. Acceptable range: up to $65/mo. Mention the competitor offer as leverage. If they offer a promotional rate, ensure it lasts at least 12 months. Do NOT agree to change the plan tier or speed. If they cannot offer any discount, ask to speak with the retention department. As a last resort, request to cancel and see if a retention offer comes.",
"caller": "negotiator",
"voice": "male",
"max_duration_minutes": 30
}
{
"dialed_number": "+14155559876",
"callee_name": "The Italian Place",
"callee_context": "Italian restaurant on Main Street. Making a reservation for dinner.",
"call_objective": "Make a dinner reservation for 4 people tonight at 7pm",
"detailed_instructions": "If 7pm is not available, try 7:30 or 8pm. Prefer a booth if possible. Name for the reservation: Jane Doe.",
"caller": "communicator",
"voice": "female",
"max_duration_minutes": 10
}
Retrieves the current status of a call. Poll this endpoint periodically until the call completes, or use the SSE stream endpoint to wait for the final result instead. Once the call completes, the response includes the full transcript (and summary if enable_summary was requested). Note: no real-time intermediate updates (partial transcripts, phase changes) are currently available while the call is in progress.
| Parameter | Type | Description |
|---|---|---|
| call_idrequired | string | The call_id returned from the initiate call endpoint |
| Header | Value |
|---|---|
| Authorizationrequired | Bearer <access_token> |
| X-Pine-User-Idrequired | Your Pine user ID |
curl https://agent3-api-gateway-staging.19pine.ai/api/v2/voice/call/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "X-Pine-User-Id: 1234567890"
{
"call_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "in_progress",
"duration_seconds": 60
}
Note: No real-time intermediate updates are available while the call is in progress. The SDK normalizes raw API statuses (e.g. HungupByPeer) to canonical values (completed, failed, cancelled).
{
"call_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "completed",
"duration_seconds": 180,
"summary": "Successfully scheduled a dental cleaning for Tuesday at 2pm.",
"transcript": [
{ "speaker": "agent", "text": "Hello, I'm calling to schedule a dental cleaning appointment." },
{ "speaker": "user", "text": "Sure, let me check our availability." },
{ "speaker": "user", "text": "We have Tuesday at 2pm available." },
{ "speaker": "agent", "text": "That works perfectly. Please book it." }
],
"credits_charged": 110
}
| Field | Type | Description |
|---|---|---|
| call_id | string | Unique call identifier |
| status | string | Call status (see Call Statuses) |
| duration_seconds | integer | Total call duration in seconds |
| summary | string | AI-generated summary of the call outcome. Only present when enable_summary was set to true in the request. |
| transcript | array | Full conversation transcript as an array of speaker/text objects |
| credits_charged | integer | Total credits deducted for this call |
Opens a Server-Sent Events (SSE) stream that waits for the final call result and delivers it as soon as it's available. This is a more efficient alternative to polling — instead of making repeated HTTP requests, the server notifies the client over a single long-lived connection when the call completes.
No real-time updates. The SSE stream does not provide real-time intermediate events such as partial transcripts, "call connected" notifications, or live conversation updates. The only event you will receive is the final result event containing the complete transcript after the call ends. There are no intermediate progress events during the call.
Recommended. The official Python and Node.js SDKs use this endpoint automatically when you call create_and_wait() / createAndWait(). If SSE fails, they fall back to polling the GET endpoint. No code changes needed.
| Parameter | Type | Description |
|---|---|---|
| call_idrequired | string | The call_id returned from the initiate call endpoint |
| Header | Value | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <access_token> |
| X-Pine-User-Idrequired | string | Your Pine user ID from authentication |
| Acceptoptional | string | text/event-stream |
| Last-Event-Idoptional | string | Event ID from a previous connection for reconnection. If the call already completed, the server sends the result immediately. |
The stream emits these event types:
| Event | Description |
|---|---|
| result | Final call result with full transcript (and summary if requested). This is the primary event you should listen for. Sent once when the call completes, then the connection closes. |
| error | An error occurred (e.g., stream timeout). The connection closes after this event. |
The server also sends SSE comment lines (: heartbeat) every ~15 seconds to keep the connection alive through proxies and load balancers.
Note: Intermediate status and transcript events are defined in the protocol but are not currently emitted. The server does not provide real-time partial transcripts, "call connected" events, or any other intermediate progress updates. The only meaningful event is result, delivered after the call ends.
id: 1
event: result
data: {"call_id":"a1b2c3d4-...","status":"completed","duration_seconds":180,"summary":"Successfully scheduled...","transcript":[...],"credits_charged":110}
curl -N https://agent3-api-gateway-staging.19pine.ai/api/v2/voice/call/CALL_ID/stream \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-Pine-User-Id: YOUR_USER_ID" \
-H "Accept: text/event-stream"
const res = await fetch(
`https://agent3-api-gateway-staging.19pine.ai/api/v2/voice/call/${callId}/stream`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"X-Pine-User-Id": userId,
"Accept": "text/event-stream",
},
}
);
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
// Parse SSE lines: look for "event: result" + "data: {...}"
console.log(chunk);
}
Each event includes an id field. If the connection drops, reconnect with the Last-Event-Id header set to the last received ID. If the call has already completed, the server will send the result immediately on reconnect.
Fallback. If SSE is unavailable (e.g., your proxy doesn't support streaming), fall back to polling GET /api/v2/voice/call/{call_id} every 10–30 seconds. The official SDKs do this automatically.
Not recommended for production. The raw API examples below are provided for educational purposes only — to illustrate how the REST endpoints work under the hood. For production use, we strongly recommend using the official Python SDK or Node.js SDK, which handle authentication, status normalization, SSE streaming with automatic polling fallback, and error handling for you.
If you prefer to call the REST API directly without an SDK, here's the full flow. Enter your details below and the code examples will update automatically.
This Python script uses raw HTTP requests (no SDK) to walk through the full flow: authenticate, call your phone with a short test message, then poll until the call completes and print the transcript.
import requests
import time
# ---- Configuration ----
EMAIL = "[email protected]"
MY_PHONE = "+14155551234"
AUTH_API = "https://www.19pine.ai"
GATEWAY = "https://agent3-api-gateway-staging.19pine.ai"
# ---- Step 1: Request verification code ----
print("Requesting verification code...")
resp = requests.post(f"{AUTH_API}/api/v2/auth/email/request", json={
"email": EMAIL
})
resp.raise_for_status()
request_token = resp.json()["data"]["request_token"]
print("Check your email for the verification code.")
# ---- Step 2: Verify code and get credentials ----
code = input("Enter the code from your email: ")
resp = requests.post(f"{AUTH_API}/api/v2/auth/email/verify", json={
"email": EMAIL,
"request_token": request_token,
"code": code
})
resp.raise_for_status()
creds = resp.json()["data"]
access_token = creds["access_token"]
user_id = creds["id"]
print(f"Authenticated! user_id={user_id}")
# ---- Step 3: Make a test call to yourself ----
headers = {
"Authorization": f"Bearer {access_token}",
"X-Pine-User-Id": user_id,
"Content-Type": "application/json",
}
print(f"Initiating test call to {MY_PHONE}...")
resp = requests.post(f"{GATEWAY}/api/v2/voice/call", headers=headers, json={
"dialed_number": MY_PHONE,
"callee_name": "Myself",
"callee_context": "This is a test call to verify my Pine Voice API integration is working.",
"call_objective": "Tell the person that Pine Voice is set up and working correctly. Confirm the setup is complete and say goodbye.",
"caller": "communicator",
"voice": "female",
"max_duration_minutes": 5,
})
resp.raise_for_status()
call_id = resp.json()["call_id"]
print(f"Call initiated! call_id={call_id}")
print("Your phone should ring shortly. Answer to hear the test message.")
# ---- Step 4: Poll for result ----
print("Polling for result...")
while True:
time.sleep(10)
resp = requests.get(
f"{GATEWAY}/api/v2/voice/call/{call_id}",
headers=headers,
)
result = resp.json()
status = result["status"]
elapsed = result.get("duration_seconds", 0)
print(f" Status: {status} ({elapsed}s elapsed)")
if status in ("completed", "failed", "cancelled"):
break
# ---- Step 5: Print results ----
print(f"\n--- Call {status} ---")
print(f"Duration: {result.get('duration_seconds', 0)}s")
print(f"Credits: {result.get('credits_charged', 0)}")
print(f"\nSummary:\n {result.get('summary', 'N/A')}")
print(f"\nTranscript:")
for entry in result.get("transcript", []):
print(f" [{entry['speaker']}] {entry['text']}")
And the same flow using cURL:
# Step 1: Request verification code
curl -s -X POST https://www.19pine.ai/api/v2/auth/email/request \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'
# Save the request_token from the response
# Step 2: Verify code (replace REQUEST_TOKEN and CODE)
curl -s -X POST https://www.19pine.ai/api/v2/auth/email/verify \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "request_token": "REQUEST_TOKEN", "code": "CODE"}'
# Response: {"status":"success","data":{"id":"...","access_token":"..."}}
# Save data.id as YOUR_USER_ID and data.access_token as YOUR_ACCESS_TOKEN
# Step 3: Make a test call to yourself
curl -s -X POST https://agent3-api-gateway-staging.19pine.ai/api/v2/voice/call \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-Pine-User-Id: YOUR_USER_ID" \
-H "Content-Type: application/json" \
-d '{
"dialed_number": "+14155551234",
"callee_name": "Myself",
"callee_context": "This is a test call to verify my Pine Voice API integration is working.",
"call_objective": "Tell the person that Pine Voice is set up and working correctly. Confirm the setup is complete and say goodbye.",
"caller": "communicator",
"voice": "female",
"max_duration_minutes": 5
}'
# Save the call_id from the response
# Step 4: Poll for result (repeat until status is terminal)
curl -s https://agent3-api-gateway-staging.19pine.ai/api/v2/voice/call/CALL_ID \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-Pine-User-Id: YOUR_USER_ID"
The easiest way to integrate Pine Voice is with the official SDKs. They handle authentication, request formatting, SSE result delivery with automatic polling fallback, and error handling for you.
pip install pine-voice
Sync & async support. Python 3.9+.
npm install pine-voice
Zero dependencies. Node 18+.
from pine_voice import PineVoice
# Authenticate (one-time setup)
request_token = PineVoice.auth.request_code("[email protected]")
code = input("Enter the code from your email: ")
credentials = PineVoice.auth.verify_code("[email protected]", request_token, code)
# Create client and make a call
client = PineVoice(
access_token=credentials.access_token,
user_id=credentials.user_id,
)
result = client.calls.create_and_wait(
to="+14155551234",
name="Dr. Smith Office",
context="Local dentist office. Open Mon-Fri 9am-5pm.",
objective="Schedule a dental cleaning for next Tuesday afternoon",
)
print(result.status, result.transcript)
import { PineVoice } from "pine-voice";
// Authenticate (one-time setup)
const { requestToken } = await PineVoice.auth.requestCode("[email protected]");
const { accessToken, userId } = await PineVoice.auth.verifyCode(
"[email protected]", requestToken, "1234"
);
// Create client and make a call
const client = new PineVoice({ accessToken, userId });
const result = await client.calls.createAndWait({
to: "+14155551234",
name: "Dr. Smith Office",
context: "Local dentist office. Open Mon-Fri 9am-5pm.",
objective: "Schedule a dental cleaning for next Tuesday afternoon",
});
console.log(result.status, result.transcript);
Tip: Once you have your access_token and user_id, save them as environment variables (PINE_ACCESS_TOKEN, PINE_USER_ID) and create the client with no arguments: PineVoice() / new PineVoice(). No need to repeat the auth flow.
Full API reference for the pine-voice Python package. Supports both synchronous and asynchronous usage.
pip install pine-voice
Creates a synchronous Pine Voice client. Credentials can be passed directly or resolved from environment variables.
| Parameter | Type | Description |
|---|---|---|
| access_tokenoptional | str | Pine access token. Falls back to PINE_ACCESS_TOKEN env var. |
| user_idoptional | str | Pine user ID. Falls back to PINE_USER_ID env var. |
| gateway_urloptional | str | Voice API gateway URL. Falls back to PINE_GATEWAY_URL env var. Default: https://agent3-api-gateway-staging.19pine.ai |
# Option 1: Explicit credentials
client = PineVoice(access_token="...", user_id="...")
# Option 2: From environment variables
# export PINE_ACCESS_TOKEN=... PINE_USER_ID=...
client = PineVoice()
# Use as a context manager to auto-close
with PineVoice() as client:
result = client.calls.create_and_wait(...)
Static authentication helpers. No credentials required — used to obtain credentials.
Sends a verification code to the given email address. Returns the request_token (a string) needed for the verify step.
| Parameter | Type | Description |
|---|---|---|
| emailrequired | str | Your Pine AI account email |
Returns: str — the request_token.
Verifies the email code and returns access credentials.
| Parameter | Type | Description |
|---|---|---|
| emailrequired | str | Same email used in request step |
| request_tokenrequired | str | Token from request_code() |
| coderequired | str | Verification code from email |
Returns: Credentials(access_token: str, user_id: str)
Initiates a phone call. Returns immediately with a call ID. Use get() to poll or create_and_wait() for a blocking call.
| Parameter | Type | Description |
|---|---|---|
| torequired | str | Phone number in E.164 format |
| namerequired | str | Name of the person or business being called |
| contextrequired | str | Background context about the callee |
| objectiverequired | str | Specific goal for the call |
| instructionsoptional | str | Detailed strategy and instructions |
| calleroptional | str | "negotiator" or "communicator" |
| voiceoptional | str | "male" or "female" |
| max_duration_minutesoptional | int | Max call duration in minutes (1–120). Default: 120 |
| enable_summaryoptional | bool | Request an AI summary. Default: False |
Returns: CallInitiated(call_id: str, status: str)
Gets the current status of a call. Returns CallStatus while in progress, or CallResult when the call reaches a terminal state.
| Parameter | Type | Description |
|---|---|---|
| call_idrequired | str | The call ID from create() |
Returns: CallStatus | CallResult
import time
from pine_voice import PineVoice, CallResult
client = PineVoice()
initiated = client.calls.create(
to="+14155551234",
name="Dr. Smith Office",
context="Local dentist office",
objective="Schedule a cleaning",
)
print(f"Call started: {initiated.call_id}")
# Poll until complete
while True:
time.sleep(10)
result = client.calls.get(initiated.call_id)
if result.status in ("completed", "failed", "cancelled"):
break
print(f" Still in progress... {result.duration_seconds}s")
if isinstance(result, CallResult):
for entry in result.transcript:
print(f"[{entry.speaker}] {entry.text}")
Initiates a call and blocks until it reaches a terminal state. Uses SSE streaming by default, with automatic fallback to polling if SSE fails. This is the recommended method for most use cases.
| Parameter | Type | Description |
|---|---|---|
| torequired | str | Phone number in E.164 format |
| namerequired | str | Name of the person or business being called |
| contextrequired | str | Background context about the callee |
| objectiverequired | str | Specific goal for the call |
| instructionsoptional | str | Detailed strategy and instructions |
| calleroptional | str | "negotiator" or "communicator" |
| voiceoptional | str | "male" or "female" |
| max_duration_minutesoptional | int | Max call duration in minutes (1–120). Default: 120 |
| enable_summaryoptional | bool | Request an AI summary. Default: False |
| poll_intervaloptional | int | Seconds between polls for polling fallback. Default: 10 |
| use_sseoptional | bool | Try SSE first. Set False to force polling. Default: True |
| on_progressoptional | Callable | Callback invoked with CallProgress after each poll cycle |
Returns: CallResult
result = client.calls.create_and_wait(
to="+14155551234",
name="Dr. Smith Office",
context="Local dentist office. I'm Jane Doe, existing patient.",
objective="Schedule a dental cleaning for next Tuesday afternoon",
instructions="If Tuesday is unavailable, try Wednesday or Thursday.",
enable_summary=True,
)
print(f"Status: {result.status}")
print(f"Duration: {result.duration_seconds}s")
print(f"Credits: {result.credits_charged}")
print(f"Summary: {result.summary}")
for entry in result.transcript:
print(f"[{entry.speaker}] {entry.text}")
Asynchronous client with the same API as PineVoice, but all methods are async. Use with asyncio. Has its own AsyncPineVoice.auth with async request_code() and verify_code().
import asyncio
from pine_voice import AsyncPineVoice
async def main():
async with AsyncPineVoice() as client:
result = await client.calls.create_and_wait(
to="+14155551234",
name="Dr. Smith Office",
context="Local dentist office",
objective="Schedule a cleaning",
)
print(result.transcript)
asyncio.run(main())
All types are dataclasses importable from pine_voice.
| Type | Fields | Description |
|---|---|---|
| Credentials | access_token, user_id | Returned by auth.verify_code() |
| CallInitiated | call_id, status | Returned by calls.create() |
| CallStatus | call_id, status, duration_seconds | Returned by calls.get() while in progress |
| CallResult | call_id, status, duration_seconds, summary, transcript, credits_charged | Returned when call reaches terminal state |
| CallProgress | call_id, status, duration_seconds | Passed to on_progress callback |
| TranscriptEntry | speaker, text | A single turn in the transcript |
All exceptions inherit from PineVoiceError, which provides code, message, and status attributes.
| Exception | When |
|---|---|
| PineVoiceError | Base class for all SDK errors |
| AuthError | Authentication failures (401, missing credentials, expired token) |
| CallError | Call-specific errors (invalid phone, DND blocked, policy violation) |
| RateLimitError | Rate limit exceeded (429) |
from pine_voice import PineVoice, AuthError, CallError, RateLimitError
try:
result = client.calls.create_and_wait(...)
except AuthError as e:
print(f"Auth failed: {e.code} — {e.message}")
except RateLimitError:
print("Too many concurrent calls, try again later")
except CallError as e:
print(f"Call error: {e.code} — {e.message}")
Full API reference for the pine-voice npm package. Zero dependencies, works with Node.js 18+ and any runtime with fetch.
npm install pine-voice
Creates a Pine Voice client. Credentials can be passed in the config object or resolved from environment variables.
| Config field | Type | Description |
|---|---|---|
| accessTokenoptional | string | Pine access token. Falls back to PINE_ACCESS_TOKEN env var. |
| userIdoptional | string | Pine user ID. Falls back to PINE_USER_ID env var. |
| gatewayUrloptional | string | Voice API gateway URL. Falls back to PINE_GATEWAY_URL env var. |
import { PineVoice } from "pine-voice";
// Option 1: Explicit credentials
const client = new PineVoice({ accessToken: "...", userId: "..." });
// Option 2: From environment variables
// PINE_ACCESS_TOKEN=... PINE_USER_ID=...
const client = new PineVoice();
Static authentication helpers. No credentials required — used to obtain credentials.
Sends a verification code to the given email address.
| Parameter | Type | Description |
|---|---|---|
| emailrequired | string | Your Pine AI account email |
Returns: Promise<{ requestToken: string }>
Verifies the email code and returns access credentials.
| Parameter | Type | Description |
|---|---|---|
| emailrequired | string | Same email used in request step |
| requestTokenrequired | string | Token from requestCode() |
| coderequired | string | Verification code from email |
Returns: Promise<{ accessToken: string, userId: string }>
Initiates a phone call. Returns immediately with a call ID.
| Parameter | Type | Description |
|---|---|---|
| torequired | string | Phone number in E.164 format |
| namerequired | string | Name of the person or business being called |
| contextrequired | string | Background context about the callee |
| objectiverequired | string | Specific goal for the call |
| instructionsoptional | string | Detailed strategy and instructions |
| calleroptional | string | "negotiator" or "communicator" |
| voiceoptional | string | "male" or "female" |
| maxDurationMinutesoptional | number | Max call duration in minutes (1–120). Default: 120 |
| enableSummaryoptional | boolean | Request an AI summary. Default: false |
Returns: Promise<CallInitiated> with callId and status
Gets the current status of a call. Returns CallStatus while in progress, or CallResult when the call reaches a terminal state.
| Parameter | Type | Description |
|---|---|---|
| callIdrequired | string | The call ID from create() |
Returns: Promise<CallStatus | CallResult>
const { callId } = await client.calls.create({
to: "+14155551234",
name: "Dr. Smith Office",
context: "Local dentist office",
objective: "Schedule a cleaning",
});
// Poll until complete
while (true) {
await new Promise(r => setTimeout(r, 10000));
const result = await client.calls.get(callId);
if (["completed", "failed", "cancelled"].includes(result.status)) {
console.log(result);
break;
}
}
Initiates a call and waits until it reaches a terminal state. Uses SSE streaming by default, with automatic fallback to polling. This is the recommended method for most use cases.
The first argument is the same CreateCallParams object as create(). The second argument is an optional WaitOptions object:
| Option | Type | Description |
|---|---|---|
| pollIntervalMsoptional | number | Milliseconds between polls for polling fallback. Default: 10000 |
| signaloptional | AbortSignal | Abort signal to cancel waiting |
| useSSEoptional | boolean | Try SSE first. Set false to force polling. Default: true |
| onProgressoptional | function | Callback invoked with CallProgress after each poll cycle |
Returns: Promise<CallResult>
const result = await client.calls.createAndWait(
{
to: "+14155551234",
name: "Dr. Smith Office",
context: "Local dentist office. I'm Jane Doe, existing patient.",
objective: "Schedule a dental cleaning for next Tuesday afternoon",
enableSummary: true,
},
{
// Optional: abort after 5 minutes
signal: AbortSignal.timeout(300_000),
}
);
console.log(result.status); // "completed"
console.log(result.durationSeconds); // 45
console.log(result.creditsCharged); // 70
console.log(result.summary); // "Scheduled cleaning for Tuesday 2pm"
console.log(result.transcript); // [{ speaker, text }, ...]
All types are exported from pine-voice and available for import in TypeScript.
| Type | Fields | Description |
|---|---|---|
| PineVoiceConfig | accessToken?, userId?, gatewayUrl? | Constructor config |
| CreateCallParams | to, name, context, objective, instructions?, caller?, voice?, maxDurationMinutes?, enableSummary? | Parameters for create() and createAndWait() |
| CallInitiated | callId, status | Returned by create() |
| CallStatus | callId, status, durationSeconds? | Returned by get() while in progress |
| CallResult | callId, status, durationSeconds, summary, transcript, creditsCharged | Returned when call reaches terminal state |
| WaitOptions | pollIntervalMs?, signal?, useSSE?, onProgress? | Options for createAndWait() |
| CallProgress | callId, status, durationSeconds? | Passed to onProgress callback |
| TranscriptEntry | speaker, text | A single turn in the transcript |
All errors extend PineVoiceError, which provides code, message, and status properties.
| Error class | When |
|---|---|
| PineVoiceError | Base class for all SDK errors |
| AuthError | Authentication failures (401, missing credentials, expired token) |
| CallError | Call-specific errors (invalid phone, DND blocked, policy violation) |
| RateLimitError | Rate limit exceeded (429) |
import { PineVoice, AuthError, CallError, RateLimitError } from "pine-voice";
try {
const result = await client.calls.createAndWait({...});
} catch (err) {
if (err instanceof AuthError) {
console.error(`Auth failed: ${err.code} — ${err.message}`);
} else if (err instanceof RateLimitError) {
console.error("Too many concurrent calls, try again later");
} else if (err instanceof CallError) {
console.error(`Call error: ${err.code} — ${err.message}`);
}
}
The status field in call responses indicates the current state of the call:
| Status | Description |
|---|---|
initiated |
Call has been created and is about to dial |
in_progress |
Call is currently active |
completed |
Call finished successfully — transcript available (and summary if requested) |
failed |
Call failed (no answer, network issue, safety rejection, etc.) |
cancelled |
Call was cancelled before completion |
Not currently available. The phase field is defined in the schema but is not populated during in-progress calls. You will not receive phase updates such as "connected" (callee answered). The phases below are documented for reference only and may be supported in a future release.
When a call has status: "in_progress", the phase field would provide more detail on what stage the call is in:
| Phase | Description |
|---|---|
initiated |
Call accepted, voice agent is dialing the number |
connected |
Callee answered, conversation is active |
completed |
Call finished normally |
failed |
Call failed (no answer, network issue, etc.) |
no_answer |
Nobody picked up the phone |
declined |
Call was rejected by the callee |
Do not rely on the status field to determine whether the call objective was achieved. A status like HungupByPeer only means the other party hung up — it does not mean success. You must read the full transcript to determine the actual outcome.
Treat the call as a failure if any of these are true:
The transcript field is an array of objects, each representing one turn in the conversation:
[
{ "speaker": "agent", "text": "Hello, I'm calling to..." },
{ "speaker": "user", "text": "Hi, how can I help?" }
]
The speaker field is either "agent" (Pine's voice agent) or "user" (the person who answered the call).
When an API request fails, the response body contains an error object with a machine-readable code and human-readable message:
{
"error": {
"code": "ERROR_CODE",
"message": "A human-readable description of the error."
}
}
| Code | HTTP Status | Description |
|---|---|---|
AUTH_REQUIRED |
401 | Missing or invalid Authorization header or X-Pine-User-Id |
TOKEN_EXPIRED |
401 | Access token has expired — re-authenticate to get a new one |
SUBSCRIPTION_REQUIRED |
403 | No active Pine AI Pro subscription found for this account |
INSUFFICIENT_CREDITS |
402 | Not enough credits to make a call. Add credits at 19pine.ai. |
INVALID_REQUEST |
400 | Request body is malformed or missing required fields |
INVALID_PHONE |
400 | Phone number is not in valid E.164 format or cannot be called |
INSUFFICIENT_DETAIL |
400 | Call objective or instructions are too vague for the agent to act on |
RATE_LIMITED |
429 | Too many calls — exceeded concurrent call limits |
DND_BLOCKED |
403 | Target number is on a do-not-call list |
POLICY_VIOLATION |
403 | Call was rejected for violating usage policies |
NOT_FOUND |
404 | The specified call_id does not exist |
ACCESS_DENIED |
403 | You do not have permission to access this call |
INTERNAL |
500 | Unexpected server error — retry or contact support |
To ensure fair usage and prevent abuse, the following limits are enforced:
| Limit | Value | Scope |
|---|---|---|
| Concurrent calls | 5 | Maximum in-progress calls at any given time per user |
| Max call duration | 120 minutes | Maximum duration for any single call |
Exceeding rate limits returns a RATE_LIMITED error (HTTP 429). Wait before retrying.
English only. The voice agent currently supports English only. Calls can be placed to the US, Canada, Puerto Rico (+1), UK (+44), Australia (+61), New Zealand (+64), Singapore (+65), Ireland (+353), and Hong Kong (+852).
Phone format: All phone numbers must be in E.164 format, including the + prefix and country code (e.g. +14155551234 for US numbers).