pine-voice
v0.1.4
Published
Official Pine AI Voice SDK — make phone calls via Pine's AI voice agent
Maintainers
Readme
Pine Voice SDK for JavaScript / TypeScript
Official SDK for Pine AI voice calls. Make phone calls via Pine's AI voice agent from any Node.js application — no MCP client or OpenClaw required.
Install
npm install pine-voiceQuick start
import { PineVoice } from "pine-voice";
const client = new PineVoice({
accessToken: "your-access-token",
userId: "your-user-id",
});
const result = await client.calls.createAndWait({
to: "+14155551234",
name: "Dr. Smith Office",
context:
"Local dentist office. I'm an existing patient (Jane Doe, DOB 03/15/1990). " +
"Open Mon-Fri 9am-5pm. Dr. Smith is my preferred dentist but Dr. Lee is also fine.",
objective: "Schedule a dental cleaning for next Tuesday afternoon, ideally 2-4pm",
instructions:
"If Tuesday afternoon is unavailable, try Wednesday or Thursday afternoon. " +
"If no afternoons are open this week, take the earliest available afternoon next week. " +
"Confirm the appointment date, time, and dentist name before hanging up.",
});
console.log(result.transcript);Authentication
Option A: Pass credentials directly
const client = new PineVoice({
accessToken: "your-access-token",
userId: "your-user-id",
});Option B: Use environment variables
export PINE_ACCESS_TOKEN="your-access-token"
export PINE_USER_ID="your-user-id"const client = new PineVoice(); // reads from envGetting credentials
If you don't have credentials yet, use the auth helpers:
import { PineVoice } from "pine-voice";
// Step 1: Request a verification code (sent to your Pine AI account email)
const { requestToken } = await PineVoice.auth.requestCode("[email protected]");
// Step 2: Enter the code from your email
const { accessToken, userId } = await PineVoice.auth.verifyCode(
"[email protected]",
requestToken,
"1234", // code from email
);
// Step 3: Use the credentials
const client = new PineVoice({ accessToken, userId });Making calls
Fire and poll
// Initiate (returns immediately)
const { callId } = await client.calls.create({
to: "+14155552345",
name: "Bay Area Auto Care",
context:
"Local auto repair shop. My car is a 2019 Honda Civic, ~45,000 miles. " +
"Due for a routine oil change and tire rotation. No warning lights or known issues.",
objective:
"Schedule an oil change and tire rotation for this Friday morning, ideally before noon",
instructions:
"If Friday morning is full, try Friday afternoon. " +
"If Friday is completely booked, try next Monday or Tuesday morning. " +
"Ask for a price estimate for both services combined. " +
"Ask how long the service will take so I know when to pick up the car. " +
"Confirm the appointment date, time, services, and estimated cost before hanging up.",
caller: "communicator",
voice: "female",
maxDurationMinutes: 10,
});
// Poll until complete
const status = await client.calls.get(callId);Call and wait (SSE with polling fallback)
const result = await client.calls.createAndWait(
{
to: "+14155559876",
name: "Bella Italia Restaurant",
context:
"Italian restaurant in downtown SF. Reservation for Mike Chen. " +
"Party of 4 adults, no children. One guest is vegetarian, one has a nut allergy.",
objective: "Make a dinner reservation for tonight at 7pm for 4 people",
instructions:
"If 7pm is not available, try 7:30pm or 8pm. " +
"If tonight is fully booked, try tomorrow (Saturday) at the same times. " +
"Request a booth or quiet table if possible, but not required. " +
"Mention the nut allergy and ask if they can accommodate it. " +
"Confirm the reservation date, time, party size, and name on the reservation.",
},
{
// SSE is used by default to wait for the final result.
// Falls back to polling if SSE is unavailable.
pollIntervalMs: 10_000, // polling fallback interval (default 10s)
signal: abortController.signal, // optional cancellation
},
);
console.log(result.status); // "completed" | "failed" | "cancelled"
console.log(result.transcript); // full conversation
console.log(result.summary); // LLM summary (empty unless enableSummary: true)
console.log(result.creditsCharged); // credits usedError handling
import { PineVoice, AuthError, RateLimitError, CallError } from "pine-voice";
try {
const result = await client.calls.createAndWait({ ... });
} catch (err) {
if (err instanceof AuthError) {
// Token expired or invalid — re-authenticate
console.error("Auth failed:", err.code, err.message);
} else if (err instanceof RateLimitError) {
// Too many calls — wait and retry
console.error("Rate limited:", err.message);
} else if (err instanceof CallError) {
// Call-specific issue (invalid phone, DND, policy, etc.)
console.error("Call error:", err.code, err.message);
}
}API reference
new PineVoice(config?)
| Option | Type | Default | Description |
|---|---|---|---|
| accessToken | string | PINE_ACCESS_TOKEN env | Pine access token |
| userId | string | PINE_USER_ID env | Pine user ID |
| gatewayUrl | string | https://agent3-api-gateway-staging.19pine.ai | Voice API gateway URL |
PineVoice.auth.requestCode(email)
Request a verification code. Returns { requestToken }.
PineVoice.auth.verifyCode(email, requestToken, code)
Verify the code and get credentials. Returns { accessToken, userId }.
client.calls.create(params)
Initiate a call. Returns { callId, status }.
| Param | Type | Required | Description |
|---|---|---|---|
| to | string | Yes | Phone number in E.164 format. Supported countries: US/CA/PR (+1), UK (+44), AU (+61), NZ (+64), SG (+65), IE (+353), HK (+852) |
| name | string | Yes | Name of the person or business being called |
| context | string | Yes | Background context about the callee and info needed during the call |
| objective | string | Yes | Specific goal the call should accomplish |
| instructions | string | No | Detailed strategy and instructions for the voice agent |
| caller | "negotiator" \| "communicator" | No | Caller personality. "negotiator" for complex negotiations (requires thorough strategy in context/instructions). "communicator" for general tasks. Default: "negotiator" |
| voice | "male" \| "female" | No | Voice gender. Default: "female" |
| maxDurationMinutes | number | No | Max call duration in minutes (1-120). Default: 120 |
| enableSummary | boolean | No | 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. |
client.calls.get(callId)
Get call status. Returns CallStatus or CallResult if terminal.
client.calls.createAndWait(params, options?)
Initiate and wait until complete. Returns CallResult.
Uses SSE to wait for the final call result. If the SSE connection fails or the server doesn't support it, automatically falls back to polling. Reconnects once on SSE connection drop before falling back.
Important: Real-time intermediate updates (partial transcripts, "call connected" events) are not currently available. The SSE stream delivers only the final transcript after the call completes. There are no intermediate progress events during the call.
| Option | Type | Default | Description |
|---|---|---|---|
| pollIntervalMs | number | 10000 | Polling interval in ms (fallback only) |
| signal | AbortSignal | — | Abort signal for cancellation |
| useSSE | boolean | true | Try SSE first. Set false to force polling. |
| onProgress | (progress: CallProgress) => void | — | Callback invoked with a CallProgress object after each poll cycle during polling fallback. Note: real-time progress events are not currently available. |
Supported countries
The voice agent can only speak English. Calls can be placed to the following countries:
- US, Canada, and Puerto Rico (+1)
- United Kingdom (+44)
- Australia (+61)
- New Zealand (+64)
- Singapore (+65)
- Ireland (+353)
- Hong Kong (+852)
Calls to numbers outside these country codes will be rejected with a POLICY_VIOLATION error.
Requirements
- Node.js 18+ (uses native
fetch) - Pine AI Pro subscription (19pine.ai)
License
MIT
