# agentcall

Node.js SDK for [AgentCall](https://agentcall.co) — programmable phone numbers for AI agents.

Provision numbers, send/receive SMS, extract OTP codes, and initiate voice calls from your AI agent or backend service.

## Install

```bash
npm install agentcall
```

## Quick start

```typescript
import AgentCall from 'agentcall'

const client = new AgentCall('ac_live_xxxxxxxxxxxxx')

// 1. Provision a phone number
const number = await client.numbers.provision({ country: 'US', type: 'local' })
console.log(number.number) // +12125551234

// 2. Send an SMS
await client.sms.send({
  from: number.number,
  to: '+14155559876',
  body: 'Your verification code will arrive shortly.',
})

// 3. Wait for an OTP (polls inbox automatically)
const otp = await client.sms.waitForOTP(number.id, { timeout: 60000 })
console.log(otp) // "482913"
```

## Authentication

Get your API key from the [AgentCall dashboard](https://agentcall.co/dashboard). Keys are prefixed with `ac_live_`.

```typescript
const client = new AgentCall('ac_live_xxxxxxxxxxxxx')
```

## Configuration

```typescript
const client = new AgentCall('ac_live_xxx', {
  baseUrl: 'https://api.agentcall.co', // default
  timeout: 30000,                       // request timeout in ms, default 30s
})
```

## API Reference

### `client.numbers`

#### `numbers.provision(options?)`

Provision a new phone number.

```typescript
const number = await client.numbers.provision({
  country: 'US',          // optional, default 'US'
  type: 'local',          // 'local' | 'tollfree' | 'mobile'
  label: 'Signup flow',   // optional human-readable label
})
```

**Returns:** `PhoneNumber`

```typescript
{
  id: string
  number: string          // E.164 format, e.g. '+12125551234'
  country: string
  type: string
  label: string | null
  status: string          // 'active' | 'released'
  monthlyRate: number
  provisionedAt: string   // ISO 8601
}
```

#### `numbers.list(params?)`

List all provisioned numbers.

```typescript
const result = await client.numbers.list({
  limit: 20,        // optional, default 20
  cursor: undefined, // optional, for pagination
  country: 'US',    // optional filter
  type: 'local',    // optional filter
})
// result.data — PhoneNumber[]
// result.hasMore — boolean
// result.nextCursor — string | null
```

#### `numbers.get(numberId)`

Get a single phone number by ID.

```typescript
const number = await client.numbers.get('num_abc123')
```

#### `numbers.release(numberId)`

Release (deactivate) a phone number. Stops billing. Irreversible.

```typescript
await client.numbers.release('num_abc123')
```

---

### `client.sms`

#### `sms.send(options)`

Send an SMS from a provisioned number.

```typescript
const message = await client.sms.send({
  from: '+12125551234', // your provisioned number (E.164)
  to: '+14155559876',   // destination (E.164)
  body: 'Hello!',       // max 1600 characters
})
```

**Returns:** `Message`

```typescript
{
  id: string
  direction: string       // 'inbound' | 'outbound'
  from: string
  to: string
  body: string
  otp: string | null      // extracted OTP code, if detected
  status: string          // 'queued' | 'delivered'
  cost: number
  receivedAt: string
  createdAt: string
}
```

#### `sms.inbox(numberId, options?)`

Get inbound messages for a phone number.

```typescript
const inbox = await client.sms.inbox('num_abc123', {
  limit: 20,          // optional
  cursor: undefined,   // optional, for pagination
  since: '2025-01-01T00:00:00Z', // optional ISO timestamp
  otpOnly: true,       // optional, only return messages with OTP codes
})
```

#### `sms.get(messageId)`

Get a single message by ID.

```typescript
const msg = await client.sms.get('msg_xyz789')
```

#### `sms.waitForOTP(numberId, options?)`

Poll the inbox until an OTP code arrives. This is the primary method for AI agent verification flows.

```typescript
const otp = await client.sms.waitForOTP('num_abc123', {
  timeout: 60000,      // max wait in ms, default 60s
  pollInterval: 2000,  // poll frequency in ms, default 2s
  since: new Date().toISOString(), // only look at new messages
})

if (otp) {
  console.log(`Got OTP: ${otp}`)
} else {
  console.log('Timed out waiting for OTP')
}
```

**Returns:** `string | null` — the OTP code, or `null` if timeout.

---

### `client.calls`

#### `calls.initiate(options)`

Start an outbound phone call.

```typescript
const call = await client.calls.initiate({
  from: '+12125551234',   // your provisioned number (E.164)
  to: '+14155559876',     // destination (E.164)
  webhookUrl: 'https://example.com/call-events', // optional
  record: false,          // optional, Pro plan only ($0.01/min)
})
```

**Returns:** `Call`

```typescript
{
  id: string
  direction: string       // 'inbound' | 'outbound'
  from: string
  to: string
  status: string          // 'queued' | 'ringing' | 'in-progress' | 'completed' | 'failed'
  duration: number | null // seconds
  record: boolean
  recordingUrl: string | null
  createdAt: string
}
```

#### `calls.list(params?)`

List call history.

```typescript
const calls = await client.calls.list({ limit: 20 })
```

#### `calls.get(callId)`

Get a single call by ID.

```typescript
const call = await client.calls.get('call_abc123')
```

#### `calls.hangup(callId)`

Terminate an active call.

```typescript
await client.calls.hangup('call_abc123')
```

---

### `client.webhooks`

#### `webhooks.create(options)`

Register a webhook endpoint for real-time events.

```typescript
const webhook = await client.webhooks.create({
  url: 'https://example.com/hooks/agentcall',
  events: ['sms.inbound', 'sms.otp', 'call.status'],
})
console.log(webhook.secret) // signing secret, shown once
```

**Valid events:** `sms.inbound`, `sms.otp`, `call.inbound`, `call.ringing`, `call.status`, `call.recording`, `call.transcript`, `number.released`

Use `call.transcript` to receive the full transcript and LLM summary after a call ends. Payload includes `callId`, `duration`, `transcript` (array of `{role, text, timestamp}`), and `summary` (`{summary, callerName, intent, urgency, callbackBy, spam}`). Verify the `X-AgentCall-Signature` header before processing.

#### `webhooks.list()`

List all active webhooks.

```typescript
const hooks = await client.webhooks.list()
```

#### `webhooks.delete(webhookId)`

Deactivate a webhook.

```typescript
await client.webhooks.delete('wh_abc123')
```

---

### `client.usage`

#### `usage.get(period?)`

Get usage and cost breakdown for a billing period.

```typescript
const usage = await client.usage.get('2025-06') // YYYY-MM, defaults to current month

console.log(usage.breakdown.sms.outbound)  // number of outbound SMS
console.log(usage.total)                    // total cost
console.log(usage.currency)                 // 'usd'
```

**Returns:** `UsageData`

```typescript
{
  period: string
  breakdown: {
    numbers: { count: number; cost: number }
    sms: { inbound: number; outbound: number; cost: number }
    calls: { minutes: number; cost: number }
    recording: { minutes: number; cost: number }
  }
  total: number
  currency: string
}
```

---

### `client.account`

#### `account.get()`

Get the current plan (`free` or `pro`), plan limits, usage this period, and, on Free, the inbound AI trial state (`freeInboundAiVoice: { capSeconds, usedSeconds, remainingSeconds, resetsAt }`).

```typescript
const account = await client.account.get()
console.log(account.plan) // 'free'
```

#### `account.upgrade()`

Start a Pro upgrade for the account that owns this API key. Returns a secure Stripe Checkout URL for a human to open. Pro is $19.99/mo plus usage; subscribing takes about a minute and the action that hit the plan limit can be retried right after.

```typescript
const { url } = await client.account.upgrade()
console.log(url) // https://checkout.stripe.com/c/pay/cs_live_...
```

Throws `AgentCallError` with code `upgrade_unavailable` (400) if the account is already on Pro, or `upgrade_not_configured` (503) if checkout cannot be started on our side.

---

## Error handling

All API errors throw `AgentCallError` with a `code` and `statusCode`. Plan-gate errors (`plan_limit_*`) also carry `upgradeUrl` and `upgradeToolName` so an agent relaying the error can hand the human a working link:

```typescript
import AgentCall, { AgentCallError } from 'agentcall'

try {
  await client.calls.ai({ from: 'num_abc123', to: '+14155551234', systemPrompt: '...' })
} catch (err) {
  if (err instanceof AgentCallError) {
    console.error(err.code)            // "plan_limit_voice_ai"
    console.error(err.statusCode)      // 403
    console.error(err.upgradeUrl)      // "https://agentcall.co/billing"
    console.error(err.upgradeToolName) // "upgrade_to_pro"
    if (err.upgradeUrl) {
      const { url } = await client.account.upgrade() // checkout link to show the human
    }
  }
}
```

Common error codes:

| Code | Status | Meaning |
|------|--------|---------|
| `unauthorized` | 401 | Invalid or missing API key |
| `plan_limit_*` | 403 | Feature or quota requires Pro; body carries `upgradeUrl` + `upgradeToolName` |
| `not_found` | 404 | Resource doesn't exist |
| `validation_error` | 422 | Invalid request body |
| `rate_limited` | 429 | Too many requests (auto-retried by SDK) |

The SDK automatically retries rate-limited (429) and server error (5xx) responses up to 3 times with exponential backoff.

## License

MIT
