# Firetell Developer Documentation > Complete API reference and guides for building voice-powered applications with Firetell — VoIP, Cloud Communications, Call Center, SIP, and Voice AI. --- # Authentication The Firetell API supports two authentication methods, each designed for a different use case: | Method | Scheme | Use Case | | ------------- | --------------- | --------------------------------------------- | | **API Key** | `ApiKey sk-...` | Server-to-server calls from your backend | | **JWT Token** | `Bearer ` | Client-side calls from your web or mobile app | Both methods use the `Authorization` HTTP header. --- ## API Key Authentication (Server-side) Use API Keys to authenticate **server-to-server** requests from your backend. API keys start with the `sk-` prefix and are passed using the `ApiKey` scheme. ``` Authorization: ApiKey sk-your_api_key ``` :::danger **Do NOT use API Keys in client-side code.** API Keys grant **full access** to your workspace and must **never** be embedded in frontend applications — including JavaScript bundles, mobile apps, or any code that runs on the user's device. Attackers can easily extract keys from client-side code and use them to read, modify, or delete your workspace resources. For client-side authentication, use [JWT Tokens](#jwt-authentication-client-side) instead. ::: ### Quick Example ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/phone-numbers" \ -H "Authorization: ApiKey sk-your_api_key" ``` ### Getting Your API Key 1. Log in to the [Firetell Console](https://console.firetell.com) 2. Select your workspace 3. Navigate to **Settings** → **API Keys** 4. Click **Create API Key** 5. Enter a descriptive name 6. Copy the generated key (starts with `sk-`) :::caution Your API Key is **only shown once** at creation time. Store it securely — you will not be able to retrieve it again. If lost, delete the key and create a new one. ::: :::tip Each workspace can have up to **100 API keys**. Use descriptive titles like "Production Server", "Staging", or "CRM Integration" to organize your keys. ::: ### Secure Key Storage & Encryption To ensure maximum security for enterprise credentials, Firetell adheres to defense-in-depth principles for API key storage: - **Zero Plaintext Storage**: Raw API key secrets (`sk-...`) are **never stored in plaintext** in Firetell databases. - **SHA-256 Hashing for Authentication**: Incoming requests using the `ApiKey` scheme are authenticated by computing a SHA-256 hash of the provided secret and performing an index lookup (`sk_hash`). The raw secret is never queried directly against the database. - **AES-256-GCM Encryption at Rest**: For features requiring cryptographic signature verification (such as validating JWT tokens generated with your API key), key secrets are stored using industry-standard **AES-256-GCM** authenticated encryption with dedicated master encryption keys. - **Strict One-Time Visibility**: Because plaintext secrets are not retained, neither Firetell staff nor workspace administrators can recover a lost secret. If an API key is misplaced, you must revoke it and generate a new key. - **Instant Revocation**: When you delete or revoke an API key from the Console, both the encryption entry, hash index, and distributed authentication caches are invalidated immediately. ### Code Examples #### cURL ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/phone-numbers" \ -H "Authorization: ApiKey sk-your_api_key" \ -H "Content-Type: application/json" ``` #### Node.js ```javascript const response = await fetch( "https://{workspace_id}.firetell.app/api/v1/phone-numbers", { headers: { Authorization: "ApiKey sk-your_api_key", "Content-Type": "application/json", }, }, ); const data = await response.json(); ``` #### Python ```python import requests response = requests.get( 'https://{workspace_id}.firetell.app/api/v1/phone-numbers', headers={ 'Authorization': 'ApiKey sk-your_api_key', 'Content-Type': 'application/json', }, ) data = response.json() ``` --- ## JWT Authentication (Client-side) Use JWT tokens to authenticate **client-side** requests from your web or mobile application. Your backend signs JWTs using the API Key secret (`sk-...`) with the **HS256** algorithm. ``` Authorization: Bearer eyJhbGciOiJIUzI1NiIs... ``` ### How It Works 1. Your **backend** signs a JWT using your API Key secret (`sk-...`) with HS256 2. Your backend returns the JWT to the **client** application 3. The client includes the JWT in all API requests ``` ┌──────────┐ ┌──────────────┐ │ Client │──(1)──▶ │ Your Backend │ │ (Web / │ │ Signs JWT │ │ Mobile) │◀──(2)── │ with sk-... │ │ │ └──────────────┘ │ │ │ │──(3)──────────────────────────▶ Firetell API │ │ Authorization: (Bearer JWT) └──────────┘ ``` 1. Client requests a token from your backend 2. Your backend signs a JWT locally using the API Key secret and returns it 3. Client calls Firetell API directly with `Bearer ` ### JWT Claims | Claim | Description | Example | | -------------- | -------------------------------------------------------------- | -------------------------- | | `iss` | API Key SID | `sid-your_api_key_sid` | | `sub` | Subject identifier (see Audience Types below) | `agent_123` or `user_abc` | | `aud` | Audience type: `agent-api` or `client-api` | `agent-api` | | `exp` | Expiration timestamp | 24 hours from now | | `domain` | Workspace domain (custom claim) | `yourcompany.firetell.app` | | `call_flow_id` | Visual Call Flow ID (custom claim, **required** for `client-api`) | `cf_abc123xyz789000` | #### Audience Types The `aud` claim determines what type of identity the token represents. Firetell supports two distinct identities: | Audience | `sub` requirement | Use case | | ------------ | ------------------------------------------------------------------------ | -------------------------------- | | `agent-api` | Must be the **username of an existing, active agent** in your workspace | Call center agents, support reps | | `client-api` | Can be **any identifier** you define (e.g. user ID, phone number, email) | End-user clients, in-app calling | --- ### Agent API vs Client API Understanding the difference between these two identities is key to building the right integration. #### Agent API (`agent-api`) Agents are **managed users** created by workspace admins via [Console](https://console.firetell.com) or the [Agents API](/docs/rest-api/workspace-api/agents). They represent real people in your organization (support reps, sales agents, operators). | Feature | Details | | ---------------------- | ----------------------------------------- | | **Created by** | Workspace admin (Console or API) | | **Username** | Fixed, unique within workspace | | **Quantity** | Limited by your workspace plan | | **PSTN calls** | ✅ Outbound and inbound via phone numbers | | **Call routing** | ✅ Queues, IVR, team-based routing | | **Internal calls** | ✅ Agent ↔ Agent, Agent ↔ Client | | **Call Center Portal** | ✅ Web, iOS, Android app | | **Presence** | ✅ Online/offline/busy state | | **Teams** | ✅ Assigned to teams for routing | :::info **Agent Limits** The number of agents you can create depends on your workspace plan. To view your current limits: **Console** → **Workspace Settings** → **Limits**. Need more agents? Upgrade your plan or contact [enterprise@firetell.com](mailto:enterprise@firetell.com). ::: :::note **Device Session Limit** For agents logging in via the official portal endpoint (`/auth/login`), each agent is allowed a maximum of **5 concurrent active device sessions** (e.g., 1 browser, 1 mobile app, 1 desktop app, etc.). If an agent logs in on a 6th device, the **oldest session is automatically evicted** (logged out) to enforce limits. *Note: If your backend signs JWT tokens manually (custom integration) without providing a `device_id` claim, this 5-device limit check is bypassed.* ::: #### Client API (`client-api`) Clients are **dynamic identities** — your backend signs a JWT with any `sub` value you choose. No pre-registration needed. Clients are designed for **in-app, internal VoIP calling** between your users. | Feature | Details | | ---------------------- | ----------------------------------------------- | | **Created by** | Your backend signs JWT with any username | | **Username** | Any identifier (user ID, phone, email, session) | | **Quantity** | Unlimited tokens (limit on active connections) | | **PSTN calls** | ❌ Internal VoIP only | | **Call routing** | ❌ Direct calls only | | **Internal calls** | ✅ Client ↔ Client, Client ↔ Agent | | **Call Center Portal** | ❌ Integrates into your own app | | **Presence** | ✅ Online/offline state | | **Teams** | ❌ Not applicable | :::caution **Concurrent Connection Limit** While you can generate and sign unlimited client JWT tokens (`client-api`), there is a default workspace limit of **1,000 concurrent active WebSocket signaling connections**. If your application requires higher concurrent limits, please contact [enterprise@firetell.com](mailto:enterprise@firetell.com) to increase the limit for your workspace. ::: #### Use Case Examples | Scenario | Identity | Flow | | ---------------------- | ------------ | --------------------------------------------------------- | | **Ride-hailing** | `client-api` | Driver app ↔ Customer app (in-app VoIP) | | **E-commerce support** | Both | Customer (`client-api`) → Call center agent (`agent-api`) | | **Telemedicine** | `client-api` | Patient app ↔ Doctor app | | **Customer support** | `agent-api` | Inbound PSTN call → IVR → Agent | | **Sales outreach** | `agent-api` | Agent → Outbound PSTN call to lead | | **In-app calling** | `client-api` | User A ↔ User B within your app | ``` ┌─────────────────────────────────────────────────────────┐ │ Your Application │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ Client API │ ◄── VoIP ──► │ Client API │ │ │ │ (Driver) │ │ (Customer) │ │ │ └──────────────┘ └──────┬───────┘ │ │ │ │ │ VoIP │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ Agent API │ │ │ │ (Support) │ │ │ └──────┬───────┘ │ │ │ │ └──────────────────────────────────────────┼───────────────┘ │ PSTN │ ▼ ┌─────────────┐ │ Phone │ │ Network │ └─────────────┘ ``` ### Quick Examples #### Agent API token ```javascript const jwt = require("jsonwebtoken"); const token = jwt.sign( { domain: "yourcompany.firetell.app" }, "sk-your_api_key_secret", { algorithm: "HS256", expiresIn: "24h", issuer: "sid-your_api_key_sid", subject: "agent_123", // Must be an existing, active agent username audience: "agent-api", }, ); ``` #### Client API token ```javascript const jwt = require("jsonwebtoken"); const token = jwt.sign( { domain: "yourcompany.firetell.app", call_flow_id: "cf_your_call_flow_id", // Required for client-api to route calls to a Call Flow }, "sk-your_api_key_secret", { algorithm: "HS256", expiresIn: "24h", issuer: "sid-your_api_key_sid", subject: "user_abc", // Any custom identifier you define audience: "client-api", }, ); ``` For detailed examples in Python, PHP, and more, see the [Client Authentication](/docs/sdks/javascript/authentication) guide. :::tip JWT tokens are designed for client-side usage and expire after a short period. Always sign tokens on your backend and pass them to the client — never expose your API Key secret to client-side code. ::: --- ## Workspace Scope Both API keys and JWT tokens are scoped to a single workspace. The workspace is determined by the subdomain in the request URL: ``` https://{workspace_id}.firetell.app/api/v1/... ^^^^^^^^^^^^^^ workspace scope ``` You cannot access resources from other workspaces with a single key or token. --- ## Choosing the Right Method | | API Key (`ApiKey`) | JWT Token (`Bearer`) | | ---------------- | ------------------------------------------ | ---------------------- | | **Where to use** | Backend / server | Frontend / client | | **Lifetime** | Long-lived (until revoked) | Short-lived (expires) | | **Security** | Must be kept secret | Safe to send to client | | **Access level** | Full workspace access | Scoped by JWT claims | | **Example** | REST API integrations, webhooks, cron jobs | WebRTC, real-time apps | :::warning **Never expose API Keys in client-side code.** API keys grant full access to your workspace. Use JWT tokens for any code that runs in the browser or on mobile devices. ::: :::tip **Use separate API keys for each authentication flow.** Create one key for server-to-server API calls (`ApiKey` scheme) and a different key for signing client JWT tokens (`Bearer` scheme). This makes it easy to identify which flow is affected if a key is compromised, and you can revoke a single key without disrupting the other. ::: --- ## Rate Limiting API requests are rate-limited to protect service quality: | Tier | Requests per minute | | ---------- | ------------------- | | Default | 100 | | Auth | 10 | | Heavy | 30 | | Enterprise | Custom | When rate limited, you'll receive a `429 Too Many Requests` response with a `Retry-After` header. Implement exponential backoff in your retry logic. See [Rate Limiting](/docs/rest-api/overview#rate-limiting) for full details. --- ## Error Responses ### Missing Authorization Header ```json { "statusCode": 401, "message": "Missing Authorization header" } ``` ### Unsupported Scheme ```json { "statusCode": 401, "message": "Unsupported authorization scheme \"Basic\". Use \"Bearer\" or \"ApiKey\"" } ``` ### Invalid API Key ```json { "statusCode": 401, "message": "Invalid API Key" } ``` ### Invalid or Expired JWT ```json { "statusCode": 401, "message": "Unauthorized" } ``` --- ## Security Best Practices - **Encrypted at rest & hashed** — Firetell protects all API key secrets at rest using AES-256-GCM encryption and SHA-256 hashing. Raw secrets are never stored in plaintext and cannot be recovered after creation. - **Never expose API keys in client-side code** — Use them only on your backend server. - **Use JWT tokens for clients** — Generate short-lived tokens for browser and mobile apps. - **Rotate keys regularly** — Delete old keys and create new ones periodically. - **Use separate keys per environment** — Different keys for development, staging, and production. - **Store secrets securely on your servers** — Use environment variables or a secret manager (such as AWS Secrets Manager, HashiCorp Vault, or Google Secret Manager), never hardcode keys in source control. ## Next Steps - [Quickstart](/docs/getting-started/quickstart) — Make your first API call - [API Reference](/docs/rest-api/overview) — Explore all available endpoints --- # Introduction Welcome to the **Firetell Developer Platform**. Firetell provides a suite of cloud communications APIs that enable you to build voice-powered applications, automate call workflows, and integrate intelligent voice agents into your products. ## What is Firetell? Firetell is a **Communications Platform as a Service (CPaaS)** that offers: - **Cloud Phone Numbers** — Provision and manage virtual phone numbers worldwide - **Programmable Voice** — Make, receive, and control calls via API - **Smart IVR / Call Flows** — Build drag-and-drop interactive voice response systems - **SIP Trunking** — Connect your existing PBX infrastructure to the cloud - **Call Center** — Manage agents, teams, queues, and call routing - **Voice AI** — Deploy AI-powered voice agents for customer interactions - **Call Recording** — Record and store call audio for compliance and quality assurance - **Webhooks** — Receive real-time event notifications ## Who is this for? This documentation is designed for **developers** who want to integrate Firetell's capabilities into their applications using our REST APIs, webhooks, and SDKs. ## Base URL Each workspace has its own unique API domain. The base URL for all developer API requests follows this pattern: ``` https://{workspace_id}.firetell.app/api/v1 ``` Replace `{workspace_id}` with your workspace's subdomain. To find it: 1. Log in to [Firetell Console](https://console.firetell.com) 2. Select your workspace 3. Go to **Workspace Settings** → **General** 4. Copy the **Domain** value For example, if your domain is `yourcompany`, your base URL would be: ``` https://yourcompany.firetell.app/api/v1 ``` ## Workspaces & Regions When you create a workspace, you select a **region** that determines where your data is stored and API requests are routed. Each workspace gets a unique subdomain (`{workspace_id}.firetell.app`) that is DNS-resolved to the regional server cluster. ### How It Works ``` Create workspace "companyA" with region ASIA-SOUTH1 │ ▼ companyA.firetell.app → DNS → asia-south-1 cluster ├─ API servers ├─ Database ├─ Media servers ├─ Signaling servers └─ Storage servers ``` All API traffic, call data, recordings, and agent sessions for that workspace are processed and stored within the selected region. ### Available Regions | Region | ID | Location | | ----------------- | ---------------- | ------------- | | **ASIA-EAST1** | `asia-east-1` | Vietnam | | **ASIA-SOUTH1** | `asia-south-1` | Singapore | | **US-EAST1** | `us-east-1` | United States | | **EUROPE-NORTH1** | `europe-north-1` | France | :::warning The region **cannot be changed** after workspace creation. Choose the region closest to your users and agents for the lowest latency. ::: :::note **Need a custom domain?** Enterprise customers can use their own domain (e.g., `api.yourcompany.com`) instead of `*.firetell.app`. Contact [enterprise@firetell.com](mailto:enterprise@firetell.com) for details. ::: ## Call Center Portal Every workspace comes with a **ready-to-use Call Center Portal** that your agents can use immediately — no development required. ### Access Points | Platform | URL / App | Description | | ----------- | ------------------------------------------------------------------------- | -------------------------------- | | **Web** | `https://{workspace_id}.firetell.app` | Browser-based agent portal | | **iOS** | [App Store](https://apps.apple.com/app/firetell) | Native iOS app with VoIP push | | **Android** | [Google Play](https://play.google.com/store/apps/details?id=com.firetell) | Native Android app with FCM push | Agents log in with their username and password to make/receive calls, manage their availability, chat with team members, and more. ### Built on the Same API The Call Center Portal is built entirely on the **same public API** documented here. This means: - Everything the portal does, **your app can do too** - The portal serves as a **reference implementation** of the API - You can build your own custom agent experience using the same endpoints ### Open Source The Call Center Portal source code is available on GitHub: | Repository | Platform | Description | | ---------- | -------- | ----------- | | [firetell-call-center-web](https://github.com/firetellcom/firetell-call-center-web) | Web | Browser-based portal (Angular) | | [firetell-call-center-app](https://github.com/firetellcom/firetell-call-center-app) | iOS, Android | Mobile app (Flutter) | | — | Windows, macOS | Desktop app *(coming soon)* | You can fork and customize these apps to match your brand, add custom features, or use them as a starting point for your own agent application. ## Authentication Firetell APIs support authentication using either a server-side **API Key** or a client-side **JWT Token**. ### API Key (Server) Used for backend/server-to-server integrations. ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/phone-numbers" \ -H "Authorization: ApiKey YOUR_API_KEY" ``` ### JWT Token (Client) Used for client-side applications (such as WebRTC or mobile SDKs) to authenticate agents or clients securely. ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/phone-numbers" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` See the [Authentication](/docs/getting-started/authentication) guide and [Client Authentication](/docs/sdks/javascript/authentication) guide for more details. ## Next Steps - [Authentication](/docs/getting-started/authentication) — Learn how to authenticate API requests - [Quickstart](/docs/getting-started/quickstart) — Make your first API call - [API Reference](/docs/rest-api/overview) — Explore all available endpoints --- # Quickstart Get up and running with the Firetell API in just a few minutes. ## Prerequisites - A [Firetell Console](https://console.firetell.com) account - An API key (see [Authentication](/docs/getting-started/authentication)) - `curl` or any HTTP client ## Step 1: Find Your Workspace Domain Each workspace has its own API domain. To find yours: 1. Log in to [Firetell Console](https://console.firetell.com) 2. Select your workspace 3. Go to **Workspace Settings** → **General** 4. Note the **Domain** value (e.g., `yourcompany`) Your API base URL will be: ``` https://{workspace_id}.firetell.app/api/v1 ``` :::tip Replace `yourcompany` in all the examples below with your actual workspace domain. ::: ## Step 2: List Phone Numbers Verify your API key works by listing the phone numbers in your workspace: ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/phone-numbers" \ -H "Authorization: ApiKey YOUR_API_KEY" ``` **Response:** ```json { "data": [ { "id": "pn_abc123", "number": "+84901234567", "type": "local", "status": "active" } ], "total": 1, "page": 1, "limit": 20 } ``` ## Step 3: Make a Call Initiate an outbound call: ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/calls/make" \ -H "Authorization: ApiKey YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": { "number": "+84901234567", "name": "Customer Support" }, "to": { "number": "+84909876543", "name": "John Doe" } }' ``` ## What's Next? Now that you've made your first API call, explore the full capabilities: - [REST API Overview](/docs/rest-api/overview) — Learn about all available endpoints - [Call Flows](/docs/rest-api/workspace-api/call-flows) — Build automated call routing - [Webhooks](/docs/webhooks/overview) — Receive real-time event notifications - [Voice Agents](/docs/rest-api/workspace-api/voice-agents) — Integrate AI-powered voice bots --- # Account Self-service endpoints for agents to manage their own profile, teams, devices, and session. All endpoints require a valid **JWT token** with the `agent-api` audience (obtained via [Agent Login](/docs/rest-api/agent-api/auth#agent-login)). ## Endpoints | Method | Endpoint | Description | | ------- | ------------------------------------- | ------------------------------------- | | `GET` | `/me` | Get my profile | | `GET` | `/me/teams` | List my teams | | `GET` | `/me/teams/:id/agents` | List other agents in my team | | `PATCH` | `/me` | Update my display name | | `PATCH` | `/me/avatar` | Upload/change avatar | | `PATCH` | `/me/password` | Change my password | | `POST` | `/me/devices/voip-push-token` | Register VoIP push notification token | | `POST` | `/me/devices/notification-push-token` | Register notification push token | | `GET` | `/me/devices` | List my registered devices | | `POST` | `/me/logout` | Logout and remove device token | ## Agent Profile Object The profile object returned by `/me` endpoints: | Field | Type | Description | | -------------- | -------------- | ------------------------------------------------------------ | | `id` | string | Unique agent ID (prefixed with `ag_`) | | `username` | string | Agent login username | | `display_name` | string | Agent display name | | `email` | string \| null | Agent email address | | `avatar` | string \| null | Avatar image URL | | `state` | string | Real-time presence: `available`, `incall`, `busy`, `offline` | | `is_active` | boolean | Whether the agent account is active | | `workspace_id` | string | Workspace identifier | | `domain` | string | Agent's SIP domain | | `country_code` | string | ISO 3166-1 alpha-2 country code | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | --- ## Get My Profile ``` GET /me ``` Retrieve the current authenticated agent's profile. ### Authentication Requires `Bearer` JWT with `agent-api` audience. ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/me" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` ```json { "id": "ag_7f3a2b1c9d4e5f6a8b0c1d2e", "username": "johndoe", "display_name": "John Doe", "email": "john@example.com", "avatar": "https://storage.firetell.app/avatars/ag_7f3a2b1c.jpg", "role": "member", "state": "available", "is_active": true, "workspace_id": "my_workspace", "domain": "my_workspace.app.firetell.com", "country_code": "VN", "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-07-13T09:00:00.000Z" } ``` ### Error Responses | Status | Description | | ------ | ------------------------------------------------ | | `403` | JWT audience is not `agent-api` | | `404` | Agent profile not found or account is not active | --- ## List My Teams ``` GET /me/teams ``` Retrieve a paginated list of teams that the current agent belongs to. ### Authentication Requires `Bearer` JWT with `agent-api` audience. ### Query Parameters | Parameter | Type | Default | Description | | --------- | ------ | ------- | --------------------------------- | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `10` | Items per page (min: 1, max: 100) | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/me/teams?page=1&limit=10" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` ```json { "data": [ { "team_id": "te_a1b2c3d4e5f6g7h8", "team": { "id": "te_a1b2c3d4e5f6g7h8", "title": "Support Team", "workspace_id": "my_workspace", "agent_count": 5 } }, { "team_id": "te_x9y8z7w6v5u4t3s2", "team": { "id": "te_x9y8z7w6v5u4t3s2", "title": "Sales Team", "workspace_id": "my_workspace", "agent_count": 3 } } ], "meta": { "total": 2, "page": 1, "limit": 10, "total_pages": 1 } } ``` ### Item Fields | Field | Type | Description | | --------- | ------ | ------------------------------------- | | `team_id` | string | Team ID | | `team` | object | Team details (id, title, agent_count) | :::info Agent roles are no longer stored per-team. Each agent has a single workspace-level role (`agent`, `leader`, or `supervisor`) set by an admin. See [Roles & Permissions](/docs/rest-api/agent-api/roles-permissions) for details. ::: ### Error Responses | Status | Description | | ------ | ------------------------------------------------ | | `403` | JWT audience is not `agent-api` | | `404` | Agent profile not found or account is not active | --- ## List Team Agents ``` GET /me/teams/:id/agents ``` Retrieve a paginated list of other agents in a team that you belong to. The current agent is excluded from the results. This endpoint is designed for displaying teammate lists for messaging or calling. ### Authentication Requires `Bearer` JWT with `agent-api` audience. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------ | | `id` | string | Team ID (e.g. `te_a1b2c3d4e5f6g7h8`) | ### Query Parameters | Parameter | Type | Default | Description | | --------- | ------ | ------- | --------------------------------- | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `10` | Items per page (min: 1, max: 100) | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/me/teams/te_a1b2c3d4e5f6g7h8/agents?page=1&limit=10" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` ```json { "data": [ { "username": "jane_smith", "display_name": "Jane Smith", "avatar": "https://storage.firetell.app/avatars/ag_x9y8z7.jpg", "state": "available", "role": "leader" }, { "username": "bob_wilson", "display_name": "Bob Wilson", "avatar": null, "state": "incall", "role": "member" }, { "username": "alice_chen", "display_name": "Alice Chen", "avatar": "https://storage.firetell.app/avatars/ag_m3n4o5.jpg", "state": "offline", "role": "member" } ], "meta": { "total": 4, "page": 1, "limit": 10, "total_pages": 1 } } ``` ### Item Fields | Field | Type | Description | | -------------- | -------------- | ---------------------------------------------------------------- | | `username` | string | Agent username | | `display_name` | string | Agent display name | | `avatar` | string \| null | Avatar URL | | `state` | string | Presence state: `available`, `incall`, `busy`, `offline` | | `role` | string | Agent's workspace-level role: `agent`, `leader`, or `supervisor` | :::info The `role` field reflects the agent's **account-level role**, not a team-specific role. Roles are assigned globally per agent by workspace admins. ::: ### Error Responses | Status | Description | | ------ | ------------------------------------------------ | | `403` | You are not a member of this team | | `404` | Agent profile not found or account is not active | --- ## Update My Profile ``` PATCH /me ``` Update the current agent's display name. Only `display_name` can be modified by the agent. Other fields like `username`, `email`, `is_active`, and `country_code` can only be changed by workspace admins via the [Agents API](/docs/rest-api/workspace-api/agents). ### Authentication Requires `Bearer` JWT with `agent-api` audience. ### Request Body | Field | Type | Description | | -------------- | ------ | ------------------------------------------------------- | | `display_name` | string | Display name (1–35 characters, letters and spaces only) | ### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/me" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "display_name": "John D." }' ``` ### Response `200 OK` Returns the updated agent profile object. ### Error Responses | Status | Description | | ------ | ------------------------------------------------ | | `403` | JWT audience is not `agent-api` | | `404` | Agent profile not found or account is not active | --- ## Upload / Change Avatar ``` PATCH /me/avatar ``` Upload or replace the current agent's avatar image. The previous avatar (if any) is automatically deleted from storage. ### Authentication Requires `Bearer` JWT with `agent-api` audience. ### Request Body Send as `multipart/form-data` with a single file field: | Field | Type | Required | Description | | ------ | ---- | -------- | --------------------------------------------- | | `file` | file | ✅ | Image file (max 5MB, must be an image format) | ### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/me/avatar" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -F "file=@/path/to/avatar.jpg" ``` ### Response `200 OK` Returns the updated agent profile object with the new `avatar` URL. ### Error Responses | Status | Description | | ------ | ------------------------------------------------ | | `400` | File is missing or not an image format | | `403` | JWT audience is not `agent-api` | | `404` | Agent profile not found or account is not active | --- ## Change My Password ``` PATCH /me/password ``` Change the current agent's login password. Unlike the admin [Change Password](/docs/rest-api/workspace-api/agents#change-agent-password) endpoint, this requires the **current password** for verification. ### Authentication Requires `Bearer` JWT with `agent-api` audience. ### Request Body | Field | Type | Required | Description | | ------------------ | ------ | -------- | ------------------------------ | | `current_password` | string | ✅ | Current (old) password | | `new_password` | string | ✅ | New password (6–50 characters) | ### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/me/password" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "current_password": "oldPassword123", "new_password": "newSecurePassword456" }' ``` ### Response `200 OK` ```json { "message": "Password changed successfully" } ``` ### Error Responses | Status | Description | | ------ | ------------------------------------------------ | | `401` | Current password is incorrect | | `403` | JWT audience is not `agent-api` | | `404` | Agent profile not found or account is not active | :::caution After changing the password, the agent's existing JWT tokens remain valid until expiration. No active sessions are terminated. The new password takes effect on the next login. ::: --- ## Register VoIP Push Notification Token ``` POST /me/devices/voip-push-token ``` Register or update a **VoIP push notification token** for the current device. This enables the agent to receive **incoming call notifications** via VoIP Push (APNs VoIP / FCM) even when the app is in the background or the WebSocket connection is closed. If a device with the same `device_id` already exists, it will be updated with the new token and metadata. :::info This endpoint registers the **VoIP** push token used for incoming call pushes. To register a token for **regular notifications** (e.g., messages, alerts), use [`POST /me/devices/notification-push-token`](#register-notification-push-token) instead. The device record is created or updated (upserted by `device_id`). On **iOS**, VoIP push (APNs VoIP) and regular push (APNs standard) use **different tokens**. Both must be registered separately. On **Android/Web**, FCM handles both types — you may use the same FCM token for both endpoints. ::: ### Authentication Requires `Bearer` JWT with `agent-api` audience. ### Request Body | Field | Type | Required | Description | | -------------- | ------ | -------- | -------------------------------------------------------- | | `push_token` | string | ✅ | Push notification token (APNs VoIP token or FCM token) | | `device_id` | string | ✅ | Unique device identifier (e.g., IDFV on iOS, Android ID) | | `platform` | string | ✅ | Device platform: `ios`, `android`, or `web` | | `os_version` | string | | OS version (e.g., `"17.5.1"`, `"14"`) | | `app_version` | string | | Application version (e.g., `"1.2.0"`) | | `device_model` | string | | Device model (e.g., `"iPhone 15 Pro"`, `"Galaxy S24"`) | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/me/devices/voip-push-token" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "push_token": "abc123...xyz", "device_id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890", "platform": "ios", "os_version": "17.5.1", "app_version": "1.2.0", "device_model": "iPhone 15 Pro" }' ``` ### Response `200 OK` ```json { "id": "dev_a1b2c3d4e5f6g7h8", "workspace_id": "my_workspace", "username": "johndoe", "device_id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890", "platform": "ios", "push_provider": "apns_voip", "os_version": "17.5.1", "app_version": "1.2.0", "device_model": "iPhone 15 Pro", "voip_enabled": true, "last_active_at": "2026-07-13T10:00:00.000Z", "created_at": "2026-07-13T10:00:00.000Z", "updated_at": "2026-07-13T10:00:00.000Z" } ``` :::tip Call this endpoint every time the app launches or the push token refreshes to ensure the server always has the latest token. Stale tokens cannot receive push notifications. ::: --- ## Register Notification Push Token ``` POST /me/devices/notification-push-token ``` Register or update a **regular push notification token** for the current device. This enables the agent to receive **inbound SMS notifications (`message.received`)** and call dismissal alerts (`call.canceled`, `call.ended`) via standard alert push (APNs / FCM). The device must already be registered via [`POST /me/devices/voip-push-token`](#register-voip-push-notification-token). :::info On **iOS**, the standard APNs alert token is **different** from the VoIP APNs token. Both must be registered separately. On **Android/Web**, FCM handles both VoIP and regular push — the same token can be used. ::: ### Authentication Requires `Bearer` JWT with `agent-api` audience. ### Request Body | Field | Type | Required | Description | | ---------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `notification_push_token` | string | ✅ | Regular push notification token (raw APNs 64-hex token or FCM registration token) | | `device_id` | string | ✅ | Unique device identifier (same as used in `/me/devices`) | | `platform` | string | ✅ | Device platform: `ios`, `android`, or `web` | | `notification_push_provider` | string | ❌ | Push provider: `apns` or `fcm`. If omitted, auto-detected: raw 64-hex string on iOS defaults to `apns`, while Firebase Messaging tokens (proxied on iOS) auto-detect to `fcm` | | `os_version` | string | ❌ | Operating system version (e.g., `17.5.1`, `14`) | | `app_version` | string | ❌ | Client application version (e.g., `1.2.0`) | | `device_model` | string | ❌ | Hardware model (e.g., `iPhone 15 Pro`, `Pixel 8`) | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/me/devices/notification-push-token" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "notification_push_token": "def456...uvw", "device_id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890", "platform": "ios", "notification_push_provider": "fcm" }' ``` ### Response `200 OK` ```json { "id": "dev_a1b2c3d4e5f6g7h8", "workspace_id": "my_workspace", "username": "johndoe", "device_id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890", "platform": "ios", "push_provider": "apns_voip", "os_version": "17.5.1", "app_version": "1.2.0", "device_model": "iPhone 15 Pro", "voip_enabled": true, "notification_push_provider": "fcm", "last_active_at": "2026-07-13T10:05:00.000Z", "created_at": "2026-07-13T10:00:00.000Z", "updated_at": "2026-07-13T10:05:00.000Z" } ``` ### Error Responses | Status | Description | | ------ | ---------------------------------------------------------------------------------------- | | `403` | JWT audience is not `agent-api` | | `404` | Device not found — register VoIP push token first via `POST /me/devices/voip-push-token` | :::tip Call this endpoint after registering the VoIP push token, or whenever the notification push token refreshes. On iOS, register both tokens on app launch: 1. `POST /me/devices/voip-push-token` — with the VoIP APNs token 2. `POST /me/devices/notification-push-token` — with the standard APNs token ::: --- ## List My Devices ``` GET /me/devices ``` List all devices with registered push notification tokens for the current agent. ### Authentication Requires `Bearer` JWT with `agent-api` audience. ### Response `200 OK` ```json [ { "id": "dev_a1b2c3d4e5f6g7h8", "workspace_id": "my_workspace", "username": "johndoe", "device_id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890", "platform": "ios", "push_provider": "apns_voip", "os_version": "17.5.1", "app_version": "1.2.0", "device_model": "iPhone 15 Pro", "voip_enabled": true, "last_active_at": "2026-07-13T10:00:00.000Z" }, { "id": "dev_x9y8z7w6v5u4t3s2", "workspace_id": "my_workspace", "username": "johndoe", "device_id": "android-device-id-123", "platform": "android", "push_provider": "fcm", "os_version": "14", "app_version": "1.2.0", "device_model": "Samsung Galaxy S24", "voip_enabled": true, "last_active_at": "2026-07-12T08:00:00.000Z" } ] ``` --- ## Logout ``` POST /me/logout ``` Logout from a specific device. This removes the push notification token for the specified device, preventing it from receiving further call notifications. :::caution **Client applications MUST call this endpoint when the agent logs out.** If the agent simply closes the app without calling logout, the server will continue attempting to send push notifications to the stale token, which wastes resources and may cause the push provider (APNs/FCM) to throttle or revoke the token. Inactive device registrations are automatically cleaned up after **30 days** of inactivity as a safety net, but explicit logout is always preferred. ::: ### Authentication Requires `Bearer` JWT with `agent-api` audience. ### Request Body | Field | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------------- | | `device_id` | string | ✅ | The device identifier used during push token registration | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/me/logout" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "device_id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890" }' ``` ### Response `200 OK` ```json { "message": "Logged out successfully" } ``` :::info This endpoint is **idempotent** — calling it multiple times with the same `device_id` always returns success, even if the device was already unregistered. ::: ### What happens on logout 1. The push notification token for the specified device is deleted 2. If the agent has no more devices with push tokens **and** no active WebSocket connections, their presence state transitions to `offline` 3. The JWT token itself remains valid until expiration — logout only removes the push notification registration --- # Address Books Organize your [Contacts](/docs/rest-api/agent-api/contacts) into named address books for better management. Address books act as folders — **every contact must belong to an address book**. When a new workspace is created, a system address book named **`Default`** is automatically initialized. If a contact is created without specifying an `address_book_id`, it is automatically assigned to the `Default` address book. ## Endpoints | Method | Endpoint | Description | | -------- | ------------------------------------ | ------------------------- | | `GET` | `/call-center/address-books` | List all address books | | `GET` | `/call-center/address-books/:id` | Get address book details | | `POST` | `/call-center/address-books` | Create a new address book | | `PUT` | `/call-center/address-books/:id` | Update an address book | | `DELETE` | `/call-center/address-books/:id` | Delete an address book | ## Authentication All endpoints require `Bearer` JWT with `agent-api` audience. ## Address Book Object | Field | Type | Description | | -------------- | ------- | ---------------------------------------------------------------------- | | `id` | string | Unique address book ID (prefixed `ab_`) | | `workspace_id` | string | Workspace identifier | | `title` | string | Address book name | | `description` | string | Optional description | | `is_default` | boolean | Whether this is the system default address book (cannot be deleted/modified) | | `owner_id` | string | ID of the owning agent or team (null for `everyone`) | | `owner_type` | string | Ownership scope: `agent`, `team`, or `everyone` | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | --- ## List All Address Books ``` GET /call-center/address-books ``` Retrieve a paginated list of all address books accessible to the current agent. ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------- | --------------------------------- | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `20` | Items per page (min: 1, max: 100) | | `search` | string | — | Search by title (text search) | | `sort_field` | string | — | Sort by: `created_at`, `title` | | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/address-books?page=1&limit=10" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` ```json { "data": [ { "id": "ab_x1y2z3w4v5u6t7s8", "workspace_id": "my_workspace", "title": "VIP Customers", "description": "High-value customers with active contracts", "created_at": "2026-01-10T10:00:00.000Z", "updated_at": "2026-07-01T12:00:00.000Z" }, { "id": "ab_a9b8c7d6e5f4g3h2", "workspace_id": "my_workspace", "title": "Leads Q3 2026", "description": null, "created_at": "2026-07-01T08:00:00.000Z", "updated_at": "2026-07-01T08:00:00.000Z" } ], "meta": { "total": 2, "page": 1, "limit": 10, "total_pages": 1 } } ``` --- ## Get Address Book Details ``` GET /call-center/address-books/:id ``` Retrieve detailed information about a specific address book. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------------------- | | `id` | string | Address book ID (e.g., `ab_x1y2...`) | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/address-books/ab_x1y2z3w4v5u6t7s8" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` Returns the full address book object. ### Error Responses | Status | Description | | ------ | ----------------------- | | `404` | Address book not found | --- ## Create an Address Book ``` POST /call-center/address-books ``` Create a new address book in the workspace. ### Request Body | Field | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------------------------- | | `title` | string | ✅ | Address book name (1–100 characters) | | `description` | string | | Optional description (max 500 chars) | | `owner_type` | string | | Ownership scope: `agent`, `team`, or `everyone` (defaults to `agent` in Agent API) | | `owner_id` | string | | ID of the owning team (required if `owner_type` is `team`). Automatically assigned to your agent ID if `owner_type` is `agent`. | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/call-center/address-books" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "title": "Enterprise Clients", "description": "Contacts from enterprise accounts with dedicated support", "owner_type": "team", "owner_id": "tm_sales123" }' ``` ### Response `201 Created` ```json { "id": "ab_n3w1d2e3f4g5h6i7", "workspace_id": "my_workspace", "title": "Enterprise Clients", "description": "Contacts from enterprise accounts with dedicated support", "is_default": false, "owner_type": "team", "owner_id": "tm_sales123", "created_at": "2026-07-21T08:00:00.000Z", "updated_at": "2026-07-21T08:00:00.000Z" } ``` ### Error Responses | Status | Description | | ------ | --------------------------------------------------------------------------- | | `400` | Validation error | | `403` | Forbidden — Only leaders can create address books for a team or `everyone` | --- ## Update an Address Book ``` PUT /call-center/address-books/:id ``` Update an existing address book's title, description, or ownership settings. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ---------------- | | `id` | string | Address book ID | ### Request Body | Field | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------------------------- | | `title` | string | | Updated name (1–100 characters) | | `description` | string | | Updated description (max 500 chars) | | `owner_type` | string | | Ownership scope: `agent`, `team`, or `everyone` | | `owner_id` | string | | ID of the owning agent or team | ### Request ```bash curl -X PUT "https://{workspace_id}.firetell.app/api/v1/call-center/address-books/ab_x1y2z3w4v5u6t7s8" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "title": "VIP Customers 2026", "description": "Updated address book for active VIP accounts in 2026", "owner_type": "team", "owner_id": "tm_support456" }' ``` ### Response `200 OK` Returns the updated address book object. ### Error Responses | Status | Description | | ------ | ------------------------------------------------------------------------------------------------------- | | `400` | Validation error or attempting to modify `Default` address book | | `403` | Forbidden — Only leaders can modify team/shared address books, or attempting to modify another agent's book | | `404` | Address book not found | --- ## Delete an Address Book ``` DELETE /call-center/address-books/:id ``` Permanently delete an address book. Contacts that belong to this book are **not deleted** — they are automatically reassigned to the system **`Default`** address book instead. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ---------------- | | `id` | string | Address book ID | ### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/call-center/address-books/ab_x1y2z3w4v5u6t7s8" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` Returns the deleted address book object. ### Error Responses | Status | Description | | ------ | ------------------------------------------------------------------------------------------------------- | | `400` | Cannot delete system `Default` address book | | `403` | Forbidden — Only leaders can delete team/shared address books, or attempting to delete another agent's book | | `404` | Address book not found | :::info When a custom address book is deleted, all contacts that belonged to it will be automatically reassigned to the system **`Default`** address book. The contacts themselves are preserved. ::: --- # Authentication Authentication endpoints for agents to login, verify tokens, refresh sessions, and recover forgotten passwords. All endpoints in this section are **public** — no authentication required (except `/auth/verify` which validates a token). ## Endpoints | Method | Endpoint | Description | | ------ | ----------------------- | --------------------------------------- | | `POST` | `/auth/login` | Authenticate agent and get JWT | | `POST` | `/auth/verify` | Verify agent JWT token | | `POST` | `/auth/refresh` | Refresh access token | | `POST` | `/auth/check-username` | Check username and get masked email | | `POST` | `/auth/forgot-password` | Request a password reset email | | `POST` | `/auth/reset-password` | Reset password using a token from email | --- ## Agent Login ``` POST /auth/login ``` Authenticates an agent using their username and password. On successful authentication, returns an access token (JWT), refresh token, and basic agent info. ### Request Body | Field | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------------- | | `username` | string | ✅ | Agent username (3–30 characters) | | `password` | string | ✅ | Agent password (6–50 characters) | | `device_id` | string | ✅ | Unique device identifier (IDFV on iOS, Android ID, or UUID for web) | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/auth/login" \ -H "Content-Type: application/json" \ -d '{ "username": "agent_john", "password": "secretpassword", "device_id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890" }' ``` ### Response `200 OK` ```json { "tokens": { "access_token": "eyJhbGciOiJIUzI1NiIsIn...", "expires_at": 1783935600000, "refresh_token": "eyJhbGciOiJIUzI1NiIsIn..." }, "info": { "username": "agent_john", "domain": "yourcompany.firetell.app", "display_name": "John Doe" } } ``` ### Error Responses | Status | Description | | ------ | ------------------------------------------------ | | `400` | `workspace_id not found` or validation error | | `401` | Invalid credentials or agent account is inactive | --- ## Verify Agent JWT ``` POST /auth/verify ``` Verifies a previously generated JWT token and returns information about the token's subject (the authenticated agent or client). ### Request Body | Field | Type | Required | Description | | ------- | ------ | -------- | ----------------------- | | `token` | string | ✅ | The JWT token to verify | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/auth/verify" \ -H "Content-Type: application/json" \ -d '{ "token": "eyJhbGciOiJIUzI1NiIsIn..." }' ``` ### Response `200 OK` ```json { "username": "agent_john", "domain": "yourcompany.firetell.app", "workspace_id": "ws_123456", "display_name": "John Doe", "avatar": "https://storage.firetell.app/avatars/ag_123.jpg", "expires_at": 1783935600000 } ``` ### Error Responses | Status | Description | | ------ | ---------------------------------------- | | `401` | Missing token or invalid token signature | --- ## Refresh Token ``` POST /auth/refresh ``` Exchange a valid refresh token for a new access token and refresh token pair. This implements **token rotation** — each refresh token can only be used once, and a new pair is issued on every refresh. ### Token Lifecycle | Token | Lifetime | Purpose | | --------------- | -------- | -------------------------------------------- | | `access_token` | 1 day | Used for API access (Bearer header) | | `refresh_token` | 30 days | Used only at this endpoint to get new tokens | :::info Refresh tokens **cannot** be used as Bearer tokens for API access. The server will reject them with `401 Unauthorized`. ::: ### Request Body | Field | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------ | | `refresh_token` | string | ✅ | The refresh token from login/refresh | | `device_id` | string | ✅ | Same device ID used during login | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/auth/refresh" \ -H "Content-Type: application/json" \ -d '{ "refresh_token": "eyJhbGciOiJIUzI1NiIsIn...", "device_id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890" }' ``` ### Response `200 OK` ```json { "access_token": "eyJhbGciOiJIUzI1NiIsIn...", "refresh_token": "eyJhbGciOiJIUzI1NiIsIn...", "expires_at": 1784022000000 } ``` :::warning After refreshing, the **previous refresh token is no longer valid**. Always store and use the new `refresh_token` from the response. Reusing an old refresh token will be rejected as a potential token theft. ::: ### Error Responses | Status | Description | | ------ | ---------------------------------------------- | | `401` | Invalid, expired, or wrong type of token | | `401` | Refresh token has been revoked or already used | | `401` | Agent not found or inactive | | `400` | Missing workspace_id | --- ## Check Username ``` POST /auth/check-username ``` Check whether an agent username exists in the workspace and return the agent's **masked email address**. This helps agents who remember their username but have forgotten which email address is associated with their account. :::info This endpoint is **public** — no authentication required. ::: ### Request Body | Field | Type | Required | Description | | ---------- | ------ | -------- | -------------------------------- | | `username` | string | ✅ | Agent username (3–30 characters) | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/auth/check-username" \ -H "Content-Type: application/json" \ -d '{ "username": "agent_john" }' ``` ### Response `200 OK` ```json { "email": "ag*********@gmail.com" } ``` The email address is masked to protect privacy — only the first two characters of the local part are visible, and the domain remains intact. | Original email | Masked email | | --------------------------- | ----------------------- | | `your.agentemail@gmail.com` | `yo*********@gmail.com` | | `ab@company.com` | `a*@company.com` | | `a@example.com` | `*@example.com` | ### Error Responses | Status | Description | | ------ | ---------------------------------------------- | | `400` | `workspace_id not found` or validation error | | `404` | Agent not found or no email address configured | --- ## Forgot Password ``` POST /auth/forgot-password ``` Request a password reset email. If the `username` and `email` combination matches an active agent in the workspace, a reset email is sent with a link that expires in **30 minutes**. :::info This endpoint is **public** — no authentication required. ::: :::tip Security This endpoint always returns the same success message regardless of whether the agent exists or the email matches. This prevents attackers from enumerating valid accounts. ::: ### Request Body | Field | Type | Required | Description | | ---------- | ------ | -------- | -------------------------------- | | `username` | string | ✅ | Agent username (3–30 characters) | | `email` | string | ✅ | Agent's email address | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/auth/forgot-password" \ -H "Content-Type: application/json" \ -d '{ "username": "agent_john", "email": "john@company.com" }' ``` ### Response `200 OK` ```json { "message": "If the account exists, a reset email has been sent." } ``` ### Reset Email If the credentials match, the agent receives an email from **Firetell • Workspace Name** with: - Workspace name and portal URL - A "Reset Password" button linking to `https://{workspace_id}.firetell.app/auth/reset-password?token={JWT}` - The link expires in **30 minutes** ### Error Responses | Status | Description | | ------ | -------------------------------------------- | | `400` | `workspace_id not found` or validation error | --- ## Reset Password ``` POST /auth/reset-password ``` Reset an agent's password using the token received via the password reset email. The token is a JWT signed by the server with a 30-minute expiry. :::info This endpoint is **public** — no authentication required. The reset token itself serves as proof of identity. ::: ### Request Body | Field | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------- | | `token` | string | ✅ | Reset token from the email link | | `new_password` | string | ✅ | New password (6–50 characters) | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/auth/reset-password" \ -H "Content-Type: application/json" \ -d '{ "token": "eyJhbGciOiJIUzI1NiIsIn...", "new_password": "myNewSecurePassword123" }' ``` ### Response `200 OK` ```json { "message": "Password has been reset successfully." } ``` ### Error Responses | Status | Description | | ------ | ---------------------------------------------- | | `400` | Invalid or expired reset token | | `400` | Agent not found or has been deleted | | `500` | Password reset is not configured on the server | --- ## Password Reset Flow The complete password reset flow involves three steps: ``` 1. Agent calls POST /auth/check-username → Receives masked email (e.g. "ag*****@gmail.com") → Now knows which email to use 2. Agent calls POST /auth/forgot-password with username + email → Server sends reset email to the agent's address → Agent clicks "Reset Password" link in email 3. Client extracts token from URL query parameter → Calls POST /auth/reset-password with token + new password → Password is updated, agent can log in with new credentials ``` :::warning Reset tokens are **single-purpose** JWT tokens — they can only be used to reset a password and cannot be used as access tokens for other API calls. ::: --- # Call History View the history of calls you have handled as an agent. These endpoints are scoped — you can only see calls where you were the assigned agent. :::info Data Scoping Only calls where `agent_id` matches the authenticated agent are returned. Workspace-wide call history and call events/logs are available to admins via the Workspace API. ::: ## Endpoints | Method | Endpoint | Description | | ------ | ------------------------------- | ---------------- | | `GET` | `/call-center/call-history` | List my calls | | `GET` | `/call-center/call-history/:id` | Get call details | ## Authentication All endpoints require `Bearer` JWT with `agent-api` audience. --- ## List My Calls ``` GET /call-center/call-history ``` Retrieve a paginated list of calls handled by the authenticated agent, sorted by most recent first. ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------- | ------------------------------------------------------------------------------------------- | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `20` | Items per page (max: 100) | | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | | `direction` | string | `all` | Filter by call direction: `inbound`, `outbound`, `internal`, `all` | | `status` | string | `all` | Filter by call status: `answered`, `completed`, `failed`, `no_answer`, etc. | | `sentiment` | string | `all` | Filter by AI sentiment analysis: `positive`, `neutral`, `negative`, `all` | | `search` | string | — | Search by phone number or party name | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/call-history?page=1&limit=10&sentiment=positive" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` ```json { "data": [ { "id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "workspace_id": "my_workspace", "direction": "inbound", "status": "completed", "type": "audio", "number": "84901234567", "client_number": "+84912345678", "from": { "name": null, "number": "+84912345678" }, "to": { "name": "Sales Line", "number": "84901234567" }, "agent_id": "ag_abc123", "source": "call_flow", "duration": 245, "billsec": 240, "call_flow_id": "cf_xyz789", "hangup_cause": "normal_clearing", "recording": "ready", "transcription": "completed", "sentiment": "positive", "summary": "Customer reached out to inquire about smart call center solutions.", "action_items": ["Send Enterprise plan quotation", "Schedule system demo"], "created_at": "2026-07-21T07:30:00.000Z", "answered_at": "2026-07-21T07:30:05.000Z", "ended_at": "2026-07-21T07:34:10.000Z" }, { "id": "cl_n3w1d2e3f4g5h6i7j8k9l0", "workspace_id": "my_workspace", "direction": "outbound", "status": "completed", "type": "audio", "number": "84901234567", "client_number": "+84987654321", "from": { "name": "Agent John", "number": "84901234567" }, "to": { "name": null, "number": "+84987654321" }, "agent_id": "ag_abc123", "source": "agent_app", "duration": 120, "hangup_cause": "normal_clearing", "created_at": "2026-07-21T06:15:00.000Z", "answered_at": "2026-07-21T06:15:08.000Z", "ended_at": "2026-07-21T06:17:08.000Z" } ], "meta": { "total": 42, "page": 1, "limit": 10, "total_pages": 5 } } ``` ### Call Object | Field | Type | Description | | --------------- | ------------ | --------------------------------------------------------------- | | `id` | string | Call ID | | `workspace_id` | string | Workspace identifier | | `direction` | string | `inbound`, `outbound`, or `internal` | | `status` | string | `created`, `started`, `active`, `completed`, `failed`, `missed` | | `type` | string | Call type: `audio` | | `number` | string | Workspace phone number used | | `client_number` | string | External party phone number | | `from` | object | Caller info (`name`, `number`) | | `to` | object | Callee info (`name`, `number`) | | `agent_id` | string | Handling agent ID | | `source` | string | Call origin: `client-api`, `agent-api`, `server-api` | | `duration` | number | Call duration in seconds | | `call_flow_id` | string\|null | Call flow ID if routed via a flow | | `hangup_cause` | string\|null | SIP hangup cause (e.g., `normal_clearing`) | | `created_at` | string | ISO 8601 creation timestamp | | `answered_at` | string\|null | ISO 8601 when call was answered | | `ended_at` | string\|null | ISO 8601 when call ended | --- ## Get Call Details ``` GET /call-center/call-history/:id ``` Retrieve detailed information about a specific call you handled. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------ | | `id` | string | Call ID (e.g., `cl_a1b2c3...`) | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/call-history/cl_a1b2c3d4e5f6g7h8i9j0k1" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` Returns the full call object. ### Error Responses | Status | Description | | ------ | ---------------------------------------- | | `404` | Call not found or you do not have access | --- # Call Supervision Real-time call supervision for supervisors. These endpoints allow supervisors to monitor, coach, and join active calls handled by agents in teams they supervise. :::caution **Supervisor Only** All endpoints on this page require the `supervisor` role in a team that the call's agent belongs to. See [Roles & Permissions](/docs/rest-api/agent-api/roles-permissions) for details. ::: ## Endpoints | Method | Endpoint | Description | | -------- | ----------------------------------------- | ------------------ | | `POST` | `/call-center/calls/:call_id/listen` | Silent monitor | | `POST` | `/call-center/calls/:call_id/whisper` | Coach agent | | `POST` | `/call-center/calls/:call_id/barge` | Join as 3-way call | | `DELETE` | `/call-center/calls/:call_id/supervision` | Stop supervision | ## Authentication All endpoints require `Bearer` JWT with `agent-api` audience and `supervisor` role. ## Supervision Modes | Mode | Supervisor hears | Agent hears supervisor | Caller hears supervisor | | ----------- | :--------------: | :--------------------: | :---------------------: | | **Listen** | ✅ | ❌ | ❌ | | **Whisper** | ✅ | ✅ | ❌ | | **Barge** | ✅ | ✅ | ✅ | --- ## Call Session Token (`call_token`) Every successful supervision call (`listen`, `whisper`, `barge`) returns a short-lived **`call_token`** (JWT valid for 15 minutes). This `call_token` is used by the Client SDK (or WebRTC app) to connect directly to the Signaling Server / WebRTC media stream with zero-latency authentication. ### `call_token` JWT Payload Structure ```json { "sub": "supervisor.jane", "aud": "call-session", "workspace_id": "ws_123456789", "call_id": "cl_a1b2c3d4e5f6", "mode": "listen", "fs_uuid": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6", "exp": 1753114200 } ``` :::tip **Unified Call Token Architecture** The `call_token` architecture is unified across all call interactions: - **Participant / Inbound Push**: Included in VoIP Push payload data so mobile apps can connect immediately to WebSocket without extra HTTP roundtrips. - **Supervision**: Returned by supervision APIs to authorize WebRTC audio streams (`listen`, `whisper`, `barge`). ::: --- ## Silent Listen ``` POST /call-center/calls/:call_id/listen ``` Start silently monitoring an active call. Neither the agent nor the caller will know you are listening. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | --------------------- | | `call_id` | string | ID of the active call | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/call-center/calls/cl_a1b2c3d4e5f6/listen" \ -H "Authorization: Bearer YOUR_SUPERVISOR_JWT" ``` ### Response `200 OK` ```json { "call_id": "cl_a1b2c3d4e5f6", "mode": "listen", "supervisor": "supervisor.jane", "status": "supervision_started", "call_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_in": 900 } ``` --- ## Whisper (Coach) ``` POST /call-center/calls/:call_id/whisper ``` Start whispering to the agent. The agent can hear you, but the caller cannot. Useful for real-time coaching. ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/call-center/calls/cl_a1b2c3d4e5f6/whisper" \ -H "Authorization: Bearer YOUR_SUPERVISOR_JWT" ``` ### Response `200 OK` ```json { "call_id": "cl_a1b2c3d4e5f6", "mode": "whisper", "supervisor": "supervisor.jane", "status": "supervision_started", "call_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_in": 900 } ``` --- ## Barge-in (3-Way Call) ``` POST /call-center/calls/:call_id/barge ``` Join the call as a third participant. Both the agent and the caller can hear you. ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/call-center/calls/cl_a1b2c3d4e5f6/barge" \ -H "Authorization: Bearer YOUR_SUPERVISOR_JWT" ``` ### Response `200 OK` ```json { "call_id": "cl_a1b2c3d4e5f6", "mode": "barge", "supervisor": "supervisor.jane", "status": "supervision_started", "call_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_in": 900 } ``` --- ## Stop Supervision ``` DELETE /call-center/calls/:call_id/supervision ``` Stop any active supervision session on a call. ### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/call-center/calls/cl_a1b2c3d4e5f6/supervision" \ -H "Authorization: Bearer YOUR_SUPERVISOR_JWT" ``` ### Response `200 OK` ```json { "call_id": "cl_a1b2c3d4e5f6", "supervisor": "supervisor.jane", "status": "supervision_stopped" } ``` --- ## Call Transfer Any active agent can transfer an active call to another agent or department. ``` POST /call-center/calls/:call_id/transfer ``` ### Request Body | Field | Type | Required | Description | | ----------------- | ------ | -------- | -------------------------------------------------------------- | | `target_username` | string | ✅ | Username of the target agent to receive the transfer | | `team_id` | string | ❌ | Optional target team ID (auto-detected if omitted) | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/call-center/calls/cl_a1b2c3d4e5f6/transfer" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "target_username": "agent.bob" }' ``` ### Response `200 OK` ```json { "call_id": "cl_a1b2c3d4e5f6", "status": "transfer_initiated", "from_agent_id": "ag_original123", "to_agent": { "id": "ag_target456", "username": "agent.bob" }, "transferred_by": "agent.jane" } ``` ### Error Responses | Status | Description | | ------ | -------------------------------------------------------------------- | | `400` | Target agent not found, not in specified team, or not available | | `404` | Call not found or not active | :::tip The target agent must have `available` presence state to receive a transfer. Check agent states first using `GET /call-center/teams/:team_id/agents/states`. ::: ## Error Responses | Status | Description | | ------ | ----------------------------------------------------------- | | `403` | Not a supervisor, or call agent is not in a supervised team | | `404` | Call not found or not active | --- # Outbound Calls & WebRTC Signaling Outbound calls are initiated via HTTP REST API (`POST /call-center/calls`). The API validates permissions and returns a short-lived **`call_token`** and `ws_url`. The client then opens a native WebSocket connection for WebRTC SDP signaling scoped exclusively to that call. --- ## Make Outbound Call ``` POST /call-center/calls ``` ### Request Body | Field | Type | Required | Description | | ------ | ------ | -------- | --------------------------------------------------------- | | `to` | string | ✅ | Recipient phone number or extension (e.g. `+84901234567`) | | `from` | string | ❌ | Outbound Caller ID phone number (e.g. `+842471000000`) | | `type` | string | ❌ | Call type: `audio` (default) or `video` | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/call-center/calls" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "to": "+84901234567", "from": "+842471000000" }' ``` ### Response `201 Created` ```json { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "status": "created", "call_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "ws_url": "wss://{workspace_id}.firetell.app/ws", "expires_in": 900 } ``` --- ## Native WebSocket Signaling Protocol Once `call_token` is received, open a native WebSocket to `ws_url`. :::important **3-Second Authentication Requirement** Upon opening the WebSocket connection, the client **must send the `session.connect` event within 3 seconds**, containing the `call_token`. Unauthenticated sockets are automatically terminated after 3 seconds. ::: ### 1. Handshake (`session.connect`) **Client Send:** ```json { "event": "session.connect", "data": { "token": "YOUR_CALL_TOKEN" } } ``` **Server Response:** ```json { "event": "session.connected", "data": { "session_id": "socket_uuid_123", "workspace_id": "ws_123456789", "username": "agent.john", "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "mode": "participant" } } ``` --- ### 2. WebRTC SDP Offer (`call.offer`) **Server Dispatch for Incoming Call:** ```json { "event": "call.offer", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "from": "+84901234567", "from_name": "Nguyen Van A (VIP)", "to": "+842471000000", "sdp": { "type": "offer", "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..." }, "is_transfer": false } } ``` **Client Send for Outbound Call:** ```json { "event": "call.offer", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "sdp": { "type": "offer", "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..." } } } ``` **Server Response:** ```json { "event": "call.offered", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1" } } ``` --- ### 3. ICE Candidates (`call.candidate`) **Client Send:** ```json { "event": "call.candidate", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "candidate": { "candidate": "candidate:1 1 UDP 2013266431 192.168.1.1 54321 typ host", "sdpMid": "0", "sdpMLineIndex": 0 } } } ``` :::info **ICE Candidate Signaling Note** Firetell Voice Platform uses **Full SDP Exchange (Non-Trickle ICE)** where ICE candidates (`a=candidate:...`) are bundled directly within the initial SDP offer (`call.offer`) and SDP answer (`call.answer`). Sending individual `call.candidate` events via WebSocket is supported for compatibility (acknowledged with `call.candidate_ack`), but bundling candidates directly in the SDP is the recommended and primary mechanism for media establishment. ::: --- ### 4. End Call (`call.hangup` / `call.ended`) **Client Send (`call.hangup`):** To hang up the call: ```json { "event": "call.hangup", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1" } } ``` **Server Dispatch (`call.ended`):** Notifies the client that the call session has been terminated. ```json { "event": "call.ended", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "reason": "hangup" } } ``` --- ### 5. Answer Call (`call.answer` / `call.answered`) **Client Send (`call.answer`):** Sent by the client to accept an incoming call, containing the client's WebRTC SDP answer. ```json { "event": "call.answer", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..." } } ``` **Server Dispatch (`call.answered`):** Notifies the client (e.g. for an outbound call) that the call has been answered. ```json { "event": "call.answered", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..." } } ``` --- ### 6. Reject Call (`call.reject` / `call.rejected`) **Client Send (`call.reject`):** Sent by the client to reject an incoming call. ```json { "event": "call.reject", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1" } } ``` **Server Dispatch (`call.rejected`):** Notifies the client that the call was rejected. ```json { "event": "call.rejected", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "reason": "rejected" } } ``` --- ### 7. Hold Call (`call.hold` / `call.held`) **Client Send (`call.hold`):** Sent by the client to place the active call on hold. ```json { "event": "call.hold", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "sdp": { "type": "offer", "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..." } } } ``` **Server Dispatch (`call.held`):** Notifies the client that the call was successfully held. ```json { "event": "call.held", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "sdp": { "type": "answer", "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..." } } } ``` --- ### 8. Resume Call (`call.unhold` / `call.unheld`) **Client Send (`call.unhold`):** Sent by the client to take the call off hold. ```json { "event": "call.unhold", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "sdp": { "type": "offer", "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..." } } } ``` **Server Dispatch (`call.unheld`):** Notifies the client that the call was successfully resumed. ```json { "event": "call.unheld", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "sdp": { "type": "answer", "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1..." } } } ``` --- ### 9. Mute Call (`call.mute`) **Client Send:** Sent by the client to update the local microphone mute state. ```json { "event": "call.mute", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "muted": true } } ``` --- ### 10. Send DTMF (`call.dtmf`) **Client Send:** Sent by the client to play DTMF tones on the call. ```json { "event": "call.dtmf", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "digit": "5", "duration": 250 } } ``` --- ### 11. Transfer Call (`call.transfer` / `call.transferred`) **Client Send (`call.transfer`):** Sent by the client to transfer the call to another agent. ```json { "event": "call.transfer", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "to": "agent.smith", "team_id": "te_123456" } } ``` **Server Dispatch (`call.transferred`):** Notifies the client that the call transfer is complete. ```json { "event": "call.transferred", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "reason": "transferred" } } ``` --- ## Client-Initiated Call Flows (client-api) When integrating calling capabilities into end-user client applications (e.g., driver apps, customer support widgets) using **`client-api`** JWT tokens, calls are routed through a Visual Call Flow builder rather than being connected directly as normal operator/agent calls. ### Workflow & Routing 1. **JWT Verification:** The client-api JWT token **must** contain a valid `call_flow_id` claim in its payload. 2. **Call Initiation:** The client makes a `POST /call-center/calls` request. The API validates that the `call_flow_id` exists in the workspace and has `trigger_type` set to `'outbound'`. 3. **Internal VoIP Route:** Direct PSTN dialing is disabled for the `client-api` audience. The call behaves as an internal WebRTC VoIP call. 4. **Flow Execution:** When the client connects via WebSocket and initiates the call, Firetell automatically routes the call directly into the compiled Call Flow actions (e.g., executing an IVR, connecting the caller to a team, or routing to a specific agent). --- :::tip **Auto-Hangup on Disconnect** Closing or dropping the WebSocket connection automatically ends active calls associated with that `call_id`. ::: --- # Contacts Manage contacts within your workspace. Contacts store customer or lead information including phone numbers, emails, and custom metadata. They can be organized into [Address Books](/docs/rest-api/agent-api/address-books). :::info **Data Scoping** Agents can only see contacts within [Address Books](/docs/rest-api/agent-api/address-books) they have access to (books owned by them `owner_type: agent`, books owned by teams they belong to `owner_type: team`, or shared books `owner_type: everyone`). ::: ## Endpoints | Method | Endpoint | Description | | -------- | ----------------------------------- | ------------------------------- | | `GET` | `/call-center/contacts` | List all contacts | | `GET` | `/call-center/contacts/lookup` | Look up contact by phone number | | `GET` | `/call-center/contacts/:id` | Get contact details | | `POST` | `/call-center/contacts` | Create a new contact | | `POST` | `/call-center/contacts/assign-list` | Batch assign contacts to a list | | `PUT` | `/call-center/contacts/:id` | Update a contact | | `DELETE` | `/call-center/contacts/:id` | Delete a contact | ## Authentication All endpoints require `Bearer` JWT with `agent-api` audience. ## Contact Object | Field | Type | Description | | ------------------- | -------- | ----------------------------------------------------------------------- | | `id` | string | Unique contact ID (prefixed with `ct_`) | | `workspace_id` | string | Workspace identifier | | `address_book_id` | string | ID of the address book this contact belongs to (defaults to Default book if omitted) | | `first_name` | string | Contact first name | | `last_name` | string | Contact last name | | `company` | string | Company name | | `title` | string | Job title | | `emails` | object[] | List of email addresses (see [Email Object](#email-object)) | | `phone_numbers` | object[] | List of phone numbers (see [Phone Number Object](#phone-number-object)) | | `tags` | string[] | Tags for categorization | | `notes` | string | Free-text notes (max 1000 chars) | | `timezone` | string | Contact timezone (e.g., `"Asia/Ho_Chi_Minh"`) | | `language` | string | Preferred language (e.g., `"vi"`, `"en"`) | | `country_code` | string | ISO 3166-1 alpha-2 country code (e.g., `"VN"`) | | `assigned_agent_id` | string | Optional ID of the assigned agent responsible for this contact | | `source` | string | Origin of this contact (e.g., `"manual"`, `"import"`, `"call"`) | | `custom_fields` | object | Key-value pairs for custom data | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | ### Phone Number Object | Field | Type | Description | | -------------- | ------- | ------------------------------------------------- | | `phone_number` | string | Phone number string (e.g., `"+84901234567"`) | | `type` | string | Type label: `mobile`, `work`, `home`, `fax`, etc. | | `is_primary` | boolean | Whether this is the primary phone number | ### Email Object | Field | Type | Description | | ------------ | ------- | ------------------------------------ | | `email` | string | Email address | | `type` | string | Type label: `work`, `personal`, etc. | | `is_primary` | boolean | Whether this is the primary email | --- ## List Contacts ``` GET /call-center/contacts ``` Retrieve a paginated list of all contacts in the workspace. ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------- | ----------------------------------------------------------- | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `20` | Items per page (min: 1, max: 100) | | `search` | string | — | Search by name, phone number, or email (text search) | | `sort_field` | string | — | Sort by: `created_at`, `first_name`, `last_name`, `company` | | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/contacts?page=1&limit=10&search=john" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` ```json { "data": [ { "id": "ct_a1b2c3d4e5f6g7h8i9j0k1", "workspace_id": "my_workspace", "address_book_id": "ab_x1y2z3w4v5u6t7s8", "first_name": "John", "last_name": "Doe", "company": "Acme Corp", "title": "CTO", "emails": [ { "email": "john@acme.com", "type": "work", "is_primary": true } ], "phone_numbers": [ { "phone_number": "+84901234567", "type": "mobile", "is_primary": true } ], "tags": ["vip", "enterprise"], "notes": "Key decision maker", "timezone": "Asia/Ho_Chi_Minh", "language": "en", "country_code": "VN", "owner_id": "ag_abc123", "owner_type": "agent", "source": "manual", "custom_fields": { "deal_size": "50000" }, "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-07-13T09:00:00.000Z" } ], "meta": { "total": 1, "page": 1, "limit": 10, "total_pages": 1 } } ``` --- ## Lookup Contact by Phone Number ``` GET /call-center/contacts/lookup ``` Find a contact by their phone number. The phone number is automatically normalized for matching, so formats like `+84901234567`, `0901234567`, or `84901234567` all match the same contact. ### Query Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------- | | `number` | string | ✅ | Phone number to look up | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/contacts/lookup?number=84901234567" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` Returns the matching contact object, or `null` if no contact is found. ```json { "id": "ct_a1b2c3d4e5f6g7h8i9j0k1", "first_name": "John", "last_name": "Doe", "company": "Acme Corp", "phone_numbers": [ { "phone_number": "+84901234567", "type": "mobile", "is_primary": true } ] } ``` :::tip This endpoint is commonly used during incoming calls to display caller information. The call flow engine also uses it internally for the **Contact Lookup** node. ::: --- ## Get Contact Details ``` GET /call-center/contacts/:id ``` Retrieve detailed information about a specific contact. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | --------------------------------- | | `id` | string | Contact ID (e.g., `ct_a1b2c3...`) | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/contacts/ct_a1b2c3d4e5f6g7h8i9j0k1" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` Returns the full contact object. ### Error Responses | Status | Description | | ------ | ----------------- | | `404` | Contact not found | --- ## Create a Contact ``` POST /call-center/contacts ``` Create a new contact in the workspace. All fields except the basic identity fields are optional. ### Request Body | Field | Type | Required | Description | | ----------------- | -------------- | -------- | --------------------------------------------------------------- | | `first_name` | string | | First name (max 100 chars) | | `last_name` | string | | Last name (max 100 chars) | | `company` | string | | Company name (max 100 chars) | | `title` | string | | Job title (max 100 chars) | | `emails` | object[] | | Email addresses (see [Email Object](#email-object)) | | `phone_numbers` | object[] | | Phone numbers (see [Phone Number Object](#phone-number-object)) | | `tags` | string[] | | Tags for categorization | | `notes` | string | | Free-text notes (max 1000 chars) | | `timezone` | string | | Timezone identifier (max 50 chars) | | `language` | string | | Language code (max 10 chars) | | `country_code` | string | | ISO country code (max 10 chars) | | `address_book_id` | string | | Assign to an address book (defaults to Default book if omitted) | | `assigned_agent_id` | string | | Optional assigned agent responsible for this contact | | `source` | string | | Origin label (max 50 chars) | | `custom_fields` | object | | Key-value pairs for custom data | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/call-center/contacts" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "first_name": "Jane", "last_name": "Smith", "company": "TechStart Inc.", "title": "Product Manager", "emails": [ { "email": "jane@techstart.io", "type": "work", "is_primary": true } ], "phone_numbers": [ { "phone_number": "+84912345678", "type": "mobile", "is_primary": true }, { "phone_number": "+84281234567", "type": "work" } ], "tags": ["lead", "tech"], "notes": "Met at TechConf 2026", "timezone": "Asia/Ho_Chi_Minh", "language": "vi", "country_code": "VN", "address_book_id": "ab_x1y2z3w4v5u6t7s8", "owner_type": "agent", "source": "manual" }' ``` ### Response `201 Created` Returns the created contact object. ```json { "id": "ct_n3w1d2e3f4g5h6i7j8k9l0", "workspace_id": "my_workspace", "first_name": "Jane", "last_name": "Smith", "company": "TechStart Inc.", "title": "Product Manager", "emails": [ { "email": "jane@techstart.io", "type": "work", "is_primary": true } ], "phone_numbers": [ { "phone_number": "+84912345678", "type": "mobile", "is_primary": true, "normalized_number": "84912345678" }, { "phone_number": "+84281234567", "type": "work", "normalized_number": "84281234567" } ], "tags": ["lead", "tech"], "notes": "Met at TechConf 2026", "address_book_id": "ab_x1y2z3w4v5u6t7s8", "owner_type": "agent", "source": "manual", "created_at": "2026-07-21T08:00:00.000Z", "updated_at": "2026-07-21T08:00:00.000Z" } ``` ### Error Responses | Status | Description | | ------ | ----------------------------------------------- | | `400` | Validation error or address book does not exist | :::info Phone numbers are automatically normalized for consistent lookup. The `normalized_number` field is generated server-side and used for the [Lookup](#lookup-contact-by-phone-number) endpoint. ::: --- ## Update a Contact ``` PUT /call-center/contacts/:id ``` Update an existing contact. All fields are optional — only include the fields you want to change. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ----------- | | `id` | string | Contact ID | ### Request Body Same fields as [Create a Contact](#create-a-contact). All fields are optional. ### Request ```bash curl -X PUT "https://{workspace_id}.firetell.app/api/v1/call-center/contacts/ct_a1b2c3d4e5f6g7h8i9j0k1" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "company": "TechStart Global", "title": "VP of Product", "tags": ["vip", "enterprise", "tech"] }' ``` ### Response `200 OK` Returns the updated contact object. ### Error Responses | Status | Description | | ------ | ----------------------------------------------- | | `400` | Validation error or address book does not exist | | `404` | Contact not found | --- ## Delete a Contact ``` DELETE /call-center/contacts/:id ``` Permanently delete a contact from the workspace. This action cannot be undone. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ----------- | | `id` | string | Contact ID | ### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/call-center/contacts/ct_a1b2c3d4e5f6g7h8i9j0k1" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` Returns the deleted contact object. ### Error Responses | Status | Description | | ------ | ----------------- | | `404` | Contact not found | :::caution Deleting a contact is **irreversible**. Consider updating the contact's `tags` or moving it to a different address book instead if you need to archive it. ::: --- ## Assign Contacts to an Address Book ``` POST /call-center/contacts/assign-list ``` Batch assign multiple contacts to an address book (or unassign by passing `null`). You can assign up to **50 contacts** per request. ### Request Body | Field | Type | Required | Description | | ----------------- | -------------- | -------- | ------------------------------------------------------------------------ | | `address_book_id` | string \| null | ✅ | Target address book ID. Pass `null` to unassign contacts from their book | | `contact_ids` | string[] | ✅ | Array of contact IDs to assign (min: 1, max: 50) | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/call-center/contacts/assign-list" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "address_book_id": "ab_x1y2z3w4v5u6t7s8", "contact_ids": [ "ct_a1b2c3d4e5f6g7h8i9j0k1", "ct_n3w1d2e3f4g5h6i7j8k9l0", "ct_z9y8x7w6v5u4t3s2r1q0p9" ] }' ``` ### Response `200 OK` ```json { "matched": 3, "modified": 3 } ``` | Field | Type | Description | | ---------- | ------ | ----------------------------------------------------- | | `matched` | number | Number of contacts found that you have access to | | `modified` | number | Number of contacts actually updated (excludes no-ops) | :::tip If `matched` is less than the number of `contact_ids` you sent, it means some contacts don't exist or you don't have access to them. ::: ### Unassign Contacts To remove contacts from their current address book, pass `null` as the `address_book_id`: ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/call-center/contacts/assign-list" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "address_book_id": null, "contact_ids": ["ct_a1b2c3d4e5f6g7h8i9j0k1"] }' ``` ### Error Responses | Status | Description | | ------ | ------------------------------------------ | | `400` | Validation error or address book not found | --- # Phone Numbers View phone numbers available to the teams you belong to. These endpoints are **read-only** — phone number management (create, update, delete) is only available via the admin Workspace API. :::info Data Scoping Agents can only see phone numbers where: - `shared_teams_id` contains at least one of the agent's teams, **or** - `shared_teams_id` is empty (available to all agents) Only phone numbers with `active` status are returned. ::: ## Endpoints | Method | Endpoint | Description | | ------ | ------------------------------------- | ----------------------------------- | | `GET` | `/call-center/phone-numbers` | List phone numbers (scoped) | | `GET` | `/call-center/phone-numbers/:id` | Get phone number details | ## Authentication All endpoints require `Bearer` JWT with `agent-api` audience. --- ## List Phone Numbers ``` GET /call-center/phone-numbers ``` Retrieve a paginated list of phone numbers available to your teams. ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------- | ------------------------ | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `20` | Items per page (max: 100)| | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/phone-numbers?page=1&limit=10" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` ```json { "data": [ { "id": "pn_a1b2c3d4e5f6g7h8i9j0k1", "number": "84901234567", "country_code": "VN", "dial_code": "84", "status": "active", "capabilities": ["voice"], "enable_outbound": true, "shared_teams_id": ["tm_x1y2z3w4v5u6t7s8"], "created_at": "2026-01-10T10:00:00.000Z" }, { "id": "pn_n3w1d2e3f4g5h6i7j8k9l0", "number": "84281234567", "country_code": "VN", "dial_code": "84", "status": "active", "capabilities": ["voice"], "enable_outbound": false, "shared_teams_id": [], "created_at": "2026-03-15T08:00:00.000Z" } ], "meta": { "total": 3, "page": 1, "limit": 10, "total_pages": 1 } } ``` ### Phone Number Object | Field | Type | Description | | ------------------ | -------- | ---------------------------------------------------- | | `id` | string | Phone number ID (prefixed `pn_`) | | `number` | string | Full phone number (without `+`) | | `country_code` | string | ISO 3166-1 alpha-2 country code (e.g., `"VN"`) | | `dial_code` | string | Country calling code (e.g., `"84"`) | | `status` | string | Always `active` for call center endpoints | | `capabilities` | string[] | Supported features (e.g., `["voice"]`) | | `enable_outbound` | boolean | Whether outbound calls are enabled | | `shared_teams_id` | string[] | Teams that have access to this number | | `created_at` | string | ISO 8601 creation timestamp | --- ## Get Phone Number Details ``` GET /call-center/phone-numbers/:id ``` Retrieve details of a specific phone number you have access to. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------- | | `id` | string | Phone number ID (e.g., `pn_a1b2...`) | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/phone-numbers/pn_a1b2c3d4e5f6g7h8i9j0k1" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` Returns the phone number object. ### Error Responses | Status | Description | | ------ | ------------------------------------------------ | | `404` | Phone number not found or you do not have access | :::tip Use the `enable_outbound` field to determine which phone numbers can be used as the caller ID when making outbound calls. Numbers with `enable_outbound: false` can only receive inbound calls. ::: --- # Realtime Events (SSE) Firetell provides a **Server-Sent Events (SSE)** stream for subscribing to real-time business events. Unlike WebSocket connections, SSE operates over standard HTTP/2 and natively handles automatic reconnections. :::info **Architecture Note** SSE is used for **server-to-client event observation** (agent presence, teammate status changes, team assignments, contact mutations, queue updates). Call audio and WebRTC signaling use short-lived per-call WebSockets with `call_token`. ::: ## Endpoints | Method | Endpoint | Content-Type | Description | | ------ | ------------------------------ | ------------------- | ----------------------------------- | | `GET` | `/stream?token=YOUR_AGENT_JWT` | `text/event-stream` | Subscribes to real-time events | | `PUT` | `/call-center/events/presence` | `application/json` | Set own presence (`busy` / `ready`) | ## Authentication Authentication is passed via the **`token` query parameter**: `?token=YOUR_AGENT_JWT`. :::info **Why Query Parameter?** The standard W3C browser `EventSource` API does not support custom HTTP headers (such as `Authorization: Bearer`). Passing the JWT via `?token=` query parameter allows native browser `EventSource` to authenticate seamlessly. ::: --- ## Subscribing via JavaScript `EventSource` ```javascript const agentJwt = "YOUR_AGENT_JWT"; const sseUrl = `https://{workspace_id}.firetell.app/stream?token=${encodeURIComponent(agentJwt)}`; const eventSource = new EventSource(sseUrl); // Incoming Call Ring Alert Event (Popup UI / Ring Device) eventSource.addEventListener("call.ring", (e) => { const data = JSON.parse(e.data); console.log("Incoming call ring alert:", data.call_id); console.log("Caller:", data.from.name, data.from.number); console.log("WebSocket URL:", data.ws_url); console.log("Call Token:", data.call_token); // Connect WebSocket per call using ws_url and call_token const ws = new WebSocket(data.ws_url); ws.onopen = () => { ws.send( JSON.stringify({ event: "session.connect", data: { token: data.call_token }, }), ); }; }); // Call Lifecycle Events (Realtime Call List / Live Monitoring) eventSource.addEventListener("call.created", (e) => { const payload = JSON.parse(e.data); console.log("Call created:", payload.data.call_id, payload.data.status); }); eventSource.addEventListener("call.started", (e) => { const payload = JSON.parse(e.data); console.log("Call ringing / started:", payload.data.call_id, payload.data.to); }); eventSource.addEventListener("call.answered", (e) => { const payload = JSON.parse(e.data); console.log("Call answered:", payload.data.call_id, payload.data.answer_time); }); eventSource.addEventListener("call.ended", (e) => { const payload = JSON.parse(e.data); console.log( "Call ended:", payload.data.call_id, payload.data.hangup_cause, payload.data.duration, ); }); // Agent State Change Event eventSource.addEventListener("agent.state", (e) => { const data = JSON.parse(e.data); console.log("Teammate state changed:", data.username, data.state); }); // Contact Created Event eventSource.addEventListener("contact.created", (e) => { const eventData = JSON.parse(e.data); console.log("New contact created:", eventData.data); }); // Team Assigned Event eventSource.addEventListener("team.assigned", (e) => { const data = JSON.parse(e.data); console.log("Assigned to team:", data.team_id, "Role:", data.role); }); // Message (SMS/MMS) Events eventSource.addEventListener("message.received", (e) => { const event = JSON.parse(e.data); console.log("Inbound SMS from:", event.data.from, "Body:", event.data.body); }); eventSource.addEventListener("message.sent", (e) => { const event = JSON.parse(e.data); console.log( "Outbound SMS sent by:", event.data.sender_id, "Body:", event.data.body, ); }); eventSource.addEventListener("message.updated", (e) => { const event = JSON.parse(e.data); console.log("SMS status updated:", event.data.id, event.data.status); }); eventSource.addEventListener("conversation.updated", (e) => { const event = JSON.parse(e.data); console.log("Conversation updated:", event.data.id, "status:", event.data.status, "unread:", event.data.unread_count); }); ``` --- ## Set Agent Presence Agents can set their own presence state to `busy` (Do Not Disturb) or `ready` (return to active state). :::info **`ready` is not a stored state** — it's a command that tells the system to revert the agent to their appropriate active state based on connectivity: - If the agent has an active SSE connection → `online` - If the agent has a registered VoIP push device → `available` - Otherwise → `offline` ::: ### Request ``` PUT /call-center/events/presence Authorization: Bearer YOUR_AGENT_JWT Content-Type: application/json ``` **Request Body:** | Field | Type | Required | Description | | ------- | -------- | -------- | --------------------------------------------------------------- | | `state` | `string` | ✅ | `busy` to set Do Not Disturb, `ready` to return to active state | ```json { "state": "busy" } ``` ### Response ```json { "username": "agent.jane", "state": "busy" } ``` **Unset busy (return to active):** ```json // Request { "state": "ready" } // Response — state determined by connectivity { "username": "agent.jane", "state": "online" } ``` --- ## Event Types ### `system.connected` Emitted immediately upon opening the SSE stream. Contains the list of Server-Sent Events (SSE) Pub/Sub channels subscribed for the agent (`ws:{workspace_id}:{username}`, `presence:{workspace_id}`, `workspace:{workspace_id}`, and `team:{workspace_id}:{team_id}` for all teams the agent belongs to). ```json { "event": "system.connected", "workspace_id": "ws_123456789", "username": "agent.jane", "subscribed_channels": [ "ws:ws_123456789:agent.jane", "presence:ws_123456789", "workspace:ws_123456789", "team:ws_123456789:tm_sales123" ], "timestamp": "2026-07-21T19:20:00.000Z" } ``` ### `system.ping` Emitted periodically every **15 seconds** to maintain stream activity and prevent intermediate proxies, NGINX, or load balancers from closing the connection due to idle timeouts. ```json { "event": "system.ping", "timestamp": "2026-07-21T19:20:15.000Z" } ``` ### `call.ring` Emitted when an incoming or transferred call is ringing for the agent. Triggered instantly over the SSE stream to allow the client application to display the Ringing UI Popup and establish a short-lived WebSocket connection to `ws_url` per call using the provided `call_token`. ```json { "event": "call.ring", "call_id": "call_1770000000000", "call_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "ws_url": "wss://ws_123456789.firetell.app/ws", "from": { "number": "+12025550143", "name": "Alice Smith", "avatar": null }, "to": { "number": "+18005550199", "name": "Support Team" }, "is_transfer": false, "timestamp": "2026-07-30T16:07:00.000Z" } ``` --- ## Call Lifecycle Events Call lifecycle events stream real-time updates for call state transitions (creation, ringing, answered, ended). They carry the full `Call` object inside `data` and are used to power live dashboards, call monitoring tables, and real-time history updates. :::info **Role-Based Event Scoping** - **Console Admin / Leader / Supervisor**: Subscribes to the workspace firehose (`calls:{workspace_id}:all`) and receives all call events in real time. - **Member (Agent)**: Receives only calls relevant to their scope: - Calls where the agent is the caller (`from.number`) or callee (`to.number`). - Calls dispatched to teams the agent belongs to. - Inbound/Outbound calls on phone numbers shared with the agent's teams. ::: ### `call.created` Emitted when a new call session is initialized in the workspace. ```json { "event": "call.created", "workspace_id": "ws_123456789", "data": { "id": "call_5c1180ce64848be4395d12", "call_id": "call_5c1180ce64848be4395d12", "workspace_id": "ws_123456789", "number": "18005550199", "client_number": "12025550143", "from": { "type": "client", "number": "12025550143", "name": "Alice Smith", "avatar": null }, "to": { "type": "agent", "number": "18005550199", "name": "Support Desk", "avatar": null }, "source": "client-api", "type": "audio", "direction": "inbound", "status": "started", "start_time": "2026-08-14T08:32:13.819Z", "answer_time": null, "end_time": null, "duration": 0, "billsec": 0, "cost": 0, "hangup_cause": null, "hangup_code": null, "hangup_source": null, "recordings": [], "queue": null, "outbound": null, "agent_id": null, "call_flow_id": null, "created_at": "2026-08-14T08:32:13.819Z", "updated_at": "2026-08-14T08:32:13.819Z" }, "timestamp": "2026-08-14T08:32:13.825Z" } ``` ### `call.started` Emitted when the call begins ringing / progressing to an agent, team, or carrier destination. ```json { "event": "call.started", "workspace_id": "ws_123456789", "data": { "id": "call_5c1180ce64848be4395d12", "call_id": "call_5c1180ce64848be4395d12", "workspace_id": "ws_123456789", "number": "18005550199", "client_number": "12025550143", "from": { "type": "client", "number": "12025550143", "name": "Alice Smith", "avatar": null }, "to": { "type": "agent", "number": "agent.jane", "name": "Jane Doe", "avatar": null }, "source": "client-api", "type": "audio", "direction": "inbound", "status": "started", "start_time": "2026-08-14T08:32:13.819Z", "answer_time": null, "end_time": null, "duration": 0, "billsec": 0, "cost": 0, "hangup_cause": null, "hangup_code": null, "hangup_source": null, "recordings": [], "queue": null, "outbound": null, "agent_id": "ag_123456", "call_flow_id": "flow_123456", "created_at": "2026-08-14T08:32:13.819Z", "updated_at": "2026-08-14T08:32:23.071Z" }, "timestamp": "2026-08-14T08:32:23.075Z" } ``` ### `call.answered` Emitted when the call is answered by a destination agent or connected via bridge. ```json { "event": "call.answered", "workspace_id": "ws_123456789", "data": { "id": "call_5c1180ce64848be4395d12", "call_id": "call_5c1180ce64848be4395d12", "workspace_id": "ws_123456789", "number": "18005550199", "client_number": "12025550143", "from": { "type": "client", "number": "12025550143", "name": "Alice Smith", "avatar": null }, "to": { "type": "agent", "number": "agent.jane", "name": "Jane Doe", "avatar": null }, "source": "client-api", "type": "audio", "direction": "inbound", "status": "active", "start_time": "2026-08-14T08:32:13.819Z", "answer_time": "2026-08-14T08:32:28.140Z", "end_time": null, "duration": 0, "billsec": 0, "cost": 0, "hangup_cause": null, "hangup_code": null, "hangup_source": null, "recordings": [], "queue": null, "outbound": null, "agent_id": "ag_123456", "call_flow_id": "flow_123456", "created_at": "2026-08-14T08:32:13.819Z", "updated_at": "2026-08-14T08:32:28.140Z" }, "timestamp": "2026-08-14T08:32:28.145Z" } ``` ### `call.ended` Emitted when the call finishes and all channels are terminated. Contains final call metrics (`duration`, `billsec`, `hangup_cause`, `cost`). ```json { "event": "call.ended", "workspace_id": "ws_123456789", "data": { "id": "call_5c1180ce64848be4395d12", "call_id": "call_5c1180ce64848be4395d12", "workspace_id": "ws_123456789", "number": "18005550199", "client_number": "12025550143", "from": { "type": "client", "number": "12025550143", "name": "Alice Smith", "avatar": null }, "to": { "type": "agent", "number": "agent.jane", "name": "Jane Doe", "avatar": null }, "source": "client-api", "type": "audio", "direction": "inbound", "status": "completed", "start_time": "2026-08-14T08:32:13.819Z", "answer_time": "2026-08-14T08:32:28.140Z", "end_time": "2026-08-14T08:32:35.676Z", "duration": 22, "billsec": 7, "cost": 0.005, "hangup_cause": "NORMAL_CLEARING", "hangup_code": 200, "hangup_source": "caller", "recordings": [], "queue": null, "outbound": null, "agent_id": "ag_123456", "call_flow_id": "flow_123456", "created_at": "2026-08-14T08:32:13.819Z", "updated_at": "2026-08-14T08:32:35.676Z" }, "timestamp": "2026-08-14T08:32:35.680Z" } ``` ### `agent.state` Emitted whenever an agent's presence state changes. State transitions happen automatically based on agent connectivity and call activity. | State | Description | | ----------- | -------------------------------------------------------------------------------------------------------------------- | | `online` | Agent is actively connected via SSE event stream | | `available` | Agent is reachable via VoIP push notification (has registered device with push token) but not actively streaming SSE | | `incall` | Agent is currently in an active call (set when agent offers or answers a call, reverted when call ends) | | `busy` | Agent is busy (set manually or by supervisor) | | `offline` | Agent has no active connections or registered push devices | ```json { "event": "agent.state", "workspace_id": "ws_123456789", "username": "agent.jane", "state": "online", "timestamp": "2026-07-30T16:07:00.000Z" } ``` :::tip **State transition examples:** - Agent opens the app and connects SSE → `online` - Agent closes the app but has VoIP push token registered → `available` - Agent answers or makes a call → `incall` - Call ends while SSE is connected → `online` - Call ends while only push token exists → `available` - Agent logs out from all devices → `offline` ::: ### `agent.state.forced` Emitted when a team leader or supervisor force-changes an agent's state. Currently, supervisors can only force agents to `offline` state. Automatic states (`online`, `available`, `incall`) are managed by the system based on connectivity. ```json { "event": "agent.state.forced", "workspace_id": "ws_123456789", "id": "650000000000000000000001", "target_agent_id": "650000000000000000000001", "target_username": "agent.john", "new_state": "offline", "reason": "Shift ended", "forced_by": "supervisor.smith", "timestamp": "2026-07-30T18:00:00.000Z" } ``` ### `agent.created` / `agent.updated` / `agent.deleted` Emitted when a workspace agent account is created, updated, or deleted. ```json { "event": "agent.created", "workspace_id": "ws_123456789", "data": { "id": "650000000000000000000001", "username": "agent.john", "display_name": "John Doe", "email": "john@company.com", "role": "agent", "avatar": "https://cdn.firetell.com/avatars/agent_john.png", "country_code": "US", "is_active": true }, "timestamp": "2026-08-03T15:00:00.000Z" } ``` ### `contact.created` / `contact.updated` / `contact.deleted` Emitted when a contact is created, updated, or deleted within the workspace. ```json { "event": "contact.created", "workspace_id": "ws_123456789", "data": { "id": "ct_987654321", "display_name": "John Doe", "phone_numbers": [{ "phone_number": "+1234567890", "type": "mobile" }], "owner_type": "everyone" }, "timestamp": "2026-07-21T16:00:00.000Z" } ``` ### `team.assigned` / `team.unassigned` Emitted when an agent is assigned to or removed from a team. ```json { "event": "team.assigned", "workspace_id": "ws_123456789", "team_id": "tm_123456789", "agent_id": "ag_123456789", "role": "member", "timestamp": "2026-07-21T16:00:00.000Z" } ``` --- ## Message (SMS/MMS) Events Real-time message events are streamed across both the agent's scoped unicast channels (`ws:{workspace_id}:{username}`) and workspace channels (`workspace:{workspace_id}`) to update live SMS inbox views, chat consoles, and notification badges. :::info **Delivery Scoping & Team Routing** Message events are routed directly to the unicast channel of: 1. The agent assigned to the thread (`assigned_agent_id`). 2. All members of the team assigned to the thread (`assigned_team_id`). 3. If unassigned, all members of teams associated with the DID number (`shared_teams_id`). 4. For outbound messages, the sending agent's other open tabs/windows are also notified. ::: ### `message.received` Emitted when a new incoming SMS or MMS message arrives from a customer on any active workspace phone number. ```json { "event": "message.received", "workspace_id": "ws_123456789", "timestamp": "2026-09-27T10:00:00.000Z", "data": { "id": "msg_be52701819fa34a72ae414", "conversation_id": "conv_a1b2c3d4e5f6", "phone_number_id": "pn_99bf875464f1c096f46aff", "direction": "inbound", "from": "+18647123123", "to": "+13074295456", "client_number": "+18647123123", "client_name": "Alice Smith", "body": "Hello, I need assistance with my reservation.", "status": "received", "sender_type": "client", "media_urls": [], "unread_count": 1, "created_at": "2026-09-27T10:00:00.000Z" } } ``` ### `message.sent` Emitted when an agent or system service dispatches an outbound SMS message in a conversation thread. Used by browser Call Center Portals for **real-time chat updates** and **collision avoidance**, preventing teammates in the same team from sending duplicate responses. ```json { "event": "message.sent", "workspace_id": "ws_123456789", "timestamp": "2026-09-27T10:02:00.000Z", "data": { "id": "msg_05f18045cba1452204774f", "conversation_id": "conv_a1b2c3d4e5f6", "phone_number_id": "pn_99bf875464f1c096f46aff", "direction": "outbound", "from": "+13074295456", "to": "+18647123123", "client_number": "+18647123123", "client_name": "Alice Smith", "body": "Hello Alice, I have updated your reservation for 7:00 PM.", "status": "queued", "sender_type": "agent", "sender_id": "agent_002", "media_urls": [], "unread_count": 0, "created_at": "2026-09-27T10:02:00.000Z" } } ``` ### `message.updated` Emitted when an outbound message changes delivery state with the telecom carrier (e.g. transitioning from `queued` to `sent`, `delivered`, or `failed`). Used by client applications to update delivery checkmarks and error badges in real time. ```json { "event": "message.updated", "workspace_id": "ws_123456789", "timestamp": "2026-09-27T10:02:05.000Z", "data": { "id": "msg_05f18045cba1452204774f", "phone_number_id": "pn_99bf875464f1c096f46aff", "direction": "outbound", "from": "+13074295456", "to": "+18647123123", "body": "Hello Alice, I have updated your reservation for 7:00 PM.", "status": "delivered", "error_code": null, "error_message": null, "created_at": "2026-09-27T10:02:00.000Z", "updated_at": "2026-09-27T10:02:05.000Z" } } ``` ### `conversation.updated` Emitted when a conversation thread's metadata or state changes — such as when an agent marks messages as read (`unread_count` resets to 0 and `last_read_at` updates), changes thread status between `open` and `closed`, reassigns the conversation to another agent or team, or when client contact info is updated. Used by web consoles to update conversation list badges and assignment chips in real time. ```json { "event": "conversation.updated", "workspace_id": "ws_123456789", "timestamp": "2026-09-28T03:40:00.000Z", "data": { "id": "conv_a1b2c3d4e5f6", "workspace_id": "ws_123456789", "phone_number_id": "pn_99bf875464f1c096f46aff", "system_number": "+13074295456", "client_number": "+18647123123", "client_name": "Alice Smith", "assigned_agent_id": "agent_001", "assigned_team_id": null, "status": "open", "unread_count": 0, "last_message": { "id": "msg_05f18045cba1452204774f", "body": "Hello Alice, I have updated your reservation for 7:00 PM.", "direction": "outbound", "sender_type": "agent", "sender_id": "agent_001", "created_at": "2026-09-28T03:39:00.000Z" }, "last_message_at": "2026-09-28T03:39:00.000Z", "last_read_at": "2026-09-28T03:40:00.000Z", "created_at": "2026-09-27T10:00:00.000Z", "updated_at": "2026-09-28T03:40:00.000Z" } } ``` --- # Roles & Permissions Every agent in a workspace has a **role** that determines what they can do. The Firetell Call Center uses a 3-tier role system. ## Roles | Role | Description | |---|---| | **member** | Standard agent. Can make/receive calls, manage their own contacts, and view their own call history. | | **leader** | Team lead. Has all member permissions plus team-wide visibility and agent management capabilities. | | **supervisor** | Call center supervisor. Focuses on call quality assurance — real-time call supervision (listen, whisper, barge) plus team-wide visibility. | ## Permission Matrix | Feature | `member` | `leader` | `supervisor` | |---|:---:|:---:|:---:| | **Basic Operations** | | | | | View teams I belong to | ✅ | ✅ | ✅ | | View teammates + presence | ✅ | ✅ | ✅ | | CRUD my own contacts | ✅ | ✅ | ✅ | | View contacts in accessible lists (personal, team, shared) | ✅ | ✅ | ✅ | | Create / update / delete personal address books (`owner_type: agent`) | ✅ | ✅ | ✅ | | View phone numbers (scoped by team) | ✅ | ✅ | ✅ | | View my call history | ✅ | ✅ | ✅ | | Transfer calls between agents | ✅ | ✅ | ✅ | | **Team Management** | | | | | Create / update / delete team address books (`owner_type: team`) | ❌ | ✅ | ❌ | | Create / update / delete shared address books (`owner_type: everyone`) | ❌ | ✅ | ❌ | | View team-wide call history | ❌ | ✅ | ✅ | | View all team contacts | ❌ | ✅ | ✅ | | Assign / remove agents from teams | ❌ | ✅ | ❌ | | **Call Supervision** | | | | | Silent listen (monitor) | ❌ | ❌ | ✅ | | Whisper (coach agent) | ❌ | ❌ | ✅ | | Barge-in (3-way call) | ❌ | ❌ | ✅ | ## Setting Roles Roles are assigned when adding an agent to a team via the admin console or the Workspace API: ```bash PUT /teams/:team_id/agents/:agent_id ``` ```json { "role": "leader" } ``` Valid values: `member` (default), `leader`, `supervisor`. An agent has a **single workspace-level role** that applies across all teams they belong to. ::: ## Endpoint Access by Role ### All Agents (member, leader, supervisor) | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/call-center/teams` | List my teams | | `GET` | `/call-center/teams/:id/agents` | List teammates | | `GET` | `/call-center/contacts` | List contacts in my accessible lists | | `POST` | `/call-center/contacts` | Create contact | | `PUT` | `/call-center/contacts/:id` | Update contact | | `DELETE` | `/call-center/contacts/:id` | Delete contact | | `GET` | `/call-center/address-books` | List accessible address books | | `GET` | `/call-center/address-books/:id` | Get address book details | | `POST` | `/call-center/address-books` | Create personal address book (`owner_type: agent`) | | `PUT` | `/call-center/address-books/:id` | Update personal address book | | `DELETE` | `/call-center/address-books/:id` | Delete personal address book | | `GET` | `/call-center/phone-numbers` | List phone numbers | | `GET` | `/call-center/call-history` | List my calls | | `POST` | `/call-center/calls/:call_id/transfer` | Transfer call | ### Leader Only | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/call-center/address-books` | Create team/shared address book (`owner_type: team` or `everyone`) | | `PUT` | `/call-center/address-books/:id` | Update team/shared address book | | `DELETE` | `/call-center/address-books/:id` | Delete team/shared address book | | `GET` | `/call-center/teams/:team_id/call-history` | Team call history | | `GET` | `/call-center/teams/:team_id/call-history/:id` | Team call details | | `PUT` | `/call-center/teams/:id/agents/:agent_id` | Assign agent to team | | `DELETE` | `/call-center/teams/:id/agents/:agent_id` | Remove agent from team | | `GET` | `/call-center/teams/:team_id/contacts` | Team contacts | | `GET` | `/call-center/teams/:team_id/agents/states` | Team agent states | | `PUT` | `/call-center/teams/:team_id/agents/:username/state` | Force agent state | ### Leader & Supervisor | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/call-center/teams/:team_id/call-history` | Team call history | | `GET` | `/call-center/teams/:team_id/call-history/:id` | Team call details | | `GET` | `/call-center/teams/:team_id/agents/states` | Team agent states | | `PUT` | `/call-center/teams/:team_id/agents/:username/state` | Force agent state | ### Supervisor Only | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/call-center/calls/:call_id/listen` | Silent monitor | | `POST` | `/call-center/calls/:call_id/whisper` | Coach agent | | `POST` | `/call-center/calls/:call_id/barge` | Join as 3-way call | | `DELETE` | `/call-center/calls/:call_id/supervision` | Stop supervision | ## Error Responses When an agent tries to access an endpoint they don't have permission for: ```json { "statusCode": 403, "message": "This action requires one of the following roles: leader, supervisor", "error": "Forbidden" } ``` When an agent tries to access a team they don't belong to: ```json { "statusCode": 403, "message": "You are not a member of this team", "error": "Forbidden" } ``` --- # SMS Conversations The **SMS Conversations API** provides call center agents with a unified two-way messaging inbox. Conversations use a **Persistent Thread** model — exactly one thread exists per client phone number and system DID hotline in the workspace, ensuring complete message history across agent shifts and customer touchpoints. :::info **Data Scoping & Team Assignment** Access to conversation threads is strictly scoped based on the agent's role and team assignments: - **Leader / Supervisor / Admin**: Full access to view and manage all conversations across the workspace. - **Member (Agent)**: Can only access conversations where the system DID is assigned to one of their teams (`shared_teams_id`), or threads assigned directly to them or their team. Regular members can only send new SMS from numbers assigned to their teams. ::: --- ## Endpoints | Method | Endpoint | Description | | ------- | ----------------------------------------- | ---------------------------------------------- | | `GET` | `/call-center/conversations` | List conversation threads (SMS Inbox) | | `POST` | `/call-center/conversations` | Start a new conversation thread (New Message) | | `GET` | `/call-center/conversations/:id` | Get details of a single conversation thread | | `GET` | `/call-center/conversations/:id/messages` | List message history for a conversation thread | | `POST` | `/call-center/conversations/:id/messages` | Send an outbound SMS reply in a thread | | `PATCH` | `/call-center/conversations/:id/read` | Mark conversation messages as read | | `PATCH` | `/call-center/conversations/:id` | Update conversation status or agent assignment | ## Authentication All endpoints require a `Bearer` JWT with `agent-api` audience. ```http Authorization: Bearer YOUR_AGENT_JWT ``` --- ## The Conversation Object | Field | Type | Description | | ------------------- | ---------------- | ---------------------------------------------------------------------------------- | | `id` | `string` | Unique conversation identifier prefixed with `conv_` (e.g. `conv_a1b2c3d4e5f6`) | | `workspace_id` | `string` | Workspace identifier | | `phone_number_id` | `string` | ID of the workspace DID number used for the thread (`pn_...`) | | `system_number` | `string` | E.164 phone number of the workspace DID (e.g. `+13074295456`) | | `client_number` | `string` | E.164 phone number of the client (e.g. `+18647123123`) | | `client_name` | `string \| null` | Contact display name if matched in address books or manually set | | `contact_id` | `string \| null` | Associated contact ID if resolved in contacts (`con_...`) | | `assigned_agent_id` | `string \| null` | Agent ID currently assigned to handle this thread | | `assigned_team_id` | `string \| null` | Team ID assigned to handle this thread | | `status` | `string` | Thread lifecycle status: [`open` \| `closed`](#conversation-status) | | `unread_count` | `number` | Number of unread inbound messages from client | | `last_message` | `object \| null` | Snippet object of the latest message in the thread (see below) | | `last_message_at` | `string \| null` | ISO 8601 timestamp of the latest message | | `last_read_at` | `string \| null` | ISO 8601 timestamp when an agent last marked the thread as read | | `created_at` | `string` | ISO 8601 creation timestamp | | `updated_at` | `string` | ISO 8601 last update timestamp | ### The `last_message` Snippet Object Embedded inside `conversation.last_message`: | Field | Type | Description | | ------------- | ---------------- | ----------------------------------------------------------------------------------------------- | | `id` | `string` | Message identifier prefixed with `msg_` | | `body` | `string` | Content text of the latest message | | `direction` | `string` | Message transmission direction: [`inbound` \| `outbound`](#direction) | | `sender_type` | `string` | Originator classification: [`client` \| `agent` \| `voice_agent` \| `system`](#sender_type) | | `sender_id` | `string \| null` | Agent ID who sent the message (or `null` if sent by client, AI agent, or automated system) | | `created_at` | `string` | ISO 8601 creation timestamp | --- ## The Message Object Represents an individual SMS or MMS message within a conversation thread. | Field | Type | Description | | ----------------- | ---------------- | -------------------------------------------------------------------------------------------- | | `id` | `string` | Unique message identifier prefixed with `msg_` (e.g. `msg_682d82eb0ba1ffeace018f`) | | `conversation_id` | `string` | Associated conversation thread ID (`conv_...`) | | `workspace_id` | `string` | Workspace identifier | | `phone_number_id` | `string` | ID of the workspace DID phone number used (`pn_...`) | | `from` | `string` | Sender phone number in E.164 format (e.g. `+13074295456`) | | `to` | `string` | Recipient phone number in E.164 format (e.g. `+18647123123`) | | `client_number` | `string` | Client phone number in E.164 format | | `body` | `string` | Text content of the SMS message | | `direction` | `string` | Transmission direction: [`inbound` \| `outbound`](#direction) | | `status` | `string` | Delivery / receipt status: [`queued` \| `sending` \| `sent` \| `delivered` \| `received` \| `failed` \| `undelivered`](#message-status) | | `sender_type` | `string` | Originator entity: [`client` \| `agent` \| `voice_agent` \| `system`](#sender_type) | | `sender_id` | `string \| null` | ID of the agent who sent the message (`null` for client or system messages) | | `media_urls` | `string[]` | Optional array of public HTTPS URLs for MMS attachments | | `cost` | `number \| null` | Cost incurred in workspace credits / USD for sending the SMS | | `error_code` | `string \| null` | Telecom carrier or internal error code if transmission failed | | `error_message` | `string \| null` | Explanatory error message if transmission failed | | `created_at` | `string` | ISO 8601 creation timestamp | | `updated_at` | `string` | ISO 8601 last update timestamp | --- ## Enumerations & Value References Comprehensive reference for enumerated fields used across conversations, messages, real-time events, and webhooks. ### `direction` Specifies the communication direction relative to the workspace Call Center. | Enum Value | Flow | Description | | ---------- | ---- | ----------- | | `inbound` | Customer ➔ Workspace DID | **Incoming message**: Sent by an external customer/client to a workspace DID hotline. Increments `unread_count` on the thread and triggers incoming alert notifications. | | `outbound` | Workspace DID ➔ Customer | **Outgoing message**: Sent from a workspace DID to a customer's phone number by an agent, AI voice agent, or automated workflow. | --- ### `status` The `status` attribute is used in two contexts: **Conversation Status** (thread resolution lifecycle) and **Message Status** (carrier delivery state). #### Conversation Status Applies to `conversation.status` and query filters on `/call-center/conversations`. | Enum Value | State | Description | | ---------- | ----- | ----------- | | `open` | Active | The conversation thread is active, awaiting agent response, or undergoing live interaction. Newly created conversations or threads receiving a new customer inbound SMS are automatically set to `open`. | | `closed` | Resolved / Archived | The conversation thread has been marked as resolved or closed by an agent or supervisor. If the customer sends another inbound SMS to the same DID in the future, the thread will automatically reopen (`open`) and bump to the top of the inbox. | #### Message Status Applies to `message.status` and real-time carrier status delivery updates. | Enum Value | Direction | Category | Description | | ------------- | --------- | -------- | ----------- | | `queued` | Outbound | Pending | Message has been accepted by the API and enqueued for carrier dispatch. | | `sending` | Outbound | In-flight| Message is actively being transmitted to the telecom carrier network. | | `sent` | Outbound | Dispatched | Telecom carrier accepted the message and dispatched it into the cellular network. | | `delivered` | Outbound | Success | Carrier received a confirmed Handset Delivery Receipt (DLR) indicating the SMS was successfully delivered to the customer's phone. | | `received` | Inbound | Success | Inbound SMS was successfully received from the carrier and appended to the conversation thread. | | `failed` | Outbound | Terminal Failure | Message delivery permanently failed before or during carrier dispatch (e.g. invalid destination number, blocked prefix, or unroutable route). Check `error_code` and `error_message`. | | `undelivered` | Outbound | Terminal Failure | Carrier dispatched the SMS but the destination mobile network reported delivery failure (e.g. handset powered off, out of coverage, or unreachable). | ```mermaid stateDiagram-v2 direction LR [*] --> queued: Send Message queued --> sending: Carrier Dispatch sending --> sent: Carrier Accepted sent --> delivered: Carrier DLR Success sent --> undelivered: Carrier DLR Failure sending --> failed: Carrier Rejected queued --> failed: Invalid Route/Number [*] --> received: Inbound Message ``` --- ### `sender_type` Identifies the originator or authoring entity that created the message. | Enum Value | Typical Direction | Description | | ------------- | ----------------- | ----------- | | `client` | `inbound` | **Customer / Client**: The external mobile phone subscriber sending an SMS to the workspace DID number. | | `agent` | `outbound` | **Call Center Agent**: A human agent sending a reply from the Firetell Console inbox or Client SDK. `sender_id` will contain the agent's user ID. | | `voice_agent` | `outbound` | **AI Voice Agent**: An automated conversational Voice AI agent sending an SMS during or after a phone call (e.g. sending a scheduling link, OTP verification code, address, or summary). | | `system` | `outbound` | **System / Workflow Automation**: Automatically generated by backend platform workflows, API triggers, automated reminders, or webhooks. | --- ## List Conversations (Inbox) ```http GET /call-center/conversations ``` Retrieve a paginated list of conversation threads accessible to the authenticated agent, ordered by most recent message activity. ### Query Parameters | Parameter | Type | Default | Description | | ------------------- | --------- | ------- | ------------------------------------------------------------------------------------------- | | `page` | `number` | `1` | Page number | | `limit` | `number` | `20` | Number of items per page (max: 100) | | `status` | `string` | — | Filter by status: `open` or `closed` | | `phone_number_id` | `string` | — | Filter by specific hotline phone number ID (`pn_...`) | | `assigned_agent_id` | `string` | — | Filter by assigned agent: `'me'`, `'unassigned'`, or a specific agent ID | | `assigned_to` | `string` | — | Alias for `assigned_agent_id` | | `assigned_team_id` | `string` | — | Filter by assigned team ID | | `unread_only` | `boolean` | `false` | When set to `true`, only returns conversation threads that contain unread incoming messages | | `search` | `string` | — | Search across `client_number`, `client_name`, or `system_number` | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/conversations?status=open&page=1&limit=20" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` ```json { "data": [ { "id": "conv_a1b2c3d4e5f6", "workspace_id": "yourcompany", "phone_number_id": "pn_99bf875464f1c096f46aff", "system_number": "+13074295456", "client_number": "+18647123123", "client_name": "John Doe", "contact_id": "con_650000000000000000000130", "assigned_agent_id": "agent_001", "assigned_team_id": "team_support_tier1", "status": "open", "unread_count": 2, "last_message": { "id": "msg_682d82eb0ba1ffeace018f", "body": "Can I reschedule my appointment?", "direction": "inbound", "sender_type": "client", "sender_id": null, "created_at": "2026-09-27T10:00:00.000Z" }, "last_message_at": "2026-09-27T10:00:00.000Z", "last_read_at": "2026-09-27T09:30:00.000Z", "created_at": "2026-09-25T08:00:00.000Z", "updated_at": "2026-09-27T10:00:00.000Z" } ], "meta": { "total": 1, "page": 1, "limit": 20, "total_pages": 1 } } ``` --- ## Start New Conversation (New Message) ```http POST /call-center/conversations ``` Initiate a new conversation thread or send a message to a client directly from the agent UI (e.g., clicking **"New Message"**). If an active thread already exists between the selected DID and the client, the message is appended to the existing thread. The thread is automatically assigned to the initiating agent if currently unassigned. :::note **DID Authorization Check** For agents with the `member` role, `from` must be a DID phone number assigned to at least one of their teams. If unauthorized, a `403 Forbidden` error is returned. ::: ### Request Body | Field | Type | Required | Description | | --------------- | ---------- | -------- | --------------------------------------------------------------------------- | | `from` | `string` | **Yes** | Workspace DID phone number to send from (E.164 format, e.g. `+13074295456`) | | `client_number` | `string` | **Yes** | Client recipient phone number (E.164 format, e.g. `+18647123123`) | | `body` | `string` | **Yes** | Text body of the SMS message (up to 1,600 characters) | | `media_urls` | `string[]` | No | Optional array of media URLs for MMS messages | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/call-center/conversations" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "from": "+13074295456", "client_number": "+18647123123", "body": "Hello John, thank you for reaching out to Firetell support." }' ``` ### Response `201 Created` ```json { "conversation": { "id": "conv_a1b2c3d4e5f6", "workspace_id": "yourcompany", "phone_number_id": "pn_99bf875464f1c096f46aff", "system_number": "+13074295456", "client_number": "+18647123123", "client_name": "John Doe", "contact_id": "con_650000000000000000000130", "assigned_agent_id": "agent_001", "assigned_team_id": null, "status": "open", "unread_count": 0, "last_message": { "id": "msg_682d82eb0ba1ffeace018f", "body": "Hello John, thank you for reaching out to Firetell support.", "direction": "outbound", "sender_type": "agent", "sender_id": "agent_001", "created_at": "2026-09-27T10:05:00.000Z" }, "last_message_at": "2026-09-27T10:05:00.000Z", "last_read_at": "2026-09-27T10:05:00.000Z", "created_at": "2026-09-27T10:05:00.000Z", "updated_at": "2026-09-27T10:05:00.000Z" }, "message": { "id": "msg_682d82eb0ba1ffeace018f", "conversation_id": "conv_a1b2c3d4e5f6", "workspace_id": "yourcompany", "phone_number_id": "pn_99bf875464f1c096f46aff", "from": "+13074295456", "to": "+18647123123", "client_number": "+18647123123", "body": "Hello John, thank you for reaching out to Firetell support.", "direction": "outbound", "status": "queued", "sender_type": "agent", "sender_id": "agent_001", "media_urls": [], "created_at": "2026-09-27T10:05:00.000Z", "updated_at": "2026-09-27T10:05:00.000Z" } } ``` --- ## Get Conversation Details ```http GET /call-center/conversations/:id ``` Retrieve full details of an accessible conversation thread by its ID. ### Path Parameters | Parameter | Type | Description | | --------- | -------- | ------------------------------------- | | `id` | `string` | Conversation ID prefixed with `conv_` | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/conversations/conv_a1b2c3d4e5f6" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` ```json { "id": "conv_a1b2c3d4e5f6", "workspace_id": "yourcompany", "phone_number_id": "pn_99bf875464f1c096f46aff", "system_number": "+13074295456", "client_number": "+18647123123", "client_name": "John Doe", "contact_id": "con_650000000000000000000130", "assigned_agent_id": "agent_001", "assigned_team_id": "team_support_tier1", "status": "open", "unread_count": 2, "last_message": { "id": "msg_682d82eb0ba1ffeace018f", "body": "Can I reschedule my appointment?", "direction": "inbound", "sender_type": "client", "sender_id": null, "created_at": "2026-09-27T10:00:00.000Z" }, "last_message_at": "2026-09-27T10:00:00.000Z", "last_read_at": "2026-09-27T09:45:00.000Z", "created_at": "2026-09-25T08:00:00.000Z", "updated_at": "2026-09-27T10:00:00.000Z" } ``` --- ## List Messages in Conversation ```http GET /call-center/conversations/:id/messages ``` Retrieve paginated messages for a conversation thread. Results are sorted in **chronological order** (oldest first to newest last) to facilitate rendering chat feeds in agent interfaces. ### Query Parameters | Parameter | Type | Default | Description | | --------- | -------- | ------- | ---------------------------------------------------------- | | `page` | `number` | `1` | Page number | | `limit` | `number` | `50` | Number of messages to return per page (max: 100) | | `before` | `string` | — | ISO 8601 timestamp cursor to paginate backwards in history | | `after` | `string` | — | ISO 8601 timestamp cursor to paginate forwards in history | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/conversations/conv_a1b2c3d4e5f6/messages?page=1&limit=50" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` ```json { "data": [ { "id": "msg_682d82eb0ba1ffeace018e", "conversation_id": "conv_a1b2c3d4e5f6", "workspace_id": "yourcompany", "phone_number_id": "pn_99bf875464f1c096f46aff", "from": "+18647123123", "to": "+13074295456", "client_number": "+18647123123", "body": "Hi, I need assistance with my service.", "direction": "inbound", "status": "received", "sender_type": "client", "sender_id": null, "media_urls": [], "created_at": "2026-09-27T09:55:00.000Z", "updated_at": "2026-09-27T09:55:01.000Z" }, { "id": "msg_682d82eb0ba1ffeace018f", "conversation_id": "conv_a1b2c3d4e5f6", "workspace_id": "yourcompany", "phone_number_id": "pn_99bf875464f1c096f46aff", "from": "+13074295456", "to": "+18647123123", "client_number": "+18647123123", "body": "Can I reschedule my appointment?", "direction": "inbound", "status": "received", "sender_type": "client", "sender_id": null, "media_urls": [], "created_at": "2026-09-27T10:00:00.000Z", "updated_at": "2026-09-27T10:00:01.000Z" } ], "meta": { "total": 2, "page": 1, "limit": 50, "total_pages": 1 } } ``` --- ## Send Message (Reply) ```http POST /call-center/conversations/:id/messages ``` Send an outbound SMS/MMS reply to the client in an existing conversation thread. The message automatically dispatches from the thread's configured `system_number` to the client's `client_number`. ### Request Body | Field | Type | Required | Description | | ------------ | ---------- | -------- | ------------------------------------------------------- | | `body` | `string` | **Yes** | Text body of the SMS reply (up to 1,600 characters) | | `media_urls` | `string[]` | No | Optional array of public HTTPS URLs for MMS attachments | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/call-center/conversations/conv_a1b2c3d4e5f6/messages" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "body": "Sure thing, John! We have slots available tomorrow at 2:00 PM or 4:00 PM." }' ``` ### Response `201 Created` ```json { "id": "msg_682d82eb0ba1ffeace0190", "conversation_id": "conv_a1b2c3d4e5f6", "workspace_id": "yourcompany", "phone_number_id": "pn_99bf875464f1c096f46aff", "from": "+13074295456", "to": "+18647123123", "client_number": "+18647123123", "body": "Sure thing, John! We have slots available tomorrow at 2:00 PM or 4:00 PM.", "direction": "outbound", "status": "queued", "sender_type": "agent", "sender_id": "agent_001", "media_urls": [], "created_at": "2026-09-27T10:05:00.000Z", "updated_at": "2026-09-27T10:05:00.000Z" } ``` --- ## Mark Conversation as Read ```http PATCH /call-center/conversations/:id/read ``` Mark all unread incoming messages in a conversation thread as read. This resets `unread_count` to `0` and updates `last_read_at` to the current timestamp. ### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/call-center/conversations/conv_a1b2c3d4e5f6/read" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` ```json { "ok": true, "conversation_id": "conv_a1b2c3d4e5f6" } ``` --- ## Update Conversation ```http PATCH /call-center/conversations/:id ``` Update conversation metadata, such as assigning the thread to a specific agent or team, or closing/re-opening the thread status. ### Request Body | Field | Type | Description | | ------------------- | ---------------- | ---------------------------------------------------- | | `status` | `string` | Set thread status: `open` or `closed` | | `assigned_agent_id` | `string \| null` | Assign thread to an agent ID (or `null` to unassign) | | `assigned_team_id` | `string \| null` | Assign thread to a team ID (or `null` to unassign) | | `client_name` | `string` | Custom display name for the customer | ### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/call-center/conversations/conv_a1b2c3d4e5f6" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "status": "closed", "assigned_agent_id": "agent_002", "assigned_team_id": "team_support_tier2" }' ``` ### Response `200 OK` ```json { "id": "conv_a1b2c3d4e5f6", "workspace_id": "yourcompany", "phone_number_id": "pn_99bf875464f1c096f46aff", "system_number": "+13074295456", "client_number": "+18647123123", "client_name": "John Doe", "contact_id": "con_650000000000000000000130", "assigned_agent_id": "agent_002", "assigned_team_id": "team_support_tier2", "status": "closed", "unread_count": 0, "last_message": { "id": "msg_682d82eb0ba1ffeace018f", "body": "Can I reschedule my appointment?", "direction": "inbound", "sender_type": "client", "sender_id": null, "created_at": "2026-09-27T10:00:00.000Z" }, "last_message_at": "2026-09-27T10:00:00.000Z", "last_read_at": "2026-09-27T10:05:00.000Z", "created_at": "2026-09-25T08:00:00.000Z", "updated_at": "2026-09-27T10:10:00.000Z" } ``` --- ## Realtime Notifications & Synchronization Firetell keeps agent devices and browser call center consoles synchronized across all inbound and outbound messages in real time. ### 1. Mobile Push Notifications (Inbound SMS) When a customer sends an SMS/MMS to a workspace number, Firetell delivers a high-priority alert notification to the assigned agents' mobile apps (iOS & Android) via **APNs (Alert)** and **FCM**. #### Agent Targeting & Routing Push notifications are routed automatically based on the conversation assignment: 1. **Assigned Agent**: If the conversation has an `assigned_agent_id`, only that agent's devices receive the push. 2. **Assigned Team**: If the conversation has an `assigned_team_id`, all active agents belonging to that team receive the push. 3. **DID Shared Teams (Fallback)**: If the conversation has no assigned agent or team, all active agents in the teams mapped to the phone number (`shared_teams_id`) receive the push notification. :::info **Outbound SMS Policy** Outbound messages sent by agents do **not** trigger mobile push notifications. This prevents notification spam and alert fatigue across teammates while ensuring agents only get mobile alerts when a customer needs attention. ::: #### Device Token Registration Mobile clients must register their regular notification token with the Agent API using [`POST /me/devices/notification-push-token`](/docs/rest-api/agent-api/account#register-notification-push-token): - **iOS**: Register the standard APNs alert token (`UNUserNotificationCenter`). - **Android**: Register the FCM registration token (`FirebaseMessaging`). #### Push Payload Specification ##### iOS (APNs Alert Payload) Delivered with `apns-push-type: alert` and `apns-priority: 10`: ```json { "aps": { "alert": { "title": "Alice Smith (+18647123123)", "body": "Hello, I need assistance with my reservation." }, "sound": "default", "badge": 3, "thread-id": "conv_a1b2c3d4e5f6" }, "event": "message.received", "workspace_id": "ws_123456789", "message_id": "msg_be52701819fa34a72ae414", "conversation_id": "conv_a1b2c3d4e5f6", "from_number": "+18647123123", "to_number": "+13074295456", "client_number": "+18647123123", "client_name": "Alice Smith", "body": "Hello, I need assistance with my reservation.", "unread_count": 3, "created_at": "2026-09-27T10:00:00.000Z" } ``` ##### Android/iOS (FCM Notification + Data Payload) Sent with priority `high`: ```json { "notification": { "title": "Alice Smith", "body": "Hello, I need assistance with my reservation." }, "android": { "notification": { "channel_id": "firetell_messages", "tag": "conv_a1b2c3d4e5f6" } }, "apns": { "payload": { "aps": { "thread-id": "conv_a1b2c3d4e5f6" } } }, "data": { "event": "message.received", "workspace_id": "ws_123456789", "message_id": "msg_be52701819fa34a72ae414", "conversation_id": "conv_a1b2c3d4e5f6", "from_number": "+18647123123", "to_number": "+13074295456", "client_number": "+18647123123", "client_name": "Alice Smith", "body": "Hello, I need assistance with my reservation.", "unread_count": "3", "created_at": "2026-09-27T10:00:00.000Z" } } ``` :::tip **Conversation Grouping (Threading)** - **iOS**: Uses `aps.thread-id` set to `conversation_id`. iOS natively groups incoming notifications into an expandable conversation stack in the Notification Center. - **Android**: Uses `android.notification.tag` set to `conversation_id`. If building custom notifications via `FirebaseMessagingService`, pass `data.conversation_id` into `NotificationCompat.Builder.setGroup(conversationId)` for native Android notification grouping. ::: --- ### 2. Mobile Silent / Data-Only Push Notifications (`conversation.updated`) When a conversation is modified — such as an agent marking messages as read, reassigning to another agent/team, or updating thread status (`open` / `closed`) — Firetell dispatches a **silent, data-only push notification** (`conversation.updated`) to target mobile devices. #### Purpose & Architecture Unlike incoming SMS alerts, silent pushes contain **no alert banner, no sound, and no popup**. They wake the mobile operating system in the background to execute code: - **Local Storage Synchronization**: Update local databases (e.g. SQLite, WatermelonDB, Core Data, Realm) with the new `unread_count`, `status`, and `assigned_agent_id`. - **Badge Count Synchronization**: Update or clear the application icon badge without requiring user interaction. - **Notification Dismissal**: Dismiss existing notification center banners for this conversation when the thread has been read or resolved on another device or web portal. #### Agent Targeting & Routing Delivered automatically to: 1. **Assigned Agent**: If the conversation is assigned, the assigned agent's devices receive the silent push. 2. **Assigned Team**: If assigned to a team, all active agents in that team receive the push. 3. **DID Shared Teams (Fallback)**: If unassigned, active agents in the teams assigned to the phone number receive the push. #### Push Payload Specification ##### iOS (APNs Background Silent Push) Delivered with `apns-push-type: background` and `apns-priority: 5`: ```json { "aps": { "content-available": 1 }, "event": "conversation.updated", "workspace_id": "ws_123456789", "id": "conv_a1b2c3d4e5f6", "conversation_id": "conv_a1b2c3d4e5f6", "phone_number_id": "pn_99bf875464f1c096f46aff", "system_number": "+13074295456", "client_number": "+18647123123", "client_name": "Alice Smith", "assigned_agent_id": "agent_001", "assigned_team_id": null, "status": "open", "unread_count": 0, "last_message": { "id": "msg_05f18045cba1452204774f", "body": "Hello Alice, I have updated your reservation for 7:00 PM.", "direction": "outbound", "sender_type": "agent", "sender_id": "agent_001", "created_at": "2026-09-28T03:39:00.000Z" }, "last_message_at": "2026-09-28T03:39:00.000Z", "last_read_at": "2026-09-28T03:40:00.000Z", "created_at": "2026-09-27T10:00:00.000Z", "updated_at": "2026-09-28T03:40:00.000Z", "timestamp": "2026-09-28T03:40:00.000Z" } ``` ##### Android / iOS (FCM Data-Only Payload) Sent with `priority: high` and **no `notification` block**: ```json { "android": { "priority": "high", "ttl": "60s" }, "apns": { "headers": { "apns-priority": "5", "apns-push-type": "background" }, "payload": { "aps": { "content-available": 1 } } }, "data": { "event": "conversation.updated", "workspace_id": "ws_123456789", "id": "conv_a1b2c3d4e5f6", "conversation_id": "conv_a1b2c3d4e5f6", "phone_number_id": "pn_99bf875464f1c096f46aff", "system_number": "+13074295456", "client_number": "+18647123123", "client_name": "Alice Smith", "assigned_agent_id": "agent_001", "assigned_team_id": "", "status": "open", "unread_count": "0", "last_message": "{\"id\":\"msg_05f18045cba1452204774f\",\"body\":\"Hello Alice, I have updated your reservation for 7:00 PM.\",\"direction\":\"outbound\",\"sender_type\":\"agent\",\"sender_id\":\"agent_001\",\"created_at\":\"2026-09-28T03:39:00.000Z\"}", "last_message_at": "2026-09-28T03:39:00.000Z", "last_read_at": "2026-09-28T03:40:00.000Z", "created_at": "2026-09-27T10:00:00.000Z", "updated_at": "2026-09-28T03:40:00.000Z", "timestamp": "2026-09-28T03:40:00.000Z" } } ``` :::tip **Handling Silent Pushes in Mobile Code** - **iOS APNs (`AppDelegate` / `UNUserNotificationCenter`)**: Handle via `application(_: didReceiveRemoteNotification:fetchCompletionHandler:)`. Inspect `payload["event"] == "conversation.updated"`, sync local database, update `UIApplication.shared.applicationIconBadgeNumber`, and call `completionHandler(.newData)`. - **Android/iOS FCM (`FirebaseMessagingService`)**: In `onMessageReceived(remoteMessage: RemoteMessage)`, check `remoteMessage.data["event"] == "conversation.updated"`. Because there is no `notification` block, the Android/iOS OS will not show any system notification. Update your local database (Room / SQLite / WatermelonDB) in the background. ::: --- ### 3. Browser Realtime Synchronization (SSE) Browser-based Call Center Portals subscribe to the [Realtime Events (SSE)](/docs/rest-api/agent-api/realtime-events) stream: ```http GET /stream?token=YOUR_AGENT_JWT ``` Message events are published to the agent's scoped unicast channels #### Supported Message Events | Event | Direction | Description | | ---------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `message.received` | Inbound | Emitted when a new message is received from a customer. Updates the chat thread and increases the unread count. | | `message.sent` | Outbound | Emitted when an agent sends an outbound message. Instantly updates the conversation for all teammates in the same team to prevent collision. | | `message.updated` | Status | Emitted when delivery status updates from the carrier (e.g., transitions from `queued` to `sent`, `delivered`, or `failed`). | | `conversation.updated` | State | Emitted when a conversation thread is updated (assigned/reassigned agent, status open/closed, or unread count reset upon reading). | #### Example JavaScript Listener ```javascript const eventSource = new EventSource( `https://{workspace_id}.firetell.app/stream?token=${encodeURIComponent(agentJwt)}`, ); // 1. Inbound customer message eventSource.addEventListener("message.received", (e) => { const { data } = JSON.parse(e.data); console.log("New inbound SMS:", data.body, "from:", data.from_number); // Prepend or append to conversation chat view, increment unread badge updateConversationInbox(data.conversation_id, data); }); // 2. Outbound message from colleague (Collision Avoidance) eventSource.addEventListener("message.sent", (e) => { const { data } = JSON.parse(e.data); console.log("Teammate sent SMS:", data.body, "by agent:", data.sender_id); // Reflect teammate's reply immediately without reloading appendOutboundMessageToThread(data.conversation_id, data); }); // 3. Delivery status update (Carrier acknowledgment) eventSource.addEventListener("message.updated", (e) => { const { data } = JSON.parse(e.data); console.log("Message status updated:", data.id, "to:", data.status); // Update tick icon to delivered / failed updateMessageStatusIndicator(data.id, data.status, data.error_message); }); // 4. Conversation state updated (Assignment / Status change / Read receipt) eventSource.addEventListener("conversation.updated", (e) => { const { data } = JSON.parse(e.data); console.log( "Conversation updated:", data.id, "assigned to:", data.assigned_agent_id, "status:", data.status, ); // Update conversation inbox thread item in real time syncConversationState(data.id, data); }); ``` --- # Teams View the teams you belong to and list your teammates. These endpoints are scoped — you can only see teams where you are a member. :::info Data Scoping Unlike the admin Teams API which returns all teams, the call center Teams API only returns teams where the authenticated agent is a member. ::: ## Endpoints | Method | Endpoint | Role Required | Description | | ------ | ----------------------------------------------------- | -------------------------- | --------------------------- | | `GET` | `/call-center/teams` | Any | List active workspace teams | | `GET` | `/call-center/teams/:id/agents` | Any | List teammates (if member) | | `PUT` | `/call-center/teams/:id/agents/:agent_id` | `leader` | Assign agent to team | | `DELETE` | `/call-center/teams/:id/agents/:agent_id` | `leader` | Remove agent from team | | `GET` | `/call-center/teams/:team_id/call-history` | `leader`, `supervisor` | Team call history | | `GET` | `/call-center/teams/:team_id/contacts` | `leader` | Team contacts | | `GET` | `/call-center/teams/:team_id/agents/states` | `leader`, `supervisor` | Team agent states | | `PUT` | `/call-center/teams/:team_id/agents/:username/state` | `leader`, `supervisor` | Force agent state | :::info Role checks (`leader`, `supervisor`) are based on the agent's **workspace-level role**, not a per-team assignment. Roles are managed by admins via the [Agents API](/docs/rest-api/workspace-api/agents). ::: ## Authentication All endpoints require `Bearer` JWT with `agent-api` audience. Some endpoints require specific team roles — see [Roles & Permissions](/docs/rest-api/agent-api/roles-permissions). --- ## List Active Workspace Teams ``` GET /call-center/teams ``` Retrieve a paginated list of all active teams in the workspace (useful for choosing target departments for call transfer). Note: To list only the teams you belong to, use `GET /me/teams`. ### Query Parameters | Parameter | Type | Default | Description | | --------- | ------ | ------- | ------------------------ | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `10` | Items per page (max: 100)| ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/teams?page=1&limit=10" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` ```json { "data": [ { "id": "tm_a1b2c3d4e5f6g7h8i9j0k1", "workspace_id": "my_workspace", "title": "Sales Team", "agent_count": 5, "created_at": "2026-01-10T10:00:00.000Z", "updated_at": "2026-07-01T12:00:00.000Z" }, { "id": "tm_x9y8z7w6v5u4t3s2r1q0p9", "workspace_id": "my_workspace", "title": "Support Team", "agent_count": 8, "created_at": "2026-02-15T08:00:00.000Z", "updated_at": "2026-06-20T09:30:00.000Z" } ], "meta": { "total": 2, "page": 1, "limit": 10, "total_pages": 1 } } ``` ### Team Object | Field | Type | Description | | -------------- | ------ | -------------------------------- | | `id` | string | Team ID | | `workspace_id` | string | Workspace identifier | | `title` | string | Team name | | `agent_count` | number | Number of agents in the team | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | --- ## List Teammates ``` GET /call-center/teams/:id/agents ``` List agents in a specific team, excluding yourself. Includes real-time presence state. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------ | | `id` | string | Team ID (e.g., `tm_a1b2c3...`) | ### Query Parameters | Parameter | Type | Default | Description | | --------- | ------ | ------- | ------------------------ | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `10` | Items per page (max: 100)| ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/call-center/teams/tm_a1b2c3d4e5f6g7h8i9j0k1/agents" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` ```json { "data": [ { "username": "jane.smith", "display_name": "Jane Smith", "avatar": "https://storage.firetell.app/avatars/jane.jpg", "state": "available", "role": "leader" }, { "username": "bob.nguyen", "display_name": "Bob Nguyen", "avatar": null, "state": "on_call", "role": "member" }, { "username": "alice.tran", "display_name": "Alice Tran", "avatar": null, "state": "offline", "role": "member" } ], "meta": { "total": 4, "page": 1, "limit": 10, "total_pages": 1 } } ``` ### Teammate Object | Field | Type | Description | | -------------- | ------------ | -------------------------------------------------------- | | `username` | string | Agent username | | `display_name` | string | Display name | | `avatar` | string\|null | Avatar URL | | `state` | string | Real-time presence: `available`, `incall`, `busy`, `offline` | | `role` | string | Agent's workspace-level role: `agent`, `leader`, or `supervisor` | :::info The `role` field is the agent's **account-level role**, not a per-team assignment. Roles are managed globally by workspace admins. ::: ### Error Responses | Status | Description | | ------ | ----------------------------------- | | `403` | You are not a member of this team | | `404` | Agent not found | :::tip Use the `state` field to show real-time availability indicators in your call center UI. Agents with `available` state are ready to receive calls. ::: --- ## Assign Agent to Team ``` PUT /call-center/teams/:id/agents/:agent_id ``` Add an agent to a team. **Requires `leader` role.** ### Path Parameters | Parameter | Type | Description | | ---------- | ------ | --------------------------------------- | | `id` | string | Team ID (e.g., `tm_a1b2c3...`) | | `agent_id` | string | Agent ID to assign (e.g., `ag_x1y2...`) | ### Request ```bash curl -X PUT "https://{workspace_id}.firetell.app/api/v1/call-center/teams/tm_a1b2c3d4e5f6g7h8i9j0k1/agents/ag_x1y2z3w4v5u6t7s8" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` Returns the created team-agent membership object. ### Error Responses | Status | Description | | ------ | ---------------------------------- | | `403` | Forbidden — requires `leader` role | | `404` | Team or agent not found | --- ## Remove Agent from Team ``` DELETE /call-center/teams/:id/agents/:agent_id ``` Remove an agent from a team. **Requires `leader` role.** ### Path Parameters | Parameter | Type | Description | | ---------- | ------ | --------------------------------------- | | `id` | string | Team ID (e.g., `tm_a1b2c3...`) | | `agent_id` | string | Agent ID to remove (e.g., `ag_x1y2...`) | ### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/call-center/teams/tm_a1b2c3d4e5f6g7h8i9j0k1/agents/ag_x1y2z3w4v5u6t7s8" \ -H "Authorization: Bearer YOUR_AGENT_JWT" ``` ### Response `200 OK` Returns the removed team-agent membership object. ### Error Responses | Status | Description | | ------ | ---------------------------------- | | `403` | Forbidden — requires `leader` role | | `404` | Team-agent membership not found | --- # VoIP Push Incoming Calls Firetell provides a low-latency **VoIP Push Notification** system that wakes mobile applications when an incoming call arrives for an agent. This ensures that agents never miss inbound calls, even when the mobile app is in the background, suspended, or terminated by the operating system. When an inbound call rings an agent, Firetell dispatches an asynchronous VoIP push notification via **Apple Push Notification service (APNs VoIP)** for iOS and **Firebase Cloud Messaging (FCM)** for Android. ```mermaid sequenceDiagram autonumber actor Caller participant Firetell as Firetell Platform participant Push as APNs / FCM Gateway actor Agent as Mobile App (iOS / Android) Caller->>Firetell: Inbound Call (Call Flow / Direct Ring) Firetell->>Push: Dispatch High-Priority VoIP Push Push->>Agent: Wakeup Push (call_token, ws_url, ring_timeout_secs) Note over Agent: Display Native CallKit / Telecom UI
Start ring_timeout_secs fallback timer Agent->>Firetell: User Answers -> Open WebSocket (ws_url) Agent->>Firetell: Send session.connect { token: call_token } (within 3s) Firetell->>Agent: session.connected & call.offer (SDP) Agent->>Firetell: call.answer (SDP) Firetell->>Agent: WebRTC Audio Connected ``` --- ## Prerequisites: Registering Device Push Tokens Before a mobile device can receive incoming call pushes, it must register its VoIP push notification token with the Firetell Agent API. ### 1. Obtain Push Token on Device - **iOS (Swift / Objective-C):** Use Apple's **PushKit** framework (`PKPushRegistry`) with the `.voIP` push type. - **Android (Kotlin / Java):** Use **Firebase Cloud Messaging** (`FirebaseMessaging.getInstance().token`). :::important **iOS Token Separation** On iOS, the **VoIP push token** (from `PKPushRegistry`) is separate from the **standard APNs token** (from `UNUserNotificationCenter`). You must register the VoIP token using the dedicated VoIP registration endpoint. ::: ### 2. Register VoIP Push Token Call the [`POST /me/devices/voip-push-token`](/docs/rest-api/agent-api/account#register-voip-push-notification-token) endpoint in the Agent API: ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/me/devices/voip-push-token" \ -H "Authorization: Bearer YOUR_AGENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "push_token": "a1b2c3d4e5f6...voip_device_token", "device_id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890", "platform": "ios", "os_version": "17.5.1", "app_version": "1.2.0", "device_model": "iPhone 15 Pro" }' ``` For full request parameters and response schemas, refer to the [Register VoIP Push Notification Token](/docs/rest-api/agent-api/account#register-voip-push-notification-token) documentation. :::info **Non-VoIP / Standard Push Notifications** To receive call cancellation and termination alerts (`call.canceled`, `call.ended`) that dismiss the background ringing screen when the caller hangs up or another agent answers, register the device's regular push token via [`POST /me/devices/notification-push-token`](/docs/rest-api/agent-api/account#register-notification-push-token). On iOS, supply your standard APNs or FCM token; on Android, the same FCM token can be supplied to both endpoints. ::: :::tip **Token Lifecycle & Logout** 1. **Re-registration:** Always invoke `POST /me/devices/voip-push-token` on every application cold launch or whenever PushKit / FCM issues a refreshed token. 2. **Logout:** When the agent signs out, invoke [`POST /me/logout`](/docs/rest-api/agent-api/account#logout). Firetell will invalidate the active session and remove the push token so the device no longer wakes up for inbound calls. ::: --- ## Push Payload Specification When an incoming call rings an agent, Firetell sends a push notification payload containing all metadata required to display the native incoming call UI and authenticate the call session. ### iOS (APNs VoIP Payload) On iOS, APNs delivers the payload to `PKPushRegistryDelegate`. The root payload contains the event metadata alongside an empty `aps: {}` dictionary: ```json { "aps": {}, "event": "call.ring", "workspace_id": "ws_123456789", "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "call_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "ws_url": "wss://ws.firetell.app/ws", "ring_timeout_secs": 30, "caller_number": "+12025550143", "caller_name": "Acme Corp Support", "caller_avatar": null, "callee_number": "1001", "callee_name": "Agent John Doe", "is_transfer": false, "transfer_reason": null, "is_video": false } ``` - **APNs Topic:** `{BUNDLE_ID}.voip` (e.g. `com.firetell.agent.voip`) - **APNs Priority:** `10` (immediate delivery) - **APNs Expiry:** Automatically set to `now + ring_timeout_secs` so expired calls are never delivered. --- ### Android (FCM Data Message) On Android, Firetell sends a high-priority **data-only message** (without a `notification` key), giving your application complete control to trigger a full-screen incoming call UI or Android Telecom `ConnectionService`: ```json { "event": "call.ring", "workspace_id": "ws_123456789", "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "call_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "ws_url": "wss://ws.firetell.app/ws", "ring_timeout_secs": "30", "caller_number": "+12025550143", "caller_name": "Acme Corp Support", "caller_avatar": "", "callee_number": "1001", "callee_name": "Agent John Doe", "is_transfer": "false", "transfer_reason": "", "is_video": "false" } ``` - **Android Priority:** `high` - **FCM TTL:** Set to `ring_timeout_secs * 1000` ms. - **Value types:** In FCM data messages, all values are encoded as strings. Parse `ring_timeout_secs` as an integer and `is_transfer`, `is_video` as booleans. --- ### Incoming Call Payload Reference (`call.ring`) | Field | Type | Description | | :------------------ | :-------------------- | :---------------------------------------------------------------------------------------------------------------- | | `event` | `string` | Always `"call.ring"`. Identifies this push as an incoming call alert. | | `workspace_id` | `string` | The Firetell workspace identifier handling the call. | | `call_id` | `string` | Unique identifier for the incoming call session. | | `call_token` | `string` | Short-lived JWT credential used to authenticate the signaling WebSocket. | | `ws_url` | `string` | The WebSocket endpoint of the signaling server assigned to this call. | | `ring_timeout_secs` | `number` \| `string` | Ring timeout in seconds determined by the Call Flow node (default `30`). Use this to run a local countdown timer. | | `caller_number` | `string` | Phone number or extension of the calling party. | | `caller_name` | `string` | Display name of the caller (e.g., contact name or VIP tag). | | `caller_avatar` | `string` \| `null` | Avatar URL of the calling party, or `null` / empty string if unavailable (e.g., external PSTN caller). | | `callee_number` | `string` | Destination phone number, queue number, or agent extension. | | `callee_name` | `string` | Display name of the callee or target team. | | `is_transfer` | `boolean` \| `string` | `true` if this incoming call is being transferred by another agent or supervisor. | | `transfer_reason` | `string` \| `null` | Reason for transfer, or `null` / empty string if direct call. | | `is_video` | `boolean` \| `string` | `true` if the incoming call includes video media (video call invitation), `false` for audio-only. | --- ### Call Cancellation & End Payloads (`call.canceled` & `call.ended`) When an active or ringing call ends before being answered (for example: the caller hangs up, the Call Flow timeout forwards the call elsewhere, or another agent in the team answers), Firetell dispatches a high-priority normal/background push notification (`call.canceled` or `call.ended`) to all ringing devices. This allows the mobile app to immediately dismiss its native incoming call screen (CallKit or Android full-screen intent) without waiting for `ring_timeout_secs` to expire. #### iOS (APNs Silent Background Notification) Delivered to `application(_:didReceiveRemoteNotification:fetchCompletionHandler:)` or your notification extension: ```json { "aps": { "content-available": 1 }, "event": "call.canceled", "workspace_id": "ws_123456789", "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "reason": "caller_hangup", "timestamp": "2026-09-11T07:30:00.000Z" } ``` - **APNs Topic:** `{BUNDLE_ID}` (main app bundle ID, without `.voip`) - **APNs Push Type:** `background` - **APNs Priority:** `5` (silent background wake) #### Android & iOS via Firebase (FCM Data Message) Delivered to `FirebaseMessagingService.onMessageReceived()` (Android) or Firebase iOS delegate: ```json { "event": "call.canceled", "workspace_id": "ws_123456789", "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "reason": "caller_hangup", "timestamp": "2026-09-11T07:30:00.000Z" } ``` - **Android Priority:** `high` - **FCM TTL:** `60000` (60 seconds) #### Cancel & End Payload Field Reference | Field | Type | Description | | :--- | :--- | :--- | | `event` | `string` | `"call.canceled"` (canceled while ringing) or `"call.ended"` (call terminated). | | `workspace_id` | `string` | The Firetell workspace identifier. | | `call_id` | `string` | Unique identifier of the call session to dismiss. | | `reason` | `string` \| `null` | Reason description (e.g. `"caller_hangup"`, `"timeout"`, `"answered_elsewhere"`, `"rejected"`). | | `timestamp` | `string` | ISO-8601 timestamp when the cancellation/end event occurred. | ## iOS Implementation (PushKit & CallKit) Apple requires all VoIP pushes on iOS 13+ to be reported immediately to **CallKit**. Failure to report the call to `CXProvider` inside `pushRegistry(_:didReceiveIncomingPushWith:for:completion:)` will cause the system to terminate your app and may revoke your app's VoIP push entitlement. ### 1. Initialize PushKit ```swift import PushKit import CallKit class VoIPPushManager: NSObject, PKPushRegistryDelegate { static let shared = VoIPPushManager() private var voipRegistry: PKPushRegistry? func registerVoIPPush() { let registry = PKPushRegistry(queue: DispatchQueue.main) registry.delegate = self registry.desiredPushTypes = [.voIP] self.voipRegistry = registry } func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { if type == .voIP { let token = pushCredentials.token.map { String(format: "%02.2hhx", $0) }.joined() // Send token to Firetell Agent API: POST /me/devices/voip-push-token DeviceService.registerVoipToken(token) } } } ``` ### 2. Handle Incoming VoIP Push with CallKit ```swift func pushRegistry( _ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void ) { guard type == .voIP, let dict = payload.dictionaryPayload as? [String: Any], let event = dict["event"] as? String, event == "call.ring", let callIdString = dict["call_id"] as? String, let callUUID = UUID(uuidString: callIdString) ?? UUID() else { completion() return } let callerName = dict["caller_name"] as? String ?? "Incoming Call" let callerNumber = dict["caller_number"] as? String ?? "Unknown" let ringTimeoutSecs = (dict["ring_timeout_secs"] as? Int) ?? 30 let callToken = dict["call_token"] as? String ?? "" let wsUrl = dict["ws_url"] as? String ?? "" // Save session info for when the agent answers CallSessionStore.shared.save(callId: callIdString, token: callToken, wsUrl: wsUrl) // Configure CallKit Update let isVideo = (dict["is_video"] as? Bool) ?? false let update = CXCallUpdate() update.remoteHandle = CXHandle(type: .phoneNumber, value: callerNumber) update.localizedCallerName = callerName update.hasVideo = isVideo update.supportsGrouping = false update.supportsUngrouping = false update.supportsHolding = true // Report incoming call to CallKit let provider = CallKitManager.shared.cxProvider provider.reportNewIncomingCall(with: callUUID, update: update) { [weak self] error in if error == nil { // Start local countdown fallback timer self?.startRingTimeoutTimer(uuid: callUUID, seconds: ringTimeoutSecs) } // MUST call completion handler completion() } } ``` ### 3. Handle Answer & Signaling Handshake When the user answers via the native CallKit screen, `CXProviderDelegate` receives `provider(_:perform: CXAnswerCallAction)`. You must connect to `ws_url` and authenticate using `call_token`: ```swift func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { guard let session = CallSessionStore.shared.activeSession else { action.fail() return } // Cancel ring timeout timer self.cancelRingTimeoutTimer() // 1. Open native WebSocket to ws_url WebSocketManager.shared.connect(url: session.wsUrl) { [weak self] isConnected in guard isConnected else { action.fail() return } // 2. Send session.connect event within 3 seconds let handshake: [String: Any] = [ "event": "session.connect", "data": ["token": session.callToken] ] WebSocketManager.shared.send(handshake) // 3. Complete CallKit action action.fulfill() } } ``` ### 4. Handle Decline / Reject via HTTP Endpoint When an agent declines an incoming call on the lock screen, `CXProviderDelegate` receives `provider(_:perform: CXEndCallAction)`. :::tip **Use HTTP POST Instead of WebSocket for Call Rejection** Opening a WebSocket connection and sending `session.connect` just to reject a call takes several seconds and risks being terminated by iOS when the app is suspended. Firetell provides a fast, dedicated HTTP endpoint that completes in **~50ms**: ```http POST https://{workspace_id}.firetell.app/api/v1/call-center/calls/{call_id}/reject Authorization: Bearer Content-Type: application/json { "reason": "declined" } ``` You can use the `call_token` directly from the VoIP push payload as the `Bearer` token — no need to retrieve or refresh your main account JWT. ::: ```swift func provider(_ provider: CXProvider, perform action: CXEndCallAction) { self.cancelRingTimeoutTimer() guard let session = CallSessionStore.shared.activeSession else { action.fulfill() return } // Fast HTTP reject using call_token from push payload let url = URL(string: "https://\(workspaceId).firetell.app/api/v1/call-center/calls/\(session.callId)/reject")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(session.callToken)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try? JSONSerialization.data(withJSONObject: ["reason": "declined"]) URLSession.shared.dataTask(with: request) { _, _, _ in CallSessionStore.shared.clear() action.fulfill() }.resume() } ``` ### 5. Handle Remote Cancellation / End (`call.canceled` & `call.ended`) If the caller hangs up before answer, or another agent in the queue answers the call, Firetell dispatches a silent background notification to your registered `notification_push_token` (via standard APNs or FCM). Implement `application(_:didReceiveRemoteNotification:fetchCompletionHandler:)` to dismiss the CallKit screen immediately: ```swift func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) { guard let event = userInfo["event"] as? String, event == "call.canceled" || event == "call.ended", let callId = userInfo["call_id"] as? String else { completionHandler(.noData) return } // Dismiss CallKit if this call is currently ringing if let session = CallSessionStore.shared.activeSession, session.callId == callId { let callUUID = UUID(uuidString: callId) ?? session.uuid CallKitManager.shared.cxProvider.reportCall( with: callUUID, endedAt: Date(), reason: .remoteEnded ) CallSessionStore.shared.clear() completionHandler(.newData) return } completionHandler(.noData) } ``` --- ## Android Implementation (Firebase Cloud Messaging) ### 1. Receive FCM Data Messages (`call.ring`, `call.canceled`, `call.ended`) In your `FirebaseMessagingService` implementation, handle incoming call alerts as well as background cancellation events: ```kotlin class FiretellFirebaseMessagingService : FirebaseMessagingService() { override fun onNewToken(token: String) { super.onNewToken(token) // Send updated token to Firetell Agent API: POST /me/devices/voip-push-token DeviceRepository.registerVoipPushToken(token) } override fun onMessageReceived(remoteMessage: RemoteMessage) { val data = remoteMessage.data val event = data["event"] when (event) { "call.ring" -> { val callId = data["call_id"] ?: return val callToken = data["call_token"] ?: "" val wsUrl = data["ws_url"] ?: "" val ringTimeoutSecs = data["ring_timeout_secs"]?.toIntOrNull() ?: 30 val callerNumber = data["caller_number"] ?: "Unknown" val callerName = data["caller_name"] ?: callerNumber val isVideo = data["is_video"]?.toBooleanStrictOrNull() ?: false // Launch incoming call UI (Full Screen Intent / Telecom ConnectionService) IncomingCallNotificationManager.showIncomingCall( context = this, callId = callId, callerName = callerName, callerNumber = callerNumber, callToken = callToken, wsUrl = wsUrl, ringTimeoutSecs = ringTimeoutSecs, isVideo = isVideo ) } "call.canceled", "call.ended" -> { val callId = data["call_id"] ?: return val reason = data["reason"] ?: "caller_hangup" // Dismiss ringing notification / close Telecom ConnectionService immediately IncomingCallNotificationManager.dismissIncomingCall( context = this, callId = callId, reason = reason ) } } } } ``` --- ## Call Ring Timeout & Dismissal Architecture Because mobile networks and app background states vary, Firetell employs a **three-tier dismissal architecture** to guarantee that ringing screens never linger indefinitely when a call is canceled or answered elsewhere: ### 1. Push-Based Background Dismissal (`call.canceled` & `call.ended`) When a call is abandoned by the caller, forwarded to another agent, or answered by a peer in a ring group, Firetell sends a high-priority normal/background push (`event: "call.canceled"` or `"call.ended"`). - **iOS:** Delivered via silent APNs background notification (`content-available: 1`) to dismiss CallKit via `.remoteEnded`. - **Android:** Delivered via high-priority FCM data message to dismiss the full-screen intent / Telecom call. This works even if the mobile app has no active WebSocket or SSE connection in the background. ### 2. Real-time Stream Dismissal (SSE & WebSocket) If the mobile client is in the foreground and maintains an active real-time connection ([Server-Sent Events stream](/docs/rest-api/agent-api/realtime-events) or open signaling WebSocket), the server immediately broadcasts `call.canceled` over the connection, allowing instant sub-10ms UI dismissal. ### 3. Autonomous Offline Fallback (`ring_timeout_secs`) If the mobile device loses all network connectivity or goes into airplane mode while ringing: 1. The device relies on the `ring_timeout_secs` parameter from the initial `call.ring` payload. 2. The local countdown timer expires (e.g. after 30 seconds). 3. The client automatically dismisses the incoming call UI locally with reason `.unanswered`. ``` VoIP Push Received ────────────────────> Local Timer Started (ring_timeout_secs) │ │ ├──────> Agent Answers: Stop timer & connect WS │ │ │ ├──────> Background Push `call.canceled` / `call.ended` ──┤──> End CallKit / Telecom UI immediately │ │ ├──────> Realtime SSE / WS `call.canceled` received ──────┤──> End CallKit / Telecom UI immediately │ │ └──────> Offline Timeout Reached: Auto-end CallKit UI <───┘ ``` :::tip **Dynamic Timeout per Node** The `ring_timeout_secs` value is not static; it is defined dynamically in the workspace's Call Flow Builder. A VIP queue node may specify a 15-second ring timeout per agent, while an overflow queue may use 45 seconds. Always rely on the `ring_timeout_secs` field from the push payload rather than hardcoding a timer. ::: --- ## Establishing Call Audio (WebRTC Signaling) After accepting the call via CallKit / Android Telecom, the client completes the call setup over the WebSocket protocol: 1. **Connect:** Establish a native WebSocket connection to the `ws_url` received in the push. 2. **Authenticate:** Send the `session.connect` message within **3 seconds** using `call_token`: ```json { "event": "session.connect", "data": { "token": "YOUR_CALL_TOKEN" } } ``` 3. **Receive SDP Offer:** The server responds with `session.connected` followed by `call.offer`: ```json { "event": "call.offer", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "sdp": { "type": "offer", "sdp": "v=0\r\no=- ..." } } } ``` 4. **Send SDP Answer:** The mobile client sets the remote description on its WebRTC peer connection, creates an answer, and sends `call.answer`: ```json { "event": "call.answer", "data": { "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "sdp": { "type": "answer", "sdp": "v=0\r\no=- ..." } } } ``` For comprehensive details on ICE candidate exchange, mute/hold operations, and hangup handling, see the [Calls & WebSocket Signaling](/docs/rest-api/agent-api/calls#native-websocket-signaling-protocol) reference. --- ## Best Practices & Troubleshooting ### APNs VoIP Entitlements & Background Modes - Ensure your iOS project has enabled **Voice over IP** background mode (`UIBackgroundModes` containing `voip`). - Ensure your App ID has the **Push Notifications** and **VoIP Push** capabilities active in the Apple Developer portal. ### APNs Environment Mismatch - Tokens generated in Xcode debug builds on physical devices belong to the **Sandbox** environment. - Tokens from TestFlight and App Store builds belong to the **Production** environment. - If an agent does not receive pushes, verify that the workspace configuration matches the deployment build environment (`sandbox` vs `production`). ### Ghost Rings Prevention - Firetell configures APNs notification expiration (`expiry`) and FCM `ttl` to exactly match `ring_timeout_secs`. If delivery is delayed by the cellular carrier beyond the ring window, APNs and FCM will drop the push rather than ringing a stale, dead call. - Always implement the local countdown timer using `ring_timeout_secs` as described in [Autonomous Offline Fallback](#3-autonomous-offline-fallback-ring_timeout_secs). --- # Client API Overview & Authentication The **Client API** allows you to integrate external, third-party end-users, mobile apps, or custom web portals with the Firetell voice platform. Unlike the Agent API (which is designed for call center agents registered in the database), the Client API allows you to authenticate _any_ custom username on-the-fly using a signed JSON Web Token (JWT). --- ## 1. Authentication & JWT Payload To connect a client device via the SDK, your backend must generate and sign a JWT using the **Secret Key** of one of your workspace API Keys. The JWT must contain the following claims: | Claim | Type | Required | Description | | :------------- | :------- | :------- | :------------------------------------------------------------------------------------------------ | | `iss` | `string` | **Yes** | The API Key SID (`sid-...`) used to sign the token. | | `aud` | `string` | **Yes** | Must be set to `client-api`. | | `sub` | `string` | **Yes** | Unique external username/ID (1-64 chars. Allowed: alphanumeric, `_`, `-`, `.`. No `@` or spaces). | | `domain` | `string` | **Yes** | Your workspace domain (e.g., `company.firetell.com`). | | `exp` | `number` | **Yes** | Expiration timestamp in seconds (Unix epoch time). | | `call_flow_id` | `string` | **Yes** | The outbound Call Flow ID that will handle outbound calls made by this client. | ### Payload Example ```json { "iss": "sid-650000000000000000000001", "aud": "client-api", "sub": "partner_customer_100", "domain": "acme.firetell.com", "exp": 1786022400, "call_flow_id": "cf_650000000000000000000100" } ``` :::important **Subject (sub) Validation Rules** To ensure secure and reliable connection handling and avoid formatting conflicts: - **Allowed characters:** Alphanumeric (`a-z`, `A-Z`, `0-9`), underscores (`_`), hyphens (`-`), and dots (`.`). - **Forbidden characters:** Special characters, whitespaces, and specifically the `@` symbol are strictly prohibited. - **Length:** The `sub` value must be between **1 and 64 characters** long. ::: --- ## 2. Dynamic Connection Quotas & Limits To protect the infrastructure from misuse or intentional DDoS attacks, the following limits are strictly enforced on Client API connections: - **Per-User Session Limit:** A single user ID (`sub`) can have a maximum of **5 concurrent active sessions** (e.g., open tabs or devices). - **Workspace Limit:** All Client API connections in a single workspace are capped at a total of **1000 concurrent active connections**. :::warning Requesting Limit Increases If your production application requires higher concurrent connection capacities, please contact our support team at [enterprise@firetell.com](mailto:enterprise@firetell.com) to request custom quotas. ::: --- ## 3. Real-Time Events (`call.ring`) Clients receive real-time updates (such as incoming call alerts) via a Server-Sent Events (SSE) stream at `/stream?token={JWT}`: - **Incoming Call Notification (`call.ring`):** When a call is routed to the client, the client device receives a `call.ring` event to trigger the ringer UI: ```json { "event": "call.ring", "call_id": "call_1770000000000", "call_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "ws_url": "wss://yourcompany.firetell.app/ws", "from": { "number": "agent_username", "name": "Your driver", "avatar": null }, "to": { "number": "partner_customer_100", "name": "Partner Customer Name" }, "timestamp": "2026-07-30T16:07:00.000Z" } ``` :::tip **VoIP Push for Mobile Apps** If you're building a native mobile app and need incoming calls to wake the app from the background, see the [VoIP Push Notifications](/docs/rest-api/client-api/voip-push) integration guide. You'll need to configure your own APNs/FCM credentials via the [Push Credentials API](/docs/rest-api/workspace-api/push-credentials). ::: --- ## 4. Outbound Calling Rule (`call_flow_id`) When a client initiates an outbound call via the Client SDK (by calling the `makeCall` API), the following rules apply: - **Strict Call Flow Routing:** The call **must** be routed through the outbound Visual Call Flow identified by the `call_flow_id` claim in the client's JWT token. - **Pre-requisite:** The call flow assigned to `call_flow_id` must be pre-configured in the Workspace Portal and have its `trigger_type` set to `outbound`. - **Security & Control:** This restriction ensures that end-user calls are securely monitored, formatted, recorded (if configured), and filtered according to your business's outbound calling rules before routing to carriers or external DIDs. --- ## 5. Integrating with Client SDK Once you have generated a valid Client JWT on your backend, you can pass it to our official frontend Client SDK to initialize the real-time event stream, handle WebRTC audio/video connections, and manage the calling lifecycle on the device. To install the SDK, import it, and see complete usage guides, please refer to the [Client SDK Integration Guide](https://developers.firetell.com/docs/sdks/overview). --- ## 6. REST API Reference If you are implementing your own WebRTC signaling/client wrapper without using the official Client SDK, you can call the make call REST API directly using your Client JWT. ### Initiate Outbound Call Initiate an outbound WebRTC call and receive the signaling WebSocket session details. - **Endpoint:** `POST /api/v1/call-center/calls/make` - **Headers:** - `Authorization: Bearer ` - `Content-Type: application/json` #### Request Body ```json { "to": "agent_username", "from": "", "type": "audio" } ``` #### Response Example (`201 Created`) ```json { "call_id": "call_1770000000000", "ws_url": "wss://yourcompany.firetell.app/call-session", "call_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` After receiving this response, the client must connect to `ws_url` and send a `session.connect` event with the `call_token` within 3 seconds to complete signaling authentication. --- # VoIP Push Notifications for Custom Apps This guide explains how to set up VoIP push notifications for **your own custom mobile application** so that your end-users (Client API users) can receive incoming calls even when your app is in the background, suspended, or terminated. ```mermaid sequenceDiagram autonumber actor Caller participant Backend as Your Backend participant Firetell as Firetell Platform participant Push as APNs / FCM actor User as Your Mobile App Note over Backend: Generate Client JWT
(aud: client-api, iss: sid-xxx) Backend->>User: Deliver JWT to app User->>Firetell: Register push token
POST /me/devices/voip-push-token Caller->>Firetell: Inbound Call Note over Firetell: Resolve push credential
by api_key_sid + platform Firetell->>Push: Dispatch VoIP Push
(using YOUR credentials) Push->>User: Wake app with call.ring payload User->>Firetell: Answer → WebSocket → WebRTC ``` --- ## How It Works When you integrate the Client API, your custom mobile app authenticates with a **Client JWT** signed using your API Key Secret. The `iss` claim in the JWT contains your **API Key SID** (`sid-xxx`). When a client device registers a push token via `POST /me/devices/voip-push-token`, Firetell records the `api_key_sid` from the JWT alongside the device's push token. When an incoming call arrives for that client, Firetell: 1. Looks up the device's `api_key_sid` and `platform` (ios/android). 2. Queries your **Push Credential** (configured in [Workspace Settings → Push Credentials](/docs/rest-api/workspace-api/push-credentials)) that matches the SID and platform. 3. Decrypts your stored APNs/FCM credentials. 4. Dispatches the VoIP push notification using **your own** APNs/FCM credentials. This means incoming call pushes will arrive through your app's own bundle ID and push certificate — no dependency on the Firetell Agent app. --- ## Setup Steps ### Step 1: Create an API Key Create an API Key in your workspace via [Console → Settings → API Keys](https://console.firetell.com) or the [API Keys endpoint](/docs/rest-api/workspace-api/push-credentials). Note the **SID** (`sid-xxx`) and **Secret Key**. ### Step 2: Configure Push Credentials Upload your APNs or FCM credentials via the [Push Credentials API](/docs/rest-api/workspace-api/push-credentials) or via **Console → Settings → Push Credentials**. #### iOS (APNs Token-Based / JWT) You'll need from the [Apple Developer Portal](https://developer.apple.com/account/resources/authkeys/): | Item | Description | | --------------------- | ------------------------------------------------------------------ | | **Key ID** | 10-character identifier of your APNs auth key | | **Team ID** | 10-character Apple Developer Team ID | | **Private Key (.p8)** | Downloaded `.p8` file content | | **Bundle ID** | Your app's bundle identifier (e.g., `com.yourcompany.app`) | | **Environment** | `sandbox` for development, `production` for App Store / TestFlight | ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/push-credentials" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "My iOS App - Production", "api_key_sid": "sid-650000000000000000000001", "platform": "ios", "apns_key_id": "ABC123DEFG", "apns_team_id": "TEAM123456", "apns_private_key": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMG...\n-----END PRIVATE KEY-----", "apns_bundle_id": "com.yourcompany.app", "apns_environment": "production" }' ``` :::caution **APNs Environment Mismatch** - **Xcode debug builds** on physical devices generate **sandbox** tokens. - **TestFlight** and **App Store** builds generate **production** tokens. - If users don't receive pushes, verify that `apns_environment` matches the build type. ::: #### Android (Firebase Cloud Messaging) You'll need from the [Firebase Console](https://console.firebase.google.com/): | Item | Description | | ------------------------ | ------------------------------------------------------------------------------------------ | | **Service Account JSON** | Firebase Admin SDK service account key (download from Project Settings → Service Accounts) | | **Package Name** | Your app's Android package name (optional but recommended) | ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/push-credentials" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "My Android App", "api_key_sid": "sid-650000000000000000000001", "platform": "android", "fcm_service_account": "{\"type\": \"service_account\", \"project_id\": \"...\", ...}", "fcm_package_name": "com.yourcompany.app" }' ``` ### Step 3: Generate Client JWT on Your Backend Your backend must generate a signed JWT for each client user. See the [Client API Overview](/docs/rest-api/client-api/overview) for the full JWT specification. ```json { "iss": "sid-650000000000000000000001", "aud": "client-api", "sub": "customer_12345", "domain": "yourcompany.firetell.com", "exp": 1786022400, "call_flow_id": "cf_650000000000000000000100" } ``` :::important The `iss` claim **must** match the `api_key_sid` you configured in the Push Credential. This is how Firetell resolves which APNs/FCM credentials to use when dispatching the push. ::: ### Step 4: Register Push Token from Mobile App After the mobile app obtains its push token (PushKit on iOS, FCM on Android), register it with Firetell using the Client JWT: ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/me/devices/voip-push-token" \ -H "Authorization: Bearer YOUR_CLIENT_JWT" \ -H "Content-Type: application/json" \ -d '{ "push_token": "a1b2c3d4e5f6...device_token", "device_id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890", "platform": "ios", "os_version": "17.5.1", "app_version": "1.0.0", "device_model": "iPhone 15" }' ``` | Parameter | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------------------------------ | | `push_token` | string | **Yes** | VoIP push token (PushKit on iOS, FCM token on Android) | | `device_id` | string | **Yes** | Unique device identifier (e.g., UUID) | | `platform` | string | **Yes** | `ios` or `android` | | `os_version` | string | No | Device OS version | | `app_version` | string | No | Your app version | | `device_model` | string | No | Device model name | :::tip Re-register the push token on every app launch or whenever the system issues a new token. PushKit and FCM may refresh tokens at any time. ::: :::info **Registering Regular Push Token for Cancellation (`call.canceled` & `call.ended`)** To allow your mobile app to automatically dismiss the ringing screen when a caller hangs up or the call times out, also register the device's regular push token via `POST /me/devices/notification-push-token`: - On **iOS**: Register your standard APNs device token (`notification_push_provider: "apns"`) or Firebase FCM token (`notification_push_provider: "fcm"`). - On **Android**: The FCM token registered via `POST /me/devices/voip-push-token` is automatically used for both incoming call pushes and cancellation pushes. ::: --- ## Push Payload When an incoming call arrives, Firetell dispatches a VoIP push with the following payload. The format is identical to the [Agent API VoIP Push payload](/docs/rest-api/agent-api/voip-push-incoming-call#push-payload-specification), but delivered using **your** APNs/FCM credentials. ### iOS (APNs VoIP) The APNs topic is set to `{your_bundle_id}.voip` (e.g., `com.yourcompany.app.voip`). ```json { "aps": {}, "event": "call.ring", "workspace_id": "ws_123456789", "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "call_token": "eyJhbGciOiJIUzI1NiIs...", "ws_url": "wss://ws.firetell.app/ws", "ring_timeout_secs": 30, "caller_number": "+12025550143", "caller_name": "Support Agent", "caller_avatar": null, "callee_number": "customer_12345", "callee_name": "Customer Name", "is_transfer": false, "transfer_reason": null, "is_video": false } ``` ### Android (FCM Data Message) ```json { "event": "call.ring", "workspace_id": "ws_123456789", "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "call_token": "eyJhbGciOiJIUzI1NiIs...", "ws_url": "wss://ws.firetell.app/ws", "ring_timeout_secs": "30", "caller_number": "+12025550143", "caller_name": "Support Agent", "caller_avatar": "", "callee_number": "customer_12345", "callee_name": "Customer Name", "is_transfer": "false", "transfer_reason": "", "is_video": "false" } ``` :::info In FCM data messages, all values are **strings**. Parse `ring_timeout_secs` as an integer and `is_transfer`, `is_video` as booleans in your app. ::: --- ### Call Cancellation & End Payloads (`call.canceled` & `call.ended`) When an incoming call is canceled before answer (caller hangup, call flow timeout, or answered elsewhere), Firetell dispatches a background push notification so your app immediately dismisses the ringing screen: #### iOS (APNs Silent Background Notification) - **Topic:** `{your_bundle_id}` (without `.voip`) - **Headers:** `apns-push-type: background`, `apns-priority: 5` - **Payload:** ```json { "aps": { "content-available": 1 }, "event": "call.canceled", "workspace_id": "ws_123456789", "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "reason": "caller_hangup", "timestamp": "2026-09-11T07:30:00.000Z" } ``` #### Android & iOS via Firebase (FCM Data Message) - **Priority:** `high` - **TTL:** `60s` - **Payload:** ```json { "event": "call.canceled", "workspace_id": "ws_123456789", "call_id": "cl_a1b2c3d4e5f6g7h8i9j0k1", "reason": "caller_hangup", "timestamp": "2026-09-11T07:30:00.000Z" } ``` Upon receiving `call.canceled` or `call.ended`, your mobile app should immediately call `CXProvider.reportCall(with:endedAt:reason: .remoteEnded)` on iOS or dismiss the full-screen intent / Telecom call on Android. For complete payload field reference and iOS/Android implementation code samples (PushKit, CallKit, Firebase Messaging), see the [VoIP Push Incoming Calls](/docs/rest-api/agent-api/voip-push-incoming-call) guide — the client-side handling is identical. --- ## Handling the Incoming Call After your app receives the VoIP push and displays the native incoming call UI, the call answer flow is: 1. **User taps Answer** → Your app opens a WebSocket to `ws_url`. 2. **Authenticate** → Send `session.connect` with the `call_token` within **3 seconds**: ```json { "event": "session.connect", "data": { "token": "YOUR_CALL_TOKEN" } } ``` 3. **Receive SDP Offer** → The server sends `call.offer` with the WebRTC SDP. 4. **Send SDP Answer** → Your app creates and sends `call.answer` with the local SDP. 5. **Audio Connected** → WebRTC media flows. For the complete WebSocket signaling protocol and WebRTC setup, see [Calls & WebSocket Signaling](/docs/rest-api/agent-api/calls#native-websocket-signaling-protocol). ### Declining / Rejecting the Call (HTTP Endpoint) If the user declines the incoming call on their device (e.g., lock-screen CallKit Decline or notification action), you do **not** need to open a WebSocket connection. Simply send an HTTP POST request: ```http POST https://{workspace_id}.firetell.app/api/v1/call-center/calls/{call_id}/reject Authorization: Bearer Content-Type: application/json { "reason": "declined" } ``` - **Authentication:** Pass the `call_token` from the VoIP push payload directly as the `Bearer` token (or use your client JWT). - **Speed & Reliability:** Takes ~50ms to complete, reliably executing before the mobile OS suspends the background app. - **Signaling:** The platform immediately sends SIP `486 Busy Here` to stop ringing the caller and forward or end the call. --- ## Ring Timeout & Dismissal Architecture Firetell provides a multi-layer dismissal mechanism to prevent ghost ringing on mobile devices: 1. **Background Push Dismissal (`call.canceled` / `call.ended`):** When the caller hangs up while ringing, the Call Flow forwards the call, or another device answers, Firetell immediately dispatches a normal/background push notification (`event: "call.canceled"` or `"call.ended"`). Your app receives this in the background and dismisses the CallKit / Telecom UI immediately. 2. **Autonomous Offline Fallback (`ring_timeout_secs`):** If the mobile device loses network connectivity or enters airplane mode while ringing, the local countdown timer started from `ring_timeout_secs` ensures that the incoming call UI dismisses automatically once the timer expires. --- ## iOS Entitlements Ensure your iOS project has: - **Background Modes** → `Voice over IP` enabled (`UIBackgroundModes` containing `voip`) - **Push Notifications** capability enabled - **VoIP Services** enabled in your Apple Developer App ID configuration --- ## Credential Rotation To rotate your APNs private key or FCM service account without downtime: 1. Generate a new key in the Apple Developer Portal / Firebase Console. 2. Update the push credential via `PATCH /push-credentials/:id` with the new key. 3. The change takes effect immediately — no app update needed. For the full CRUD API reference, see [Push Credentials API](/docs/rest-api/workspace-api/push-credentials). --- # REST API Overview The Firetell REST API allows you to programmatically manage your cloud communications infrastructure. ## Base URL Each workspace has its own API domain: ``` https://{workspace_id}.firetell.app/api/v1 ``` Replace `{workspace_id}` with your workspace's subdomain (found in **Console** → **Workspace Settings** → **General**). All endpoints are relative to this base URL. ## Request Format - All requests use **HTTPS** - Request bodies must be **JSON** with `Content-Type: application/json` - Authentication via `Authorization` header — `ApiKey sk-...` for Workspace API or `Bearer ` for Agent API (see [Authentication](/docs/getting-started/authentication)) ### Example Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/phone-numbers" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" ``` ## Response Format All responses return JSON. Successful responses include a `data` field: ```json { "data": [{ ... }], "meta": { "total": 100, "page": 1, "limit": 20, "total_pages": 5 } } ``` ## HTTP Status Codes | Code | Description | | ----- | ----------------------------------------- | | `200` | Success | | `201` | Resource created | | `400` | Bad request — invalid parameters | | `401` | Unauthorized — invalid or missing API key | | `403` | Forbidden — insufficient permissions | | `404` | Resource not found | | `429` | Rate limit exceeded | | `500` | Internal server error | ## Error Response Format When a request fails validation (HTTP `400`), the API returns a structured error body describing every invalid field: ```json { "statusCode": 400, "message": "name must not be empty", "error": "Bad Request", "errors": [ { "field": "name", "messages": ["name must not be empty", "name must be a string"] }, { "field": "address.city", "messages": ["city must be a string"] } ] } ``` | Field | Type | Description | | --- | --- | --- | | `message` | `string` | The first validation message — convenient for simple error display | | `errors` | `array` | Full list of all invalid fields | | `errors[].field` | `string` | Dot-notation path to the invalid field (e.g. `address.city` for nested objects) | | `errors[].messages` | `string[]` | All constraint violation messages for that field | :::tip Use `errors` to map validation feedback directly to form fields. The top-level `message` is kept for backward compatibility and quick display. ::: ## Pagination List endpoints support pagination with the following query parameters: | Parameter | Type | Default | Description | | --------- | ------ | ------- | ---------------------------- | | `page` | number | `1` | Page number | | `limit` | number | `20` | Items per page (max 100) | | `search` | string | — | Text search filter | | `sort` | string | — | Sort field | | `order` | string | `desc` | Sort order (`asc` or `desc`) | ## Rate Limiting All API endpoints are rate-limited to protect the platform and ensure fair usage. Rate limits are enforced per minute using a fixed-window counter backed by Firetell servers. ### Rate Limit Tiers | Tier | Limit | Endpoints | | ----------- | ----------- | ---------------------------------------------- | | **Default** | 100 req/min | All standard API endpoints | | **Auth** | 10 req/min | `/auth/login`, `/auth/verify`, `/auth/refresh` | | **Heavy** | 30 req/min | Upload, resource-intensive operations | | **SMS** | 30 req/min | Outbound SMS/MMS messaging (`/messages/send`) | Rate limits are applied **per API Key** when using API Key authentication, **per agent IP** when using agent JWT authentication (`agent-api`), or **per client identifier** when using client JWT authentication (`client-api`). :::note **Need higher limits?** Enterprise customers can request custom rate limits. Contact us at [enterprise@firetell.com](mailto:enterprise@firetell.com) with your workspace ID and use case. ::: ### Response Headers Every response includes rate limit information: | Header | Description | | ----------------------- | ----------------------------------------------- | | `X-RateLimit-Limit` | Maximum requests allowed in the window | | `X-RateLimit-Remaining` | Requests remaining in the current window | | `Retry-After` | Seconds to wait before retrying (only on `429`) | ### Rate Limit Exceeded (429) When the rate limit is exceeded, the API returns a `429 Too Many Requests` response: ```json { "statusCode": 429, "message": "Rate limit exceeded. Please try again later.", "retryAfter": 42 } ``` :::tip **Best Practices** - **Implement exponential backoff** when receiving `429` responses - **Cache responses** to reduce API calls - **Use webhooks** instead of polling for real-time updates - Monitor the `X-RateLimit-Remaining` header to preemptively slow down ::: ## Available Resources ### Workspace API Management endpoints for workspace administrators. Authenticated via `ApiKey`. | Resource | Description | | ----------------------------------------------------------- | ---------------------------- | | [Phone Numbers](/docs/rest-api/workspace-api/phone-numbers) | Manage virtual phone numbers | | [Call](/docs/rest-api/workspace-api/call) | Initiate and control calls | | [Call Flows](/docs/rest-api/workspace-api/call-flows) | Manage IVR call flows | | [Extensions](/docs/rest-api/workspace-api/extensions) | Manage internal extensions | | [SIP Trunks](/docs/rest-api/workspace-api/sip-trunks) | Manage SIP trunk connections | | [SIP Accounts](/docs/rest-api/workspace-api/sip-accounts) | Manage SIP accounts | | [Teams](/docs/rest-api/workspace-api/teams) | Manage agent teams | | [Agents](/docs/rest-api/workspace-api/agents) | Manage call center agents | | [Voice Agents](/docs/rest-api/workspace-api/voice-agents) | Manage AI voice agents | | [Audios](/docs/rest-api/workspace-api/audios) | Manage audio files | | [Webhooks](/docs/rest-api/workspace-api/webhooks) | Manage webhook endpoints | | [Push Credentials](/docs/rest-api/workspace-api/push-credentials) | Manage APNs/FCM push credentials for partner apps | | [Messages (SMS/MMS)](/docs/rest-api/workspace-api/messages) | Send SMS/MMS messages and query messaging logs | ### Agent API Call center portal endpoints for agents. Authenticated via agent JWT (`agent-api` audience). | Resource | Description | | ----------------------------------------------------------------- | ------------------------------------------------------------------- | | [Roles & Permissions](/docs/rest-api/agent-api/roles-permissions) | Team roles (member, leader, supervisor) & permission matrix | | [Authentication](/docs/rest-api/agent-api/auth) | Agent login, JWT verify, token refresh & password recovery | | [Account](/docs/rest-api/agent-api/account) | Profile, teams, avatar, devices & logout | | [Realtime Events](/docs/rest-api/agent-api/realtime-events) | Server-Sent Events (SSE) stream for presence, queue & notifications | | [Outbound Calls](/docs/rest-api/agent-api/calls) | Initiate outbound calls via REST & native WebSockets per-call | | [Contacts](/docs/rest-api/agent-api/contacts) | Manage contacts — create, search, lookup & delete | | [Address Books](/docs/rest-api/agent-api/address-books) | Organize contacts into named address books | | [Teams](/docs/rest-api/agent-api/teams) | View teams, teammates, agent states & team management | | [Phone Numbers](/docs/rest-api/agent-api/phone-numbers) | View phone numbers available to your teams | | [Call History](/docs/rest-api/agent-api/call-history) | View calls you handled as an agent | | [SMS Conversations](/docs/rest-api/agent-api/sms-conversations) | Persistent SMS conversation threads (inbox), messaging history & two-way chat | | [Call Supervision](/docs/rest-api/agent-api/call-supervision) | Listen, whisper, barge (supervisor) & call transfer (all agents) | | [VoIP Push Incoming Calls](/docs/rest-api/agent-api/voip-push-incoming-call) | Incoming call notifications via APNs VoIP & FCM with CallKit integration | ## Enterprise Services Need advanced capabilities beyond the standard platform? Firetell offers custom enterprise solutions: | Service | Description | | ---------------------------- | ----------------------------------------------------------------------------- | | **Custom Rate Limits** | Higher or tailored rate limits for your workload | | **Custom Base Domain** | Use your own domain instead of `*.firetell.app` (e.g., `api.yourcompany.com`) | | **Dedicated Infrastructure** | Isolated deployment for compliance and performance | | **Priority Support** | Dedicated technical support and SLA guarantees | To discuss enterprise requirements, contact us at [enterprise@firetell.com](mailto:enterprise@firetell.com). --- # Agents Agents are members assigned to handle calls in a call center setup. Each agent has login credentials to access the agent portal at `https://{workspace_id}.firetell.app`, where they can receive and make calls. The Agents API allows you to manage agent accounts, availability status, and routing preferences. ## Endpoints | Method | Endpoint | Description | | -------- | ---------------------- | ------------------------------ | | `GET` | `/agents` | List all agents | | `GET` | `/agents/:id` | Get agent details | | `GET` | `/agents/:id/teams` | List teams an agent belongs to | | `POST` | `/agents` | Create an agent | | `PATCH` | `/agents/:id` | Update agent settings | | `PATCH` | `/agents/:id/avatar` | Upload agent avatar | | `PATCH` | `/agents/:id/password` | Change agent password | | `DELETE` | `/agents/:id` | Delete an agent | ## The Agent Object | Field | Type | Description | | -------------- | -------------- | ------------------------------------------------- | | `id` | string | Unique agent identifier (prefixed with `ag_`) | | `username` | string | Agent login username (unique per workspace) | | `domain` | string | Agent's domain (e.g., `yourcompany.firetell.app`) | | `display_name` | string | Agent's display name | | `email` | string \| null | Agent's email address | | `avatar` | string \| null | URL to agent's avatar image | | `state` | string | Current availability state | | `is_active` | boolean | Whether the agent account is active | | `workspace_id` | string | Workspace identifier | | `country_code` | string | Two-letter ISO country code (e.g., `VN`, `US`) | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | ### Agent States | State | Description | | ----------- | ------------------------------------------ | | `available` | Agent is online and ready to receive calls | | `incall` | Agent is currently on a call | | `busy` | Agent is online but not accepting calls | | `offline` | Agent is not logged in | :::info The `state` field is a **real-time presence** value powered by the presence system. It is **not stored in the database**. An agent is considered `available` if they are reachable via **any** channel — either a live WebSocket connection or a registered VoIP Push token (for mobile apps in background). An agent transitions to `offline` only when **all** reachability sources are removed (e.g., all WebSocket sessions closed **and** push token unregistered via [logout](/docs/rest-api/agent-api/account#logout)). ::: :::info The `password` field is never returned in API responses. ::: --- ## List Agents ``` GET /agents ``` Retrieve a paginated list of all agents in the workspace. ### Authentication Requires `ApiKey` with any scope. ### Query Parameters | Parameter | Type | Default | Description | | -------------- | ------- | ------------ | ----------------------------------------------------------------------------------- | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `10` | Items per page (min: 1, max: 100) | | `search` | string | — | Search by display name, email, or username | | `sort_field` | string | `created_at` | Sort field. Allowed: `created_at`, `display_name`, `email`, `username`, `is_active` | | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | | `state` | string | — | Filter by agent state: `available`, `incall`, `busy`, `offline` | | `country_code` | string | — | Filter by country code (e.g., `VN`, `US`) | | `is_active` | boolean | — | Filter by active status | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/agents?page=1&limit=10&state=available" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "data": [ { "id": "ag_7f3a2b1c9d4e5f6a8b0c1d2e", "username": "johndoe", "domain": "yourcompany.firetell.app", "display_name": "John Doe", "email": "john@example.com", "avatar": "https://s3.amazonaws.com/bucket/yourcompany/avatars/abc123.webp", "state": "available", "is_active": true, "workspace_id": "yourcompany", "country_code": "US", "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-03-20T14:00:00.000Z" } ], "meta": { "total": 25, "page": 1, "limit": 10, "total_pages": 3 } } ``` --- ## Get Agent ``` GET /agents/:id ``` Retrieve details of a specific agent. ### Authentication Requires `ApiKey` with any scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------------------------------- | | `id` | string | The agent ID (e.g., `ag_7f3a2b1c9d4e5f6a8b0c1d2e`) | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/agents/ag_7f3a2b1c9d4e5f6a8b0c1d2e" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "id": "ag_7f3a2b1c9d4e5f6a8b0c1d2e", "username": "johndoe", "domain": "yourcompany.firetell.app", "display_name": "John Doe", "email": "john@example.com", "avatar": null, "state": "offline", "is_active": true, "workspace_id": "yourcompany", "country_code": "US", "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-01-15T08:30:00.000Z" } ``` ### Error Response ```json { "statusCode": 404, "message": "Agent not found", "error": "Not Found" } ``` --- ## List Agent Teams ``` GET /agents/:id/teams ``` Retrieve a paginated list of teams that a specific agent belongs to. ### Authentication Requires `ApiKey` with any scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ------------ | | `id` | string | The agent ID | ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------------ | --------------------------- | | `page` | number | `1` | Page number | | `limit` | number | `10` | Items per page (max: 100) | | `sort_field` | string | `created_at` | Sort field | | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/agents/ag_7f3a2b1c9d4e5f6a8b0c1d2e/teams" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "data": [ { "id": "te_a1b2c3d4e5f6a7b8c9d0e1f2", "name": "Support Team", "workspace_id": "yourcompany", "agent_count": 5, "created_at": "2026-01-10T09:00:00.000Z", "updated_at": "2026-02-15T10:30:00.000Z" }, { "id": "te_b2c3d4e5f6a7b8c9d0e1f2a3", "name": "Sales Team", "workspace_id": "yourcompany", "agent_count": 8, "created_at": "2026-01-12T11:00:00.000Z", "updated_at": "2026-03-01T08:00:00.000Z" } ], "meta": { "total": 2, "page": 1, "limit": 10, "total_pages": 1 } } ``` --- ## Create Agent ``` POST /agents ``` Create a new agent in the workspace. The agent will be able to log in to the agent portal and receive calls. ### Authentication Requires `ApiKey` with `full` scope. ### Request Body | Field | Type | Required | Description | | -------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `username` | string | ✅ | Login username. 3–30 characters, must start with a lowercase letter, only lowercase letters and numbers allowed. Must be unique per workspace. | | `password` | string | ✅ | Login password. 6–50 characters. | | `display_name` | string | ✅ | Agent display name. 3–50 characters. Only letters, spaces, and hyphens allowed. | | `email` | string | — | Email address. Must be unique per workspace. | | `avatar` | string | — | URL to avatar image. | | `country_code` | string | — | Two-letter ISO country code (e.g., `VN`). Defaults to workspace's country code. | | `is_active` | boolean | — | Whether the agent is active. Defaults to `true`. | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/agents" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "username": "janedoe", "password": "securePass123", "display_name": "Jane Doe", "email": "jane@example.com", "country_code": "VN" }' ``` ### Response ```json { "id": "ag_9a8b7c6d5e4f3a2b1c0d9e8f", "username": "janedoe", "domain": "yourcompany.firetell.app", "display_name": "Jane Doe", "email": "jane@example.com", "avatar": null, "state": "offline", "is_active": true, "workspace_id": "yourcompany", "country_code": "VN", "created_at": "2026-07-09T08:00:00.000Z", "updated_at": "2026-07-09T08:00:00.000Z" } ``` ### Error Responses **Duplicate username:** ```json { "statusCode": 409, "message": "username is exists", "error": "Conflict" } ``` **Duplicate email:** ```json { "statusCode": 409, "message": "email is exists", "error": "Conflict" } ``` **Invalid username format:** ```json { "statusCode": 400, "message": [ "Invalid username. Must start with a lowercase letter, contain only lowercase letters, numbers" ], "error": "Bad Request" } ``` --- ## Update Agent ``` PATCH /agents/:id ``` Update an existing agent's settings. Only include the fields you want to change. ### Authentication Requires `ApiKey` with `full` scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ------------ | | `id` | string | The agent ID | ### Request Body All fields are optional: | Field | Type | Description | | -------------- | ------- | --------------------------------------------------------------------------------------- | | `username` | string | New username. 3–30 characters, lowercase letters, numbers, and hyphens. Must be unique. | | `display_name` | string | New display name. 1–35 characters. Only letters, spaces, and hyphens. | | `email` | string | New email address. Must be unique per workspace. | | `avatar` | string | URL to new avatar image. | | `is_active` | boolean | Enable or disable the agent account. | | `country_code` | string | Two-letter ISO country code. | ### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/agents/ag_7f3a2b1c9d4e5f6a8b0c1d2e" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "display_name": "John D", "is_active": false }' ``` ### Response Returns the updated agent object: ```json { "id": "ag_7f3a2b1c9d4e5f6a8b0c1d2e", "username": "johndoe", "domain": "yourcompany.firetell.app", "display_name": "John D", "email": "john@example.com", "avatar": null, "state": "offline", "is_active": false, "workspace_id": "yourcompany", "country_code": "US", "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-07-09T09:15:00.000Z" } ``` ### Error Response ```json { "statusCode": 400, "message": "Agent not found or does not belong to this workspace", "error": "Bad Request" } ``` --- ## Upload Agent Avatar ``` PATCH /agents/:id/avatar ``` Upload or replace an agent's avatar image. The previous avatar (if any) will be automatically deleted from storage. ### Authentication Requires `ApiKey` with `full` scope. ### Request Body This endpoint uses `multipart/form-data` (not JSON). | Field | Type | Required | Description | | ------ | ---- | -------- | ------------------------------------------------------ | | `file` | file | ✅ | Image file (max **5 MB**). Must be an image MIME type. | ### Supported Formats | Format | MIME Type | | ------ | ------------ | | JPEG | `image/jpeg` | | PNG | `image/png` | | WebP | `image/webp` | | GIF | `image/gif` | ### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/agents/ag_7f3a2b1c9d4e5f6a8b0c1d2e/avatar" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -F "file=@/path/to/avatar.jpg" ``` ### Response Returns the updated agent object with the new `avatar` URL: ```json { "id": "ag_7f3a2b1c9d4e5f6a8b0c1d2e", "username": "johndoe", "domain": "yourcompany.firetell.app", "display_name": "John Doe", "email": "john@example.com", "avatar": "https://s3.amazonaws.com/bucket/yourcompany/avatars/new-uuid.webp", "state": "available", "is_active": true, "workspace_id": "yourcompany", "country_code": "US", "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-07-09T10:00:00.000Z" } ``` ### Error Responses **Missing file:** ```json { "statusCode": 400, "message": "file is required", "error": "Bad Request" } ``` **Invalid file type:** ```json { "statusCode": 400, "message": "Only images accepted", "error": "Bad Request" } ``` --- ## Change Agent Password ``` PATCH /agents/:id/password ``` Change an agent's login password. ### Authentication Requires `ApiKey` with `full` scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ------------ | | `id` | string | The agent ID | ### Request Body | Field | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------ | | `password` | string | ✅ | New password. 6–50 characters. | ### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/agents/ag_7f3a2b1c9d4e5f6a8b0c1d2e/password" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "password": "newSecurePass456" }' ``` ### Response Returns the agent object (prior to update): ```json { "id": "ag_7f3a2b1c9d4e5f6a8b0c1d2e", "username": "johndoe", "domain": "yourcompany.firetell.app", "display_name": "John Doe", "email": "john@example.com", "state": "offline", "is_active": true, "workspace_id": "yourcompany", "country_code": "US", "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-07-09T10:30:00.000Z" } ``` :::caution Changing an agent's password will not terminate active sessions. The agent will need to use the new password on their next login. ::: --- ## Delete Agent ``` DELETE /agents/:id ``` Permanently delete an agent from the workspace. The agent will be automatically removed from all teams they belong to, and team member counts will be updated accordingly. ### Authentication Requires `ApiKey` with `full` scope. :::caution This action is irreversible. The agent will lose access to the call center portal immediately. Any active calls will not be affected, but the agent will not be able to receive new calls. ::: ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ------------ | | `id` | string | The agent ID | ### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/agents/ag_7f3a2b1c9d4e5f6a8b0c1d2e" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response Returns the deleted agent object: ```json { "id": "ag_7f3a2b1c9d4e5f6a8b0c1d2e", "username": "johndoe", "domain": "yourcompany.firetell.app", "display_name": "John Doe", "email": "john@example.com", "avatar": null, "state": "offline", "is_active": true, "workspace_id": "yourcompany", "country_code": "US", "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-07-09T08:00:00.000Z" } ``` ### Error Response ```json { "statusCode": 400, "message": "Agent not found or does not belong to this workspace", "error": "Bad Request" } ``` --- # Audios Upload and manage audio files used for IVR prompts, hold music, voicemail greetings, and other call flow audio components. Firetell automatically processes uploaded audio into multiple sample rates optimized for telephony. ## Endpoints | Method | Endpoint | Description | | -------- | ------------- | ---------------------- | | `GET` | `/audios` | List all audio files | | `GET` | `/audios/:id` | Get audio file details | | `POST` | `/audios` | Upload an audio file | | `DELETE` | `/audios/:id` | Delete an audio file | ## The Audio Object | Field | Type | Description | | ------------- | ------ | --------------------------------------------------- | | `id` | string | Unique audio identifier (prefixed with `au_`) | | `title` | string | Display name of the audio file | | `url` | string | URL to the best quality audio sample | | `codec` | string | Audio codec (default: `pcm_mulaw`) | | `sample_rate` | number | Original sample rate in Hz (default: `8000`) | | `duration` | number | Duration in seconds | | `file_size` | number | File size in MB | | `mime_type` | string | MIME type (default: `audio/wav`) | | `samples` | array | Array of processed audio samples at different rates | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | ### Audio Sample Object Each audio file is automatically converted into multiple sample rates. The `samples` array contains: | Field | Type | Description | | ---------- | ------ | -------------------------------------------------- | | `rate` | number | Sample rate in Hz (e.g., `8000`, `16000`, `48000`) | | `url` | string | URL to this sample variant | | `key` | string | Storage key identifier | | `size` | number | File size in MB | | `duration` | number | Duration in seconds | --- ## List Audios ``` GET /audios ``` Retrieve a paginated list of all audio files in the workspace. ### Authentication Requires `ApiKey` with any scope. ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------------ | ------------------------------------------------------------------- | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `10` | Items per page (min: 1, max: 100) | | `search` | string | — | Search audio files by title | | `sort_field` | string | `created_at` | Sort field. Allowed: `created_at`, `title`, `duration`, `file_size` | | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/audios?page=1&limit=10&sort_field=created_at&sort_order=desc" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "data": [ { "id": "au_7f3a2b1c9d4e5f6a8b0c1d2e", "title": "Welcome Greeting", "url": "https://s3.amazonaws.com/bucket/workspace/audios/uuid/48000hz.wav", "codec": "pcm_mulaw", "sample_rate": 8000, "duration": 15, "file_size": 0.24, "mime_type": "audio/wav", "samples": [ { "rate": 48000, "url": "https://s3.amazonaws.com/bucket/workspace/audios/uuid/48000hz.wav", "key": "workspace/audios/uuid/48000hz.wav", "size": 1.44, "duration": 15 }, { "rate": 16000, "url": "https://s3.amazonaws.com/bucket/workspace/audios/uuid/16000hz.wav", "key": "workspace/audios/uuid/16000hz.wav", "size": 0.48, "duration": 15 }, { "rate": 8000, "url": "https://s3.amazonaws.com/bucket/workspace/audios/uuid/8000hz.wav", "key": "workspace/audios/uuid/8000hz.wav", "size": 0.24, "duration": 15 } ], "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-01-15T08:30:00.000Z" } ], "meta": { "total": 3, "page": 1, "limit": 10, "total_pages": 1 } } ``` --- ## Get Audio ``` GET /audios/:id ``` Retrieve details of a specific audio file. ### Authentication Requires `ApiKey` with any scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------------------------------- | | `id` | string | The audio ID (e.g., `au_7f3a2b1c9d4e5f6a8b0c1d2e`) | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/audios/au_7f3a2b1c9d4e5f6a8b0c1d2e" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "id": "au_7f3a2b1c9d4e5f6a8b0c1d2e", "title": "Hold Music - Jazz", "url": "https://s3.amazonaws.com/bucket/workspace/audios/uuid/48000hz.wav", "codec": "pcm_mulaw", "sample_rate": 8000, "duration": 120, "file_size": 1.92, "mime_type": "audio/wav", "samples": [ { "rate": 48000, "url": "https://s3.amazonaws.com/bucket/workspace/audios/uuid/48000hz.wav", "key": "workspace/audios/uuid/48000hz.wav", "size": 11.52, "duration": 120 }, { "rate": 16000, "url": "https://s3.amazonaws.com/bucket/workspace/audios/uuid/16000hz.wav", "key": "workspace/audios/uuid/16000hz.wav", "size": 3.84, "duration": 120 }, { "rate": 8000, "url": "https://s3.amazonaws.com/bucket/workspace/audios/uuid/8000hz.wav", "key": "workspace/audios/uuid/8000hz.wav", "size": 1.92, "duration": 120 } ], "created_at": "2026-02-10T14:22:00.000Z", "updated_at": "2026-02-10T14:22:00.000Z" } ``` ### Error Response ```json { "statusCode": 404, "message": "item not found", "error": "Not Found" } ``` --- ## Upload Audio ``` POST /audios ``` Upload a new audio file. The file is automatically processed and converted into multiple sample rates for optimal telephony playback. ### Authentication Requires `ApiKey` with `full` scope. :::caution Each workspace is limited to a maximum of **100 audio files**. Attempting to upload beyond this limit will return a `403 Forbidden` error. ::: ### Request Body This endpoint uses `multipart/form-data` (not JSON). | Field | Type | Required | Description | | ------- | ------ | -------- | -------------------------------------------- | | `file` | file | ✅ | Audio file to upload (max **3 MB**) | | `title` | string | ✅ | Display name for the audio (1–50 characters) | ### Supported Formats | Format | MIME Type | Notes | | ------ | ------------ | ---------------------------------------- | | WAV | `audio/wav` | Recommended — best quality for telephony | | MP3 | `audio/mpeg` | Widely compatible | | OGG | `audio/ogg` | Compressed alternative | Any file with a MIME type starting with `audio/` is accepted. ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/audios" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -F "file=@/path/to/welcome.wav" \ -F "title=Welcome Greeting" ``` ### Response ```json { "id": "au_9a8b7c6d5e4f3a2b1c0d9e8f", "title": "Welcome Greeting", "url": "https://s3.amazonaws.com/bucket/workspace/audios/uuid/48000hz.wav", "workspace_id": "yourcompany", "duration": 10, "file_size": 0.16, "samples": [ { "rate": 48000, "url": "https://s3.amazonaws.com/bucket/workspace/audios/uuid/48000hz.wav", "size": 0.96, "duration": 10, "key": "workspace/audios/uuid/48000hz.wav" }, { "rate": 16000, "url": "https://s3.amazonaws.com/bucket/workspace/audios/uuid/16000hz.wav", "size": 0.32, "duration": 10, "key": "workspace/audios/uuid/16000hz.wav" }, { "rate": 8000, "url": "https://s3.amazonaws.com/bucket/workspace/audios/uuid/8000hz.wav", "size": 0.16, "duration": 10, "key": "workspace/audios/uuid/8000hz.wav" } ], "created_at": "2026-07-09T08:00:00.000Z", "updated_at": "2026-07-09T08:00:00.000Z" } ``` ### Error Responses **Missing file:** ```json { "statusCode": 400, "message": "file is required", "error": "Bad Request" } ``` **Invalid file type:** ```json { "statusCode": 400, "message": "Only audio accepted", "error": "Bad Request" } ``` **File limit reached:** ```json { "statusCode": 403, "message": "You have reached the maximum limit of 100 files. Please contact support.", "error": "Forbidden" } ``` :::tip For best telephony quality, upload **WAV files** with a sample rate of **16000 Hz** or higher. Firetell will automatically generate optimized versions at 8000 Hz, 16000 Hz, and 48000 Hz. ::: --- ## Delete Audio ``` DELETE /audios/:id ``` Permanently delete an audio file and all its processed samples from storage. ### Authentication Requires `ApiKey` with `full` scope. :::caution This action is irreversible. The audio file and all associated sample rate variants will be permanently deleted. Ensure the audio is not actively used in any call flows before deleting. ::: ### Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------------------------------- | | `id` | string | The audio ID (e.g., `au_7f3a2b1c9d4e5f6a8b0c1d2e`) | ### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/audios/au_7f3a2b1c9d4e5f6a8b0c1d2e" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response Returns the deleted audio object: ```json { "id": "au_7f3a2b1c9d4e5f6a8b0c1d2e", "title": "Welcome Greeting", "url": "https://s3.amazonaws.com/bucket/workspace/audios/uuid/48000hz.wav", "codec": "pcm_mulaw", "sample_rate": 8000, "duration": 15, "file_size": 0.24, "mime_type": "audio/wav", "samples": [], "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-01-15T08:30:00.000Z" } ``` ### Error Response ```json { "statusCode": 404, "message": "item not found", "error": "Not Found" } ``` --- ## Audio Processing When you upload an audio file, Firetell automatically: 1. **Validates** the file format and sample rate 2. **Converts** the audio into multiple sample rate variants (8000 Hz, 16000 Hz, 48000 Hz) 3. **Stores** all variants in cloud storage for fast access 4. **Selects** the best quality sample as the primary URL The system intelligently selects the optimal audio variant during call playback based on the codec negotiated between endpoints, minimizing real-time transcoding and ensuring the highest quality audio delivery. :::info The minimum accepted sample rate is **8000 Hz**. Files with a lower sample rate will be rejected during upload. ::: --- # Call Flows Integration Call Flows define the automated routing logic for incoming and outgoing calls. They are built and managed visually using the graphical designer inside the **Firetell Console**. Since Call Flow management and structure manipulation are internal resources, no public REST API endpoints are exposed. Instead, developers can integrate custom database lookups and dynamic routing logic directly into their own servers using two key integration nodes: **Contact Lookup** and **JSON Call Action (JCA)**. --- ## Contact Lookup Node (`contact_lookup`) The **Contact Lookup** node allows Firetell to query your CRM, database, or API endpoint to resolve the caller's identity before routing the call. ### How it Works 1. **Internal Fallback (Optional)**: If the Request URL parameter is left empty, Firetell will automatically search the internal Firetell Call Center database for a contact matching the caller's phone number. 2. **Webhook Query**: If a Request URL is provided, Firetell issues an HTTP `POST` request to your configured webhook URL. The request body contains the call context: - `event` (string): `"call_flow.contact_lookup"`. - `call_id` (string): Unique call ID. - `caller_number` (string): The caller's E.164 phone number. - `destination_number` (string): The dialed inbound DID/phone number. - `workspace_id` (string): Workspace identifier. - `call_flow_id` (string): Call flow identifier. 3. Your server must respond with a `200 OK` JSON payload containing the contact's name. ### Expected JSON Response Firetell scans the response object for a display name using the following fields (in order of priority): - `display_name` - `name` - `fullName` - `display_title` - `title` Example response: ```json { "id": "customer_99182", "display_name": "John Doe", "email": "john.doe@example.com", "tier": "VIP" } ``` ### Telephony Behavior - **Caller ID Display Name**: If a name is found, Firetell injects it into the SIP headers. Softphones, WebRTC clients, and physical IP phones will display the contact's name instead of a raw phone number. - **Session Variable**: The raw JSON response is saved into the call's channel variable (`firetell_contact_data`). This data can be referenced later during the call flow or accessed inside subsequent JCA webhooks. --- ## JSON Call Action Node (`json_call_action`) The **JSON Call Action (JCA)** node allows your server to dynamically direct and control the call flow in real-time by returning a sequence of execution actions. ### How it Works 1. When a call reaches this node, Firetell sends an HTTP `POST` request to your configured backend webhook URL. 2. Firetell sends the call context (caller number, dialed destination, workspace ID, call flow ID, call ID, and contact lookup data) in the body. 3. Your server should process the business logic and respond with a `200 OK` containing a list of `actions` to execute next. ### HTTP POST Request Body ```json { "event": "call_flow.jca", "call_id": "call_e9a0c7eb1a744b55a0ee65", "workspace_id": "ws_abc123", "call_flow_id": "cf_xyz987", "caller_number": "14155550100", "destination_number": "18005550199", "contact_data": "{\"display_name\":\"John Doe\",\"tier\":\"VIP\"}" // Stringified response from preceding Contact Lookup node } ``` ### Expected Response Your server must return a JSON response containing an array of compiled call actions. Example response (Welcome user, and then bridge to an operator): ```json { "actions": [ { "action": "play", "params": { "audio_id": "au_welcome_vip", "answer_call": true, "continue_on_play": false } }, { "action": "to_operator", "params": { "phone_number": "+14155550123", "timeout": 45 } } ] } ``` --- ## Supported Webhook Actions The following action objects can be returned in the `actions` array: ### 1. Play Audio (`play`) Plays a pre-recorded audio file stored in the workspace cache. - `audio_id` (string, required): ID of the audio metadata resource. - `answer_call` (boolean, optional): If `false`, streams the audio as early media (SIP 183 Progress) without answering/billing the call. Default: `true`. - `continue_on_play` (boolean, optional): If `true`, runs the audio in the background and immediately moves to the next action. If `false`, blocks call flow until playback ends. Default: `false`. ### 2. Text to Speech (`tts`) Plays a text announcement using synthetic voice. - `text` (string, required): The text content to read (max 1000 characters). - `voice` (string, optional): Selected TTS voice identifier. - `speed` (number, optional): Playback speed rate. ### 3. Collect Digits (`ivr`) Waits for the caller to press DTMF keys. - `timeout` (number, optional): Time in seconds to wait for input. Default: `5`. - `max_digits` (number, optional): Maximum digits to collect (between 1 and 10). Default: `1`. ### 4. To Operator (`to_operator`) Bridges the call to an external phone number or direct SIP URI. - `to` (object, required): Destination call party object: - `number` (string, required): Destination E.164 phone number or SIP URI. - `name` (string, optional): Destination display name. - `from` (object, optional): Custom outbound caller ID object: - `number` (string, required): Outbound caller ID / DID number owned by your workspace. If omitted, the call flow's assigned DID is used. - `name` (string, optional): Caller ID name displayed on the callee's device. - `gateway_id` (string, optional): Specific outbound gateway ID to route the call through. - `timeout` (number, optional): Max ringing time in seconds before failover (max 300). Default: `30`. ### 5. Connect Agent (`connect_agent`) Routes the call to a registered Firetell agent. - `agent_id` (string, required): Unique agent ID. - `timeout` (number, optional): Max ringing time in seconds. Default: `30`. ### 6. Connect Team (`connect_team`) Bridges the call to a team queue. - `team_id` (string, required): Unique team ID. - `strategy` (string, optional): ACD strategy (`ring-all`, `round-robin`, `sequential`, `random`). Default: `ring-all`. - `timeout` (number, optional): Ring timeout in seconds. Default: `30`. ### 7. Voicemail (`voicemail`) Records a voicemail message from the caller and uploads it to the S3 workspace cloud. - `greeting_audio_id` (string, optional): Audio ID to play before recording. If omitted, plays default greeting _"Please leave your message after the tone."_ - `max_duration` (number, optional): Max message recording length in seconds. Default: `60`. - `beep` (boolean, optional): Play a tone beep sound before recording starts. Default: `true`. ### 8. Disconnect (`disconnect`) Ends the call session immediately. - `cause` (string, optional): SIP hangup cause (e.g. `NORMAL_CLEARING`). ### 9. Audio Stream Fork (`stream`) Fork the call audio in real-time to your own WebSocket server. Your server receives raw PCM audio and can optionally send audio back to be played to the caller. - `ws_url` (string, required): Destination WebSocket URL (`wss://` or `ws://`). Must be a publicly routable server. - `track` (string, optional): Which legs to fork — `both` (stereo, default), `inbound` (caller only), `outbound` (callee only). - `sample_rate` (number, optional): PCM sample rate in Hz — `8000` (default) or `16000`. - `bidirectional` (boolean, optional): If `true`, audio received from your WS server is played into the call in real-time. Default: `false`. - `answer_call` (boolean, optional): Answer the call before starting the stream. Default: `true`. - `headers` (object, optional): Custom headers for the WS handshake, e.g. `{ "Authorization": "Bearer token" }`. Max 10 entries. --- ## Use Cases & Real-World Examples ``` [Driver / Courier] (PSTN) │ │ 1. Calls Virtual Proxy Number (DID) ▼ [Firetell Platform] │ │ 2. Triggers JCA Webhook POST ▼ [Partner Server] ──► 3. Looks up active delivery order for caller_number │ │ 4. Responds with to_operator action ▼ [Firetell Platform] ──► 5. Bridges to customer's real phone number (Caller ID displayed is the Virtual Proxy Number) ``` #### Step 1: Firetell sends JCA Webhook ```json { "event": "call_flow.jca", "call_id": "call_9c8df104a8b27e", "workspace_id": "ws_ecommerce_app", "call_flow_id": "cf_number_masking", "caller_number": "14155550144", "destination_number": "18005550199", "contact_data": null } ``` #### Step 2: Your Server returns Dynamic Bridge Actions ```json { "actions": [ { "action": "play", "params": { "audio_id": "au_connecting_mask_notice", "answer_call": true, "continue_on_play": true } }, { "action": "to_operator", "params": { "to": { "number": "+14155550188", "name": "Customer John" }, "from": { "number": "+18005550199", "name": "Firetell Proxy" }, "timeout": 30 } } ] } ``` :::tip **Two-Way Privacy**: Both the caller (driver) and the callee (customer) only see the Virtual Proxy Number on their phone screens, ensuring strict compliance with user privacy regulations and personal data protection standards. ::: --- ### Use Case 2: VIP Customer Dynamic Priority Routing Route callers dynamically based on their customer tier in your CRM or database. #### Scenario: 1. When a call arrives, the **Contact Lookup** node queries your CRM with `caller_number`. 2. The **JSON Call Action** receives the caller's tier (`VIP` vs `Standard`). 3. If `VIP`, the call is immediately routed to their dedicated Account Manager. If `Standard`, it is routed to the general support queue. #### Your Webhook Response (VIP Customer): ```json { "actions": [ { "action": "play", "params": { "audio_id": "au_vip_welcome_greeting", "answer_call": true, "continue_on_play": false } }, { "action": "connect_agent", "params": { "agent_id": "agt_account_mgr_alex", "timeout": 45 } } ] } ``` #### Your Webhook Response (Standard Customer): ```json { "actions": [ { "action": "connect_team", "params": { "team_id": "team_general_support", "timeout": 30 } } ] } ``` --- ### Use Case 3: Expired Order or Invalid Access Handling If a customer or driver calls after an order has already been completed or canceled, your server can reject the connection gracefully without consuming unnecessary call duration or operator time. #### Your Webhook Response: ```json { "actions": [ { "action": "play", "params": { "audio_id": "au_order_completed_notice", "answer_call": true, "continue_on_play": false } }, { "action": "disconnect", "params": { "cause": "NORMAL_CLEARING" } } ] } ``` --- ### Use Case 4: After-Hours On-Call Duty Escalation Route after-hours emergency calls dynamically based on your company's live on-call schedule (e.g. Opsgenie, PagerDuty, or internal roster). #### Your Webhook Response: ```json { "actions": [ { "action": "play", "params": { "audio_id": "au_after_hours_emergency", "answer_call": true, "continue_on_play": false } }, { "action": "to_operator", "params": { "phone_number": "+14155550177", "timeout": 40 } } ] } ``` --- ### Use Case 5: Two-Factor Authentication & Voice OTP (2FA) Voice OTP delivery provides higher deliverability and enhanced protection compared to standard SMS. You can implement Voice OTP in two ways: #### Option A: Inbound Reverse Voice OTP (User Calls Verification Number) 1. The user requests 2FA verification on your website/app and is prompted to call your verification number from their registered phone. 2. The incoming call hits the Call Flow and triggers the **JSON Call Action** with `caller_number`. 3. Your server looks up the pending 2FA login session for that phone number and responds with a sequence of pre-recorded audio digit clips (e.g. `0`–`9`) or dynamic TTS. 4. Firetell answers, plays the greeting, sequentially plays each OTP digit, and hangs up immediately. ```json { "actions": [ { "action": "play", "params": { "audio_id": "au_greeting_2fa", "answer_call": true, "continue_on_play": false } }, { "action": "play", "params": { "audio_id": "au_digit_9", "continue_on_play": false } }, { "action": "play", "params": { "audio_id": "au_digit_2", "continue_on_play": false } }, { "action": "play", "params": { "audio_id": "au_digit_8", "continue_on_play": false } }, { "action": "play", "params": { "audio_id": "au_digit_4", "continue_on_play": false } }, { "action": "disconnect", "params": { "cause": "NORMAL_CLEARING" } } ] } ``` #### Option B: Outbound Voice OTP (Automated Call Broadcast) Your server proactively triggers an outbound call to the user's phone via the [Make Call API](/docs/rest-api/workspace-api/call#make-a-call) passing the exact same OTP action chain with `max_duration: 45`. --- ### Use Case 6: Real-Time Audio Streaming to Your Server Use the `stream` action to fork live call audio to your own WebSocket server during the call. This enables two powerful patterns: #### Option A: One-Way Monitoring (Transcription / Compliance) Stream audio to your server for real-time transcription, sentiment analysis, keyword spotting, or compliance recording — without interrupting the call. ``` [Caller] ──► [Firetell] ──► Your WS Server (receives PCM audio) │ │ │ └─ STT / Analytics / Compliance engine │ └──► [Agent / Operator] (normal call continues) ``` **JCA Webhook Response:** ```json { "actions": [ { "action": "play", "params": { "audio_id": "au_call_recorded_notice", "answer_call": true, "continue_on_play": false } }, { "action": "stream", "params": { "ws_url": "wss://analytics.your-server.com/call-stream", "track": "both", "bidirectional": false, "headers": { "Authorization": "Bearer YOUR_INTERNAL_TOKEN" } } } ] } ``` Your WebSocket server receives binary PCM audio frames continuously while the call is active. When the call ends, the connection closes automatically. --- #### Option B: Full-Duplex Custom AI Voice Bot Connect the caller to your own AI voice bot — built with any model or TTS provider of your choice. Your server receives the caller's speech and sends synthesized audio back in real-time, which Firetell plays directly into the call. ``` [Caller] ◄──► [Firetell] ◄──► Your WS Server │ ├─ STT (caller speech → text) ├─ LLM (generate response) └─ TTS (text → PCM audio → caller) ``` **JCA Webhook Response:** ```json { "actions": [ { "action": "stream", "params": { "ws_url": "wss://bot.your-server.com/voice-session", "track": "both", "sample_rate": 8000, "bidirectional": true, "headers": { "Authorization": "Bearer YOUR_BOT_TOKEN", "X-Session-Context": "support" } } } ] } ``` **WebSocket protocol your server must implement:** | Direction | Frame type | Content | | ---------------------- | ----------- | --------------------------------------------------------------------- | | Firetell → Your server | Binary | PCM audio at `sample_rate` Hz — caller's speech | | Your server → Firetell | Binary | PCM audio at `sample_rate` Hz — bot speech to play to caller | | Your server → Firetell | Text (JSON) | `{"type":"killAudio"}` — stop queued bot audio immediately (barge-in) | :::tip `call_id`, `workspace_id`, `caller_number`, `caller_name`, `destination_number`, and `stream_type=firetell_stream` are automatically appended to your `ws_url` as query parameters so your server can identify the call and caller without needing custom headers. ::: :::caution `bidirectional: true` requires the call to be answered before audio can flow in both directions. The `stream` action handles this automatically when `answer_call: true` (the default). ::: > 📦 **Sample code**: [Node.js](https://github.com/firetellcom/audio-stream-nodejs-example) · [Python](https://github.com/firetellcom/audio-stream-python-example) --- # Call The Call API allows you to initiate outbound server-to-server calls, control active calls with actions, and retrieve call history. ## Endpoints | Method | Endpoint | Description | | ------ | -------------------- | -------------------------------- | | `GET` | `/calls` | List call history | | `GET` | `/calls/:id/events` | Get call events timeline | | `POST` | `/calls/make` | Initiate an outbound call | | `PUT` | `/calls/:id/actions` | Update actions on an active call | ## List Call History Returns paginated call history for your workspace. Requires `ApiKey` with any scope. ```http GET /calls?page=1&limit=20 ``` ### Query Parameters | Parameter | Type | Default | Description | | ----------- | ------ | ------- | ------------------------------------------------------------------------------------------- | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `20` | Items per page (max: 100) | | `search` | string | — | Search by phone number or party name | | `direction` | string | `all` | Filter by call direction: `inbound`, `outbound`, `internal`, `all` | | `status` | string | `all` | Filter by call status: `answered`, `completed`, `active`, `failed`, `no_answer`, `busy`, etc.| | `from` | string | — | Filter by caller phone number or username | | `to` | string | — | Filter by destination phone number or username | | `sentiment` | string | `all` | Filter by AI sentiment analysis: `positive`, `neutral`, `negative`, `all` | ### Response ```json { "data": [ { "id": "call_01h900000000000000000001", "workspace_id": "ws_123456789", "number": "18005550199", "from": { "name": "Support Agent 1", "number": "agent1001" }, "to": { "name": "Customer", "number": "14155552671" }, "status": "completed", "direction": "outbound", "duration": 45, "billsec": 42, "recording": "ready", "transcription": "completed", "sentiment": "positive", "summary": "Customer reached out to inquire about smart call center solutions.", "action_items": ["Send Enterprise plan quotation", "Schedule system demo"], "created_at": "2026-07-09T10:00:00Z" } ], "meta": { "total": 100, "page": 1, "limit": 20, "total_pages": 5 } } ``` ## Make a Call Initiate an outbound server-to-server call. Requires `ApiKey` with `full` scope. ```http POST /calls/make ``` ### Request Body | Field | Type | Required | Description | | -------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `to` | object | ✅ | Destination party `{ number, name? }`. Number can be an E.164 phone number, extension, or agent username. | | `to.number` | string | ✅ | Destination phone number (E.164), extension, or username (3-30 chars). | | `to.name` | string | — | Display name for the callee (up to 64 chars). Fallbacks to `to.number` if omitted. | | `from` | object | — | Caller party `{ number, name? }`. Phone number (E.164) or agent username. | | `from.number` | string | ✅ | Caller ID phone number (E.164) or agent username (3-30 chars). | | `from.name` | string | — | Display name for the caller / CNAM (up to 64 chars). Fallbacks to `from.number` if omitted. | | `call_flow_id` | string | — | Call flow to execute on answer (mutually exclusive with `actions`). | | `max_duration` | number | — | Maximum call duration limit in seconds (1 to 7200). The call automatically hangs up after this duration. Default: based on credit & platform safety cap (7200s). | | `actions` | array | — | Array of actions to perform sequentially when the call connects (mutually exclusive with `call_flow_id`). | :::tip **Auto-Hangup on Flow Completion** When executing sequential actions (e.g. playing an audio announcement or TTS notification), Firetell **automatically hangs up the call (`NORMAL_CLEARING`)** as soon as the last action in the list finishes executing. You do not need to explicitly append a `hangup` or `disconnect` action unless you want to terminate earlier. ::: ### Call Actions Each action object has the following shape: | Field | Type | Required | Description | | ------------- | ------- | -------- | -------------------------------------------- | | `action` | string | ✅ | Action type (see below) | | `params` | object | — | Action-specific parameters | | `continue` | boolean | — | Whether to continue to the next action | | `auto_answer` | boolean | — | Auto-answer the call before executing action | ### Action Reference All server-to-server actions are unified with Call Flow builder actions for structural consistency. #### `play` — Audio Playback Play a pre-uploaded workspace audio file. | Param | Type | Required | Description | | ------------------ | ------- | -------- | ---------------------------------------------------------------------------------------- | | `audio_id` | string | ✅ | ID of the workspace audio asset to play | | `answer_call` | boolean | — | Whether to answer the call before playing. Default: `true` | | `continue_on_play` | boolean | — | If `true`, playback runs in background while the next action executes. Default: `false`. | #### `tts` — Text-to-Speech Play a synthesized speech message. | Param | Type | Required | Description | | ------- | ------ | -------- | -------------------------- | | `text` | string | ✅ | The text to read aloud | | `voice` | string | — | Synthetic voice identifier | | `speed` | number | — | Voice speaking speed rate | #### `ivr` — Wait for Digits Wait for the callee to press DTMF keys. | Param | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------------------- | | `timeout` | number | — | Time in seconds to wait for input. Default: `5` | | `max_digits` | number | — | Maximum digits to collect (between 1-10). Default: `1` | | `webhook_url` | string | ✅ | The POST destination URL where collected digits will be sent. | ##### Collected Callback Payload When DTMF collection completes (due to timeout, reaching max digits, or pressing `#`), Firetell dispatches a queued POST request to your `webhook_url` with the following JSON payload: ```json { "event": "call.ivr_collected", "data": { "workspace_id": "ws_xyz789", "call_id": "call_abc123", "digits": "123", "status": "completed" // "completed" or "timeout" } } ``` #### `to_operator` — Connect External Phone Bridge the call to an external phone number or direct SIP URI via carrier gateway. | Param | Type | Required | Description | | ------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `phone_number` | string | ✅ | Destination E.164 phone number or direct SIP URI | | `from` | string | — | Custom DID phone number (E.164) used as caller ID. Automatically resolves the matching carrier gateway to prevent cross-carrier blockage. Default: Call DID | | `timeout` | number | — | Timeout in seconds for the ringing phase. Default: `30` | | `ringback_audio_id` | string | — | Audio file ID to play as hold music while ringing | #### `connect_agent` — Connect Agent Bridge the call to a registered Firetell agent. | Param | Type | Required | Description | | ------------------- | ------ | -------- | ------------------------------------------------- | | `agent_id` | string | ✅ | Unique registered agent ID | | `timeout` | number | — | Timeout in seconds. Default: `30` | | `ringback_audio_id` | string | — | Audio file ID to play as hold music while ringing | #### `connect_team` — Connect Team Queue Bridge the call to a queue team. | Param | Type | Required | Description | | ------------------- | ------ | -------- | ------------------------------------------------- | | `team_id` | string | ✅ | Unique registered team ID | | `timeout` | number | — | Timeout in seconds. Default: `30` | | `ringback_audio_id` | string | — | Audio file ID to play as hold music while ringing | #### `connect_voice_agent` — Connect Voice AI Agent Bridge the call to a Realtime Conversational Voice AI Agent. | Param | Type | Required | Description | | ---------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------ | | `voice_agent_id` | string | ✅ | Unique Voice Agent ID (`va_...`) | | `version` | number | — | Specific agent version number (e.g. `1`). Defaults to the current active published version if omitted. | | `timeout` | number | — | Ringing timeout in seconds before fallback. Default: `30` | | `max_duration_seconds` | number | — | Maximum duration limit in seconds for the Voice AI session. Default: `300` | | `ringback_audio_id` | string | — | Audio file ID to play as hold music while establishing connection | #### `voicemail` — Voicemail Recording Record a voicemail message from the caller and automatically upload it to Firetell Cloud Storage. | Param | Type | Required | Description | | ------------------- | ------- | -------- | ------------------------------------------------------------------------ | | `greeting_audio_id` | string | — | Pre-recorded greeting file ID. Fallbacks to default greeting if omitted. | | `max_duration` | number | — | Max recording length in seconds. Default: `60` | | `beep` | boolean | — | Whether to play a tone beep before starting recording. Default: `true` | #### `disconnect` / `hangup` — Hang Up Terminate the active call channel. Either name can be used interchangeably. | Param | Type | Required | Description | | ------- | ------ | -------- | ---------------------------------------------- | | `cause` | string | — | SIP hangup reason (default: `NORMAL_CLEARING`) | #### `recording` — Call Session Recording Record the entire call session and automatically upload the file to S3 cloud storage. :::caution This action will be automatically ignored and skipped if the workspace plan quota does not authorize call recording (specifically, `call_recording` for app-to-phone external calls, or `call_internal_recording` for app-to-app internal calls). In this case, the call flow will seamlessly proceed to the next action. ::: | Param | Type | Required | Description | | ------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------- | | `format` | string | — | Audio format: `wav` (default) or `mp3`. | | `stereo` | boolean | — | Whether to record caller and callee on separate stereo channels (left/right). Default: `false`. | | `answer_call` | boolean | — | Whether to answer the call immediately (`true`, default). Set to `false` to defer recording until bridged. | #### `stream` — Audio Stream Fork Fork the call audio in real-time to your own WebSocket server. Useful for building custom voice bots, real-time translation, call analytics, or any scenario where your server needs raw audio access during the call. | Param | Type | Required | Description | | --------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `ws_url` | string | ✅ | Destination WebSocket URL (`wss://` or `ws://`). Must be a publicly accessible server. | | `track` | string | — | Audio legs to fork: `both` (default, stereo — caller on left, callee on right), `inbound` (caller only), `outbound` (callee only). | | `sample_rate` | number | — | PCM audio sample rate in Hz: `8000` (default, G.711 compatible) or `16000` (wideband). | | `bidirectional` | boolean | — | If `true`, audio sent from your WS server back to Firetell is played into the call channel in real-time (default: `false`). | | `answer_call` | boolean | — | Answer the call before starting the stream. Default: `true`. Set to `false` only if the call was already answered by a prior `answer` action — audio will not flow until the channel is answered, regardless of whether the WebSocket connection to your server is established. | | `headers` | object | — | Custom HTTP headers to include in the WebSocket handshake (e.g. `{ "Authorization": "Bearer token" }`). Max 10 headers. | :::info **Bidirectional Audio** When `bidirectional: true` your server receives the caller's raw PCM audio and can send audio frames back in real-time. Firetell injects those frames directly into the caller's audio channel. This lets you build a fully custom voice bot, live interpreter, or real-time TTS system using the transport of your choice. When `bidirectional: false` the `stream` action only *sends* audio to your server. The call flow continues normally, and your server can consume the audio stream for transcription, recording, or analytics without affecting the call. ::: :::caution **`bidirectional: true` requires the call to be answered.** Firetell only generates audio write frames (needed to play audio back to the caller) on channels that are in the `ACTIVE` (answered) state. If the call is not answered, your server will receive the connection but audio sent from your server **will not be played** to the caller. Always use `answer_call: true` (the default) when `bidirectional: true`, or ensure the call was answered by a prior `answer` action in the flow. **Security notice**: Only `wss://` and `ws://` URLs pointing to publicly routable IP addresses are accepted. Internal addresses (RFC 1918, loopback, link-local, cloud metadata) are blocked. ::: ## Use Cases & Examples ### 1. Simple Outbound Call — Connect to Agent Call an external number and connect directly to an agent when answered. ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/calls/make" \ -H "Authorization: ApiKey YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": { "number": "+84901234567", "name": "Customer Support" }, "to": { "number": "+84909876543", "name": "John Doe" }, "actions": [ { "action": "connect_agent", "params": { "agent_id": "agt_abc123", "timeout": 30 } } ] }' ``` ### 2. Outbound Call with Hold Music Call an external number and play hold music while the agent is ringing. The music stops automatically when the agent answers. ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/calls/make" \ -H "Authorization: ApiKey YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": { "number": "+84901234567", "name": "Customer Support" }, "to": { "number": "+84909876543", "name": "John Doe" }, "actions": [ { "action": "connect_agent", "params": { "agent_id": "agt_abc123", "timeout": 30, "ringback_audio_id": "aud_holdmusic" } } ] }' ``` :::tip The `ringback_audio_id` param works on `connect_agent`, `connect_team`, `connect_voice_agent`, and `to_operator`. Upload your hold music via the [Audio API](/docs/rest-api/workspace-api/audios) first to get the `audio_id`. ::: ### 3. Agent Cascade — Sequential Fallback Try agent 1 first. If they don't answer within 10 seconds, automatically try agent 2. If agent 2 also doesn't answer, route to the support team. ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/calls/make" \ -H "Authorization: ApiKey YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": { "number": "+84901234567", "name": "Customer Support" }, "to": { "number": "+84909876543", "name": "John Doe" }, "actions": [ { "action": "connect_agent", "params": { "agent_id": "agt_primary", "timeout": 10, "ringback_audio_id": "aud_holdmusic" } }, { "action": "connect_agent", "params": { "agent_id": "agt_backup", "timeout": 10, "ringback_audio_id": "aud_holdmusic" } }, { "action": "connect_team", "params": { "team_id": "team_support", "timeout": 30, "ringback_audio_id": "aud_holdmusic" } }, { "action": "voicemail", "params": { "greeting_audio_id": "aud_vm_greeting", "max_duration": 60 } } ] }' ``` :::info Actions execute **sequentially**. If a `connect_*` action fails (no answer, timeout, agent offline), the system automatically falls through to the next action in the array. ::: ### 4. Recorded Outbound Campaign Make an outbound call with recording enabled, play a TTS message, then hang up. Useful for automated notification campaigns. ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/calls/make" \ -H "Authorization: ApiKey YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": { "number": "+84901234567", "name": "Customer Support" }, "to": { "number": "+84909876543", "name": "John Doe" }, "actions": [ { "action": "recording", "params": { "format": "wav", "stereo": false } }, { "action": "tts", "params": { "text": "Hello, this is a unified notification from your system." } }, { "action": "disconnect" } ] }' ``` :::caution The `recording` action is automatically skipped if your workspace plan does not include the `call_recording` quota. The remaining actions will still execute normally. ::: ### 5. Automated Voice Broadcast / Order Notification Make an automated announcement call. The call plays the order status audio file and **automatically hangs up** as soon as the playback finishes. Setting `max_duration: 60` enforces an absolute safety cap of 60 seconds. ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/calls/make" \ -H "Authorization: ApiKey YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": { "number": "+84901234567", "name": "Firetell Delivery" }, "to": { "number": "+84909876543", "name": "John Doe" }, "max_duration": 60, "actions": [ { "action": "play", "params": { "audio_id": "au_41817c09bc4d27a405ec5a", "answer_call": true, "continue_on_play": false } } ] }' ``` ### 6. Outbound AI Voice Agent Call Initiate an automated outbound call directly connected to a Realtime Voice AI Agent (e.g. appointment confirmation, customer survey, or booking assistant). ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/calls/make" \ -H "Authorization: ApiKey YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": { "number": "+84901234567", "name": "Virtual Clinic Assistant" }, "to": { "number": "+84909876543", "name": "John Doe" }, "max_duration": 300, "actions": [ { "action": "connect_voice_agent", "params": { "voice_agent_id": "va_01h900000000000000000001", "version": 1, "timeout": 30 } } ] }' ``` ### 7. Mid-Call Action Update After a call is in `started` or `active` status, you can dynamically push new actions using the [Update Call Actions](#update-call-actions) endpoint. This is useful for interactive scenarios where your backend decides the next step based on external logic or customer input. ```bash # Step 1: Initiate the call curl -X POST "https://{workspace_id}.firetell.app/api/v1/calls/make" \ -H "Authorization: ApiKey YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": { "number": "+84901234567", "name": "Customer Support" }, "to": { "number": "+84909876543", "name": "John Doe" }, "actions": [ { "action": "play", "params": { "audio_id": "aud_welcome" } } ] }' # Returns: { "data": { "call_id": "call_abc123", ... } } # Step 2: After your backend logic, dynamically hand over the active call to a Voice AI Agent curl -X PUT "https://{workspace_id}.firetell.app/api/v1/calls/call_abc123/actions" \ -H "Authorization: ApiKey YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "actions": [ { "action": "connect_voice_agent", "params": { "voice_agent_id": "va_01h900000000000000000001" } } ] }' ``` ### 8. Custom Voice Bot via Audio Stream Fork the call audio in real-time to your own WebSocket server. Your server receives raw PCM audio and can send synthesized speech back into the call channel. This pattern is ideal when you want full control over the AI model, TTS provider, or business logic without using Firetell's built-in Voice Agent. > 📦 **Sample code**: [Node.js](https://github.com/firetellcom/audio-stream-nodejs-example) · [Python](https://github.com/firetellcom/audio-stream-python-example) ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/calls/make" \ -H "Authorization: ApiKey YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": { "number": "+84901234567", "name": "VoiceBot Support" }, "to": { "number": "+84909876543", "name": "John Doe" }, "actions": [ { "action": "stream", "params": { "ws_url": "wss://your-bot-server.example.com/call-stream", "track": "both", "sample_rate": 8000, "bidirectional": true, "headers": { "Authorization": "Bearer your-api-token", "X-Bot-Version": "v2" } } } ] }' ``` **WebSocket protocol on your server side:** | Direction | Frame type | Content | |-----------|----------|--------| | Firetell → Your server | Binary | PCM audio at `sample_rate` Hz (stereo if `track=both`: caller on left, callee on right) | | Your server → Firetell | Binary | Mono PCM audio at `sample_rate` Hz — played back to the caller in real-time (requires `bidirectional: true`) | | Your server → Firetell | Text (JSON) | `{"type":"killAudio"}` — instructs Firetell to immediately stop any queued audio playback (barge-in) | :::tip `call_id`, `workspace_id`, `caller_number`, `caller_name`, `destination_number`, and `stream_type=firetell_stream` are automatically appended to your `ws_url` as query parameters so your server can identify the call and caller without needing custom headers. ::: ### Response All `POST /calls/make` requests return: ```json { "data": { "call_id": "call_abc123", "status": "created", "direction": "outbound", "from": { "name": "Customer Support", "number": "+84901234567" }, "to": { "name": "John Doe", "number": "+84909876543" }, "job_id": "job_xyz789", "job_status": "queued" } } ``` ## Update Call Actions Update the actions on an active call. Requires `ApiKey` with `full` scope. The call must be in `started` or `active` status, and the media channel must be ready. ```http PUT /calls/:id/actions ``` ### Request Body | Field | Type | Required | Description | | --------- | ----- | -------- | --------------------------------------------------------------------------------------------------------------------------- | | `actions` | array | ✅ | Array of call action objects (minimum 1). See [Action Reference](#action-reference) above for available actions and params. | ### Example Request ```bash curl -X PUT "https://{workspace_id}.firetell.app/api/v1/calls/call_abc123/actions" \ -H "Authorization: ApiKey YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "actions": [ { "action": "tts", "params": { "text": "Your order has been confirmed. Goodbye!" }, "continue": true }, { "action": "disconnect" } ] }' ``` ### Response ```json { "data": { "call_id": "call_abc123", "queued": true, "action_count": 2 } } ``` ## Get Call Events Retrieve the event timeline for a specific call, sorted chronologically. Requires `ApiKey` with any scope. ```http GET /calls/:id/events?page=1&limit=50 ``` ### Response ```json { "data": [ { "id": "evt_abc123", "call_id": "call_abc123", "event": "call.created", "direction": "outbound", "sip_code": null, "to": { "number": "+84909876543", "name": "John Doe" }, "created_at": "2026-07-09T10:00:00Z" }, { "id": "evt_abc124", "call_id": "call_abc123", "event": "call.ringing", "direction": "outbound", "sip_code": null, "to": { "number": "+84909876543", "name": "John Doe" }, "created_at": "2026-07-09T10:00:02Z" }, { "id": "evt_abc125", "call_id": "call_abc123", "event": "call.answered", "direction": "outbound", "sip_code": 200, "to": { "number": "+84909876543", "name": "John Doe" }, "created_at": "2026-07-09T10:00:05Z" }, { "id": "evt_abc126", "call_id": "call_abc123", "event": "call.ended", "direction": "outbound", "sip_code": 200, "hangup_cause": "NORMAL_CLEARING", "created_at": "2026-07-09T10:00:50Z" } ], "meta": { "total": 4, "page": 1, "limit": 50, "total_pages": 1 } } ``` ### Event Types | Event | Description | | ------------------------------ | ------------------------------------------------- | | `call.created` | Call channel created | | `call.answered` | Call was answered | | `call.bridged` | Two call legs bridged together | | `call.ended` | Call hangup completed | | `call.destroyed` | Call channel destroyed | | `call.dtmf` | DTMF digit received | | `call.playback_started` | Audio playback started | | `call.playback_stopped` | Audio playback stopped | | `call.recording.started` | Call recording stream started (In-Call SSE) | | `call.recording.completed` | Call recording stream completed (In-Call SSE) | | `call.recording.ready` | Call recording uploaded and ready (Webhook & SSE) | | `call.transcription.dialogue` | Live speech transcription utterance (In-Call SSE) | | `call.transcription.completed` | Call transcription with NER entities (Webhook) | --- # Extensions Extensions allow you to create 3 to 6-digit internal phone numbers (e.g. `1001`, `1002`) for agents, teams, SIP accounts, or voice AI agents within your workspace, enabling direct internal calling, IVR transfers, and call routing. ## Endpoints | Method | Endpoint | Description | | -------- | ----------------- | ---------------------- | | `GET` | `/extensions` | List all extensions | | `GET` | `/extensions/:id` | Get extension details | | `POST` | `/extensions` | Create an extension | | `PATCH` | `/extensions/:id` | Update an extension | | `DELETE` | `/extensions/:id` | Delete an extension | --- ## The Extension Object | Field | Type | Description | | ------------------ | -------- | -------------------------------------------------------------------- | | `id` | string | Unique extension identifier (prefixed with `ex_`) | | `workspace_id` | string | Workspace identifier | | `extension_number` | string | 3 to 6-digit internal extension number (e.g. `1001`, `1002`) | | `title` | string | Display label/name for the extension | | `destination` | string | Destination target (`agent`, `team`, `sip-account`, `voice-agent`) | | `agent_id` | string | Assigned agent ID (required if `destination: agent`) | | `team_id` | string | Assigned team ID (required if `destination: team`) | | `sip_account_id` | string | Assigned SIP account ID (required if `destination: sip-account`) | | `voice_agent_id` | string | Assigned voice agent ID (required if `destination: voice-agent`) | | `agent` | object | Populated agent object (when `destination: agent`) | | `team` | object | Populated team object (when `destination: team`) | | `sip_account` | object | Populated SIP account object (when `destination: sip-account`) | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | --- ## List Extensions ``` GET /extensions ``` Retrieve a paginated list of extensions in the workspace. ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------------------ | -------------------------------------------------------- | | `page` | number | `1` | Page number | | `limit` | number | `10` | Items per page (max: 100) | | `search` | string | — | Search by extension title or number | | `sort_field` | string | `created_at` | Sort field (`extension_number`, `created_at`, `title`) | | `sort_order` | string | `desc` | Sort direction (`asc`, `desc`) | ### Response `200 OK` ```json { "data": [ { "id": "ex_987654321", "workspace_id": "ws_company", "extension_number": "1001", "title": "Sales Support Line", "destination": "agent", "agent_id": "ag_abc123", "agent": { "id": "ag_abc123", "username": "john_doe", "display_name": "John Doe", "email": "john@example.com", "avatar": "https://cdn.firetell.app/avatars/john.png" }, "created_at": "2026-07-21T14:45:00.000Z", "updated_at": "2026-07-21T14:45:00.000Z" }, { "id": "ex_123456789", "workspace_id": "ws_company", "extension_number": "1002", "title": "Support Queue", "destination": "team", "team_id": "team_xyz789", "team": { "id": "team_xyz789", "title": "Customer Support", "agent_count": 5 }, "created_at": "2026-07-21T14:50:00.000Z", "updated_at": "2026-07-21T14:50:00.000Z" } ], "meta": { "total": 2, "page": 1, "limit": 10, "total_pages": 1 } } ``` --- ## Get Extension Details ``` GET /extensions/:id ``` Retrieve details of a single extension by its ID. ### Response `200 OK` ```json { "id": "ex_987654321", "workspace_id": "ws_company", "extension_number": "1001", "title": "Sales Support Line", "destination": "agent", "agent_id": "ag_abc123", "agent": { "id": "ag_abc123", "username": "john_doe", "display_name": "John Doe", "email": "john@example.com" }, "created_at": "2026-07-21T14:45:00.000Z", "updated_at": "2026-07-21T14:45:00.000Z" } ``` --- ## Create Extension ``` POST /extensions ``` Create a new internal extension and assign its routing destination. ### Request Body ```json { "title": "Sales Support Line", "extension_number": "1001", "destination": "agent", "agent_id": "ag_abc123" } ``` ### Response `201 Created` ```json { "id": "ex_987654321", "workspace_id": "ws_company", "extension_number": "1001", "title": "Sales Support Line", "destination": "agent", "agent_id": "ag_abc123", "created_at": "2026-07-21T14:45:00.000Z", "updated_at": "2026-07-21T14:45:00.000Z" } ``` ### Error Responses - `400 Bad Request`: Invalid payload or missing required `agent_id`, `team_id`, or `sip_account_id` for the selected destination. - `409 Conflict`: The `extension_number` already exists within this workspace. --- ## Update Extension ``` PATCH /extensions/:id ``` Update an extension's number, title, or routing destination. ### Request Body ```json { "title": "Tier 1 Sales Support", "extension_number": "1005", "destination": "team", "team_id": "team_sales" } ``` ### Response `200 OK` ```json { "id": "ex_987654321", "workspace_id": "ws_company", "extension_number": "1005", "title": "Tier 1 Sales Support", "destination": "team", "team_id": "team_sales", "updated_at": "2026-07-21T15:00:00.000Z" } ``` --- ## Delete Extension ``` DELETE /extensions/:id ``` Permanently remove an extension. ### Response `200 OK` ```json { "id": "ex_987654321", "workspace_id": "ws_company", "extension_number": "1005", "title": "Tier 1 Sales Support", "destination": "team" } ``` --- # Messages (SMS/MMS) Send outbound SMS and MMS messages, retrieve message logs, and inspect delivery statuses using your workspace phone numbers. Firetell handles carrier dispatch, delivery receipts (DLR), and real-time event updates via [Webhooks](/docs/webhooks/overview) and [Server-Sent Events (SSE)](/docs/rest-api/agent-api/realtime-events). --- ## Endpoints | Method | Endpoint | Description | | ------ | ---------------- | -------------------------------------------------- | | `POST` | `/messages/send` | Send an outbound SMS or MMS message | | `GET` | `/messages` | List message history with filters and pagination | | `GET` | `/messages/:id` | Retrieve detailed information for a single message | --- ## The Message Object | Field | Type | Description | | ----------------- | ---------- | ------------------------------------------------------------------------------------- | | `id` | `string` | Unique message identifier prefixed with `msg_` (e.g. `msg_05f18045cba1452204774f`) | | `workspace_id` | `string` | Workspace identifier (e.g. `yourcompany`) | | `phone_number_id` | `string` | Identifier of the workspace phone number used (`pn_...`) | | `from` | `string` | Sender phone number in E.164 format (e.g. `+13074295456`) | | `to` | `string` | Recipient phone number in E.164 format (e.g. `+18647123123`) | | `body` | `string` | Text content of the message (up to 1,600 characters) | | `direction` | `string` | Direction of the message: `outbound` or `inbound` | | `status` | `string` | Current delivery status (see [Message Statuses](#message-statuses)) | | `cost` | `number` | Total cost charged in USD including platform markup (or `null` if inbound or pending) | | `media_urls` | `string[]` | Array of media attachment URLs for MMS messages | | `error_code` | `string` | Error code returned by carrier if sending/delivery failed (e.g. `40001`, or `null`) | | `error_message` | `string` | Human-readable error message explaining failure reason (or `null`) | | `created_at` | `string` | ISO 8601 creation timestamp | | `updated_at` | `string` | ISO 8601 last update timestamp | ### Message Statuses | Status | Direction | Description | | ------------- | ---------- | ------------------------------------------------------------------------------- | | `queued` | `outbound` | Message has been accepted by Firetell and queued for carrier delivery. | | `sending` | `outbound` | Message is currently being processed by the carrier adapter. | | `sent` | `outbound` | Carrier has accepted and dispatched the message to the destination network. | | `delivered` | `outbound` | Delivery Receipt (DLR) received confirming delivery to the recipient's handset. | | `failed` | `outbound` | Message could not be sent or delivered (see `error_code` and `error_message`). | | `undelivered` | `outbound` | Carrier could not confirm delivery within the standard timeout window. | | `received` | `inbound` | Incoming SMS/MMS received on your workspace phone number. | --- ## Send Message ```http POST /messages/send ``` Queue an outbound SMS or MMS message for delivery. ### Authentication Requires `ApiKey` with `owner` or `editor` role. ### Request Headers | Header | Type | Description | | --------------- | -------- | ------------------------ | | `Authorization` | `string` | `ApiKey sk-YOUR_API_KEY` | | `Content-Type` | `string` | `application/json` | ### Request Body | Field | Type | Required | Description | | ------------ | ---------- | -------- | -------------------------------------------------------------------------------------------- | | `from` | `string` | Yes | Sender phone number in E.164 format. Must belong to the workspace and have `sms` capability. | | `to` | `string` | Yes | Destination phone number in E.164 format. | | `body` | `string` | Yes | Text content of the message (maximum 1,600 characters). | | `media_urls` | `string[]` | No | Array of publicly accessible media URLs (JPEG, PNG, GIF, audio) to send as an MMS message. | ### Requirements & Validations 1. **Phone Number Capability**: The sender number specified in `from` must belong to your workspace and have `sms` listed in its `capabilities`. 2. **Account Credit Balance**: Your workspace must maintain an available credit balance of at least `$0.01` to initiate outbound SMS sends. 3. **Format**: Phone numbers must adhere to the standard international [E.164](https://en.wikipedia.org/wiki/E.164) format (e.g. `+13074295456`). ### Rate Limiting The `POST /messages/send` endpoint operates on a dedicated **SMS** rate limit tier: | Tier | Window | Limit | Scope | | ---- | ------ | ----- | ----- | | `sms` | 60 seconds (1 min) | **30 req/min** | Per API Key (`ApiKey`) or agent/user account | This dedicated quota prevents outbound SMS sending from exhausting your workspace's general REST API rate limit (100 req/min). :::note **Need higher limits?** Enterprise customers can request custom rate limits. Contact us at [enterprise@firetell.com](mailto:enterprise@firetell.com) with your workspace ID and use case. ::: ### Example Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/messages/send" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "+13074295456", "to": "+18647123123", "body": "Your appointment is confirmed for tomorrow at 10:00 AM." }' ``` ### Example Request with MMS Media ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/messages/send" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "+13074295456", "to": "+18647123123", "body": "Here is your digital receipt.", "media_urls": [ "https://storage.yourcompany.com/receipts/inv_9981.png" ] }' ``` ### Response (`201 Created`) ```json { "id": "msg_05f18045cba1452204774f", "workspace_id": "yourcompany", "phone_number_id": "pn_e71c8caddbe5603f1d0522", "from": "+13074295456", "to": "+18647123123", "body": "Your appointment is confirmed for tomorrow at 10:00 AM.", "direction": "outbound", "status": "queued", "media_urls": [], "cost": null, "error_code": null, "error_message": null, "created_at": "2026-09-26T00:00:00.000Z", "updated_at": "2026-09-26T00:00:00.000Z" } ``` :::tip **Delivery Tracking** When you submit a message, it is returned immediately with `status: "queued"`. You do not need to poll for updates — register a [Webhook](/docs/rest-api/workspace-api/webhooks) for `message.sent` and `message.delivered` to receive real-time carrier delivery confirmations. ::: --- ## List Messages ```http GET /messages ``` Retrieve a paginated history of all inbound and outbound messages with rich search and filtering options. ### Authentication Requires `ApiKey` with any scope. ### Query Parameters | Parameter | Type | Default | Description | | ----------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------- | | `page` | `number` | `1` | Page number (minimum: 1). | | `limit` | `number` | `20` | Number of records per page (maximum: 100). | | `search` | `string` | — | Case-insensitive search matching sender (`from`), recipient (`to`), or text content (`body`). | | `direction` | `string` | — | Filter by direction: `outbound` or `inbound`. | | `status` | `string` | — | Filter by status: `queued`, `sending`, `sent`, `delivered`, `failed`, `undelivered`, or `received`. | | `phone_number_id` | `string` | — | Filter messages associated with a specific workspace phone number ID (`pn_...`). | | `from` | `string` | — | Filter messages created on or after this ISO 8601 date (e.g. `2026-09-01T00:00:00Z`). | | `to_date` | `string` | — | Filter messages created on or before this ISO 8601 date (e.g. `2026-09-30T23:59:59Z`). | | `sort_field` | `string` | `created_at` | Sort field. Allowed values: `created_at`, `updated_at`, `status`. | | `sort_order` | `string` | `desc` | Sort order: `asc` (oldest first) or `desc` (newest first). | ### Example Request Filter for all delivered outbound messages sent in September 2026: ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/messages?direction=outbound&status=delivered&from=2026-09-01T00:00:00Z&page=1&limit=20" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Search Example Search messages containing a specific customer phone number or keyword: ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/messages?search=appointment&limit=10" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response (`200 OK`) ```json { "data": [ { "id": "msg_05f18045cba1452204774f", "workspace_id": "yourcompany", "phone_number_id": "pn_e71c8caddbe5603f1d0522", "from": "+13074295456", "to": "+18647123123", "body": "Your appointment is confirmed for tomorrow at 10:00 AM.", "direction": "outbound", "status": "delivered", "cost": 0.0048, "media_urls": [], "error_code": null, "error_message": null, "created_at": "2026-09-26T00:00:00.000Z", "updated_at": "2026-09-26T00:00:05.000Z" }, { "id": "msg_be52701819fa34a72ae414", "workspace_id": "yourcompany", "phone_number_id": "pn_99bf875464f1c096f46aff", "from": "+18647123123", "to": "+13074295456", "body": "Thank you! Can I reschedule for 11:00 AM instead?", "direction": "inbound", "status": "received", "cost": null, "media_urls": [], "error_code": null, "error_message": null, "created_at": "2026-09-26T00:01:10.000Z", "updated_at": "2026-09-26T00:01:10.000Z" } ], "meta": { "total": 2, "page": 1, "limit": 20, "total_pages": 1 } } ``` --- ## Get Message ```http GET /messages/:id ``` Retrieve details and delivery status for a specific message by its unique ID. ### Authentication Requires `ApiKey` with any scope. ### Path Parameters | Parameter | Type | Required | Description | | --------- | -------- | -------- | ------------------------------------------------------- | | `id` | `string` | Yes | Message identifier (e.g. `msg_05f18045cba1452204774f`). | ### Example Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/messages/msg_05f18045cba1452204774f" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response (`200 OK`) ```json { "id": "msg_05f18045cba1452204774f", "workspace_id": "yourcompany", "phone_number_id": "pn_e71c8caddbe5603f1d0522", "from": "+13074295456", "to": "+18647123123", "body": "Your appointment is confirmed for tomorrow at 10:00 AM.", "direction": "outbound", "status": "delivered", "cost": 0.0048, "media_urls": [], "error_code": null, "error_message": null, "created_at": "2026-09-26T00:00:00.000Z", "updated_at": "2026-09-26T00:00:05.000Z" } ``` ### Error Responses #### `404 Not Found` Returned when the message ID does not exist in the requested workspace: ```json { "statusCode": 404, "message": "Message msg_invalid_id not found", "error": "Not Found" } ``` --- ## Webhook Notifications Configure your system to receive real-time HTTP callbacks whenever messaging events occur. See the [Webhook Event Catalog](/docs/webhooks/event-catalog) for sample event payloads: | Event | Trigger | | --------------------- | ----------------------------------------------------------------------------- | | `message.received` | An inbound SMS/MMS is received on your workspace phone number. | | `message.sent` | An outbound message was accepted and sent to the carrier network. | | `message.delivered` | A delivery receipt (DLR) confirmed successful delivery to the recipient. | | `message.failed` | An outbound message failed to send or deliver (includes carrier error codes). | | `message.undelivered` | Delivery could not be confirmed by the carrier within the receipt window. | --- # Phone Numbers Manage virtual phone numbers in your workspace. Phone numbers can be used for inbound/outbound calls, assigned to call flows, extensions, or SIP trunks. ## Endpoints | Method | Endpoint | Description | | -------- | ------------------------ | ---------------------------- | | `GET` | `/phone-numbers` | List all phone numbers | | `GET` | `/phone-numbers/:id` | Get phone number details | | `POST` | `/phone-numbers/connect` | Connect a new phone number | | `PATCH` | `/phone-numbers/:id` | Update phone number settings | | `DELETE` | `/phone-numbers/:id` | Release a phone number | ## Phone Number Object | Field | Type | Description | | --------------------- | -------------- | ---------------------------------------------------------------- | | `id` | string | Unique phone number ID (prefixed) | | `title` | string | Display name / label | | `number` | string | Full phone number with country calling code (e.g. `84901234567`) | | `country_code` | string | ISO 3166-1 alpha-2 country code (e.g. `VN`, `US`) | | `dial_code` | string | Country calling code (e.g. `84`, `1`) | | `status` | string | `pending`, `active`, or `inactive` | | `type` | string | `local`, `tollfree`, `mobile`, or `international` | | `capabilities` | string[] | Supported capabilities: `voice`, `sms` | | `provider` | string | Number provider: `firetell` or `sip_trunk` | | `enable_outbound` | boolean | Whether outbound calling is enabled | | `inbound_acl_id` | string | ID of the assigned [Inbound IP ACL](/docs/rest-api/workspace-api/sip-trunks#part-1-inbound-ip-access-control-lists-acls) for whitelisting inbound SIP traffic | | `outbound_gateway_id` | string | ID of the assigned [Outbound Gateway](/docs/rest-api/workspace-api/sip-trunks#part-2-outbound-gateways) for outbound trunk call routing | | `call_flow_id` | string \| null | ID of the assigned call flow (null if not assigned) | | `shared_teams_id` | string[] | IDs of teams this number is shared with | | `record_outbound` | boolean | Whether to record outbound calls | | `fee_per_month` | number | Monthly fee for this number | | `fee_per_mins_call` | number | Per-minute call fee | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | --- ## List Phone Numbers ```bash GET /phone-numbers ``` Retrieves a paginated list of all phone numbers in the workspace. ### Authentication Requires `ApiKey` with any scope. ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------- | --------------------------------------- | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `20` | Items per page (min: 1, max: 100) | | `search` | string | — | Search by title or number (text search) | | `sort_field` | string | — | Sort by field: `created_at`, `title` | | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | ### Example Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/phone-numbers?page=1&limit=10&search=support" \ -H "Authorization: ApiKey YOUR_API_KEY" ``` ### Response `200 OK` ```json { "data": [ { "id": "pn_a1b2c3d4e5f6g7h8i9j0k1", "title": "Main Number", "number": "84901234567", "country_code": "VN", "dial_code": "84", "status": "active", "type": "local", "capabilities": ["voice"], "provider": "sip_trunk", "enable_outbound": true, "inbound_acl_id": "acl_abc123", "outbound_gateway_id": "gw_def456", "call_flow_id": "cf_ghi789", "shared_teams_id": [], "record_outbound": true, "fee_per_month": 0, "fee_per_mins_call": 0, "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-01-15T08:30:00.000Z" } ], "meta": { "total": 1, "page": 1, "limit": 10, "total_pages": 1 } } ``` --- ## Get Phone Number Details ```bash GET /phone-numbers/:id ``` Retrieve detailed information about a specific phone number, including populated ACL and gateway data. ### Authentication Requires `ApiKey` with `full` scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------- | | `id` | string | Phone number ID (e.g. `pn_a1b2c3...`) | ### Example Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/phone-numbers/pn_a1b2c3d4e5f6g7h8i9j0k1" \ -H "Authorization: ApiKey YOUR_API_KEY" ``` ### Response `200 OK` ```json { "id": "pn_a1b2c3d4e5f6g7h8i9j0k1", "title": "Main Number", "number": "84901234567", "country_code": "VN", "dial_code": "84", "status": "active", "type": "local", "capabilities": ["voice"], "provider": "sip_trunk", "enable_outbound": true, "inbound_acl_id": "acl_abc123", "outbound_gateway_id": "gw_def456", "call_flow_id": "cf_ghi789", "shared_teams_id": [], "record_outbound": true, "fee_per_month": 0, "fee_per_mins_call": 0, "acl": { "id": "acl_abc123", "name": "SIP Trunk ACL", "workspace_id": "my_workspace" }, "gateway": { "id": "gw_def456", "name": "Primary Gateway", "workspace_id": "my_workspace" }, "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-01-15T08:30:00.000Z" } ``` ### Error Responses | Status | Description | | ------ | ----------------------------------------------------------- | | `404` | Phone number not found or does not belong to this workspace | --- ## Connect a Phone Number ```bash POST /phone-numbers/connect ``` Connect (provision) a new phone number to the workspace. The phone number will be created with `pending` status. The country calling code is automatically prepended to the number based on the `country_code`. ### Authentication Requires `ApiKey` with `full` scope. ### Request Body | Field | Type | Required | Description | | --------------------- | ------- | -------- | ----------------------------------------------------------------------- | | `title` | string | ✅ | Display name (1–36 characters) | | `number` | string | ✅ | Phone number **without** country code, digits only (6–14 chars) | | `country_code` | string | ✅ | ISO 3166-1 alpha-2 code (e.g. `VN`, `US`, `SG`) | | `inbound_acl_id` | string | ✅ | [Inbound ACL ID](/docs/rest-api/workspace-api/sip-trunks#part-1-inbound-ip-access-control-lists-acls) for whitelisting inbound SIP traffic | | `enable_outbound` | boolean | ✅ | Enable outbound calling | | `outbound_gateway_id` | string | Optional | [Outbound Gateway ID](/docs/rest-api/workspace-api/sip-trunks#part-2-outbound-gateways) for trunk routing (required if `enable_outbound` is `true`) | ### Example Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/phone-numbers/connect" \ -H "Authorization: ApiKey YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Support Line", "number": "901234567", "country_code": "VN", "inbound_acl_id": "acl_abc123", "enable_outbound": true, "outbound_gateway_id": "gw_def456" }' ``` ### Response `201 Created` ```json { "id": "pn_x1y2z3w4v5u6t7s8r9q0p1", "title": "Support Line", "number": "84901234567", "country_code": "VN", "dial_code": "84", "status": "pending", "type": "local", "capabilities": ["voice"], "provider": "sip_trunk", "enable_outbound": true, "inbound_acl_id": "acl_abc123", "outbound_gateway_id": "gw_def456", "call_flow_id": null, "shared_teams_id": [], "record_outbound": true, "workspace_id": "my_workspace", "created_at": "2026-07-13T08:30:00.000Z", "updated_at": "2026-07-13T08:30:00.000Z" } ``` ### Error Responses | Status | Description | | ------ | ------------------------------------------------------- | | `400` | Validation error, invalid ACL ID, or invalid gateway ID | | `403` | Maximum limit of 100 phone numbers reached | | `409` | Phone number already exists in this workspace | :::tip The `number` field should contain **only digits** without the country calling code. For example, for a Vietnamese number `+84 901 234 567`, send `"number": "901234567"` with `"country_code": "VN"`. The system will automatically prepend `84`. ::: --- ## Update a Phone Number ```bash PATCH /phone-numbers/:id ``` Update settings for an existing phone number. All fields are optional — only include the fields you want to change. ### Authentication Requires `ApiKey` with `full` scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | --------------- | | `id` | string | Phone number ID | ### Request Body | Field | Type | Description | | --------------------- | -------------- | --------------------------------------------- | | `title` | string | Display name (1–36 characters) | | `inbound_acl_id` | string | [Inbound ACL ID](/docs/rest-api/workspace-api/sip-trunks#part-1-inbound-ip-access-control-lists-acls) for whitelisting inbound SIP traffic | | `enable_outbound` | boolean | Enable/disable outbound calling | | `outbound_gateway_id` | string | [Outbound Gateway ID](/docs/rest-api/workspace-api/sip-trunks#part-2-outbound-gateways) for trunk routing | | `shared_teams_id` | string[] | Team IDs this number is shared with (max 100) | | `record_outbound` | boolean | Enable/disable outbound call recording | | `call_flow_id` | string \| null | Assign a call flow, or `null` to unassign | ### Example Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/phone-numbers/pn_a1b2c3d4e5f6g7h8i9j0k1" \ -H "Authorization: ApiKey YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Support Line", "call_flow_id": "cf_new789", "record_outbound": false }' ``` ### Response `200 OK` Returns the updated phone number object. ```json { "id": "pn_a1b2c3d4e5f6g7h8i9j0k1", "title": "Support Line", "number": "84901234567", "status": "active", "call_flow_id": "cf_new789", "record_outbound": false, "updated_at": "2026-07-13T09:00:00.000Z" } ``` ### Error Responses | Status | Description | | ------ | ---------------------------- | | `400` | Invalid ACL ID or gateway ID | | `404` | Phone number not found | --- ## Delete a Phone Number ```bash DELETE /phone-numbers/:id ``` Release (delete) a phone number from the workspace. This action is permanent and cannot be undone. ### Authentication Requires `ApiKey` with `full` scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | --------------- | | `id` | string | Phone number ID | ### Example Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/phone-numbers/pn_a1b2c3d4e5f6g7h8i9j0k1" \ -H "Authorization: ApiKey YOUR_API_KEY" ``` ### Response `200 OK` Returns the deleted phone number object. ```json { "id": "pn_a1b2c3d4e5f6g7h8i9j0k1", "title": "Main Number", "number": "84901234567", "status": "active", "workspace_id": "my_workspace" } ``` ### Error Responses | Status | Description | | ------ | ----------------------------------------------------------- | | `404` | Phone number not found or does not belong to this workspace | :::caution Deleting a phone number is **irreversible**. Any call flows, extensions, or SIP trunks referencing this number will no longer route calls to it. Make sure to update or remove those references before deleting. ::: --- # Push Credentials Configure push notification credentials for your custom mobile applications to receive VoIP push notifications when incoming calls arrive. Each credential is tied to an API Key SID and enables Firetell to dispatch push notifications using **your own** APNs (iOS) or FCM (Android) credentials instead of Firetell's built-in push infrastructure. :::info **When to Use Push Credentials** Push credentials are required when you build **your own mobile app** (not the Firetell Agent app) and want to receive incoming call pushes. If your agents use the official Firetell Agent app, push credentials are already configured — you do not need to set up anything here. ::: ## Endpoints | Method | Endpoint | Description | | -------- | ----------------------- | ---------------------------- | | `GET` | `/push-credentials` | List all push credentials | | `GET` | `/push-credentials/:id` | Get push credential details | | `POST` | `/push-credentials` | Create a new push credential | | `PATCH` | `/push-credentials/:id` | Update a push credential | | `DELETE` | `/push-credentials/:id` | Delete a push credential | ## The Push Credential Object | Field | Type | Description | | ------------------------------- | ------- | ------------------------------------------------------------------------------------------ | | `id` | string | Unique identifier (prefixed with `pc_`) | | `workspace_id` | string | Workspace identifier | | `api_key_sid` | string | The API Key SID that this credential is associated with | | `name` | string | Display name for this credential | | `platform` | string | Platform: `ios` or `android` | | `enabled` | boolean | Whether this credential is active | | `apns_key_id` | string | _(iOS only)_ 10-character Key ID from Apple Developer portal | | `apns_team_id` | string | _(iOS only)_ 10-character Team ID from Apple Developer portal | | `apns_private_key_encrypted` | string | _(iOS only)_ Always `"******"` in responses — the private key is encrypted at rest | | `apns_bundle_id` | string | _(iOS only)_ iOS app bundle identifier (e.g., `com.yourcompany.app`) | | `apns_environment` | string | _(iOS only)_ `sandbox` or `production` | | `fcm_service_account_encrypted` | string | _(Android only)_ Always `"******"` in responses — the service account is encrypted at rest | | `fcm_package_name` | string | _(Android only)_ Android app package name | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | :::caution **Credential Security** Sensitive fields (`apns_private_key`, `fcm_service_account`) are **encrypted at rest** using AES-256-GCM and are **never returned** in API responses. You will see `"******"` as a placeholder. If you lose your credentials, you must re-upload new ones. ::: --- ## Quota Each workspace can have a maximum of **30 push credentials**. If you need more, contact [enterprise@firetell.com](mailto:enterprise@firetell.com). --- ## List Push Credentials ``` GET /push-credentials ``` Retrieve a paginated list of all push credentials in the workspace. ### Authentication Requires `ApiKey` or JWT with any role. ### Query Parameters | Parameter | Type | Default | Description | | --------- | ------ | ------- | --------------------------------- | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `20` | Items per page (min: 1, max: 100) | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/push-credentials?page=1&limit=10" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "data": [ { "id": "pc_a1b2c3d4e5f6g7h8", "workspace_id": "ws_123456789", "api_key_sid": "sid-650000000000000000000001", "name": "Production iOS App", "platform": "ios", "enabled": true, "apns_key_id": "ABC123DEFG", "apns_team_id": "TEAM123456", "apns_private_key_encrypted": "******", "apns_bundle_id": "com.yourcompany.app", "apns_environment": "production", "created_at": "2026-08-01T10:00:00.000Z", "updated_at": "2026-08-01T10:00:00.000Z" } ], "meta": { "total": 1, "page": 1, "limit": 20, "total_pages": 1 } } ``` --- ## Get Push Credential ``` GET /push-credentials/:id ``` Retrieve the details of a specific push credential. ### Authentication Requires `ApiKey` or JWT with any role. ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/push-credentials/pc_a1b2c3d4e5f6g7h8" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "id": "pc_a1b2c3d4e5f6g7h8", "workspace_id": "ws_123456789", "api_key_sid": "sid-650000000000000000000001", "name": "Production iOS App", "platform": "ios", "enabled": true, "apns_key_id": "ABC123DEFG", "apns_team_id": "TEAM123456", "apns_private_key_encrypted": "******", "apns_bundle_id": "com.yourcompany.app", "apns_environment": "production", "created_at": "2026-08-01T10:00:00.000Z", "updated_at": "2026-08-01T10:00:00.000Z" } ``` --- ## Create Push Credential ``` POST /push-credentials ``` Create a new push credential. The request body varies depending on the platform. ### Authentication Requires `ApiKey` or JWT with role `owner` or `editor`. ### Request Body (iOS / APNs) | Parameter | Type | Required | Description | | ------------------ | ------ | -------- | --------------------------------------------------------- | | `name` | string | **Yes** | Display name (max 100 chars) | | `api_key_sid` | string | **Yes** | API Key SID (`sid-...`) to associate this credential with | | `platform` | string | **Yes** | Must be `ios` | | `apns_key_id` | string | **Yes** | 10-character Key ID from Apple Developer portal | | `apns_team_id` | string | **Yes** | 10-character Team ID from Apple Developer portal | | `apns_private_key` | string | **Yes** | Full content of the `.p8` private key file (PEM format) | | `apns_bundle_id` | string | **Yes** | iOS app bundle identifier (e.g., `com.yourcompany.app`) | | `apns_environment` | string | No | `sandbox` (default) or `production` | ### Request Example (iOS) ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/push-credentials" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Production iOS App", "api_key_sid": "sid-650000000000000000000001", "platform": "ios", "apns_key_id": "ABC123DEFG", "apns_team_id": "TEAM123456", "apns_private_key": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCC...\n-----END PRIVATE KEY-----", "apns_bundle_id": "com.yourcompany.app", "apns_environment": "production" }' ``` ### Request Body (Android / FCM) | Parameter | Type | Required | Description | | --------------------- | ------ | -------- | --------------------------------------------------------------------- | | `name` | string | **Yes** | Display name (max 100 chars) | | `api_key_sid` | string | **Yes** | API Key SID (`sid-...`) to associate this credential with | | `platform` | string | **Yes** | Must be `android` | | `fcm_service_account` | string | **Yes** | Firebase Service Account JSON (must have `"type": "service_account"`) | | `fcm_package_name` | string | No | Android package name (e.g., `com.yourcompany.app`) | ### Request Example (Android) ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/push-credentials" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Production Android App", "api_key_sid": "sid-650000000000000000000001", "platform": "android", "fcm_service_account": "{\"type\": \"service_account\", \"project_id\": \"my-project\", ...}", "fcm_package_name": "com.yourcompany.app" }' ``` ### Response (`201 Created`) ```json { "id": "pc_a1b2c3d4e5f6g7h8", "workspace_id": "ws_123456789", "api_key_sid": "sid-650000000000000000000001", "name": "Production iOS App", "platform": "ios", "enabled": true, "apns_key_id": "ABC123DEFG", "apns_team_id": "TEAM123456", "apns_private_key_encrypted": "******", "apns_bundle_id": "com.yourcompany.app", "apns_environment": "production", "created_at": "2026-08-01T10:00:00.000Z", "updated_at": "2026-08-01T10:00:00.000Z" } ``` --- ## Update Push Credential ``` PATCH /push-credentials/:id ``` Update an existing push credential. Only the fields you provide will be updated. ### Authentication Requires `ApiKey` or JWT with role `owner` or `editor`. ### Request Body | Parameter | Type | Description | | --------------------- | ------- | -------------------------------------------------------- | | `name` | string | Updated display name | | `enabled` | boolean | Enable or disable this credential | | `apns_key_id` | string | _(iOS)_ Updated Key ID | | `apns_team_id` | string | _(iOS)_ Updated Team ID | | `apns_private_key` | string | _(iOS)_ New private key (replaces existing) | | `apns_bundle_id` | string | _(iOS)_ Updated bundle identifier | | `apns_environment` | string | _(iOS)_ `sandbox` or `production` | | `fcm_service_account` | string | _(Android)_ New service account JSON (replaces existing) | | `fcm_package_name` | string | _(Android)_ Updated package name | :::tip To rotate credentials, only send the `apns_private_key` or `fcm_service_account` field. Other fields remain unchanged. ::: ### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/push-credentials/pc_a1b2c3d4e5f6g7h8" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Updated iOS App Name", "apns_environment": "production" }' ``` ### Response ```json { "id": "pc_a1b2c3d4e5f6g7h8", "workspace_id": "ws_123456789", "api_key_sid": "sid-650000000000000000000001", "name": "Updated iOS App Name", "platform": "ios", "enabled": true, "apns_key_id": "ABC123DEFG", "apns_team_id": "TEAM123456", "apns_private_key_encrypted": "******", "apns_bundle_id": "com.yourcompany.app", "apns_environment": "production", "created_at": "2026-08-01T10:00:00.000Z", "updated_at": "2026-08-05T14:30:00.000Z" } ``` --- ## Delete Push Credential ``` DELETE /push-credentials/:id ``` Permanently delete a push credential. Devices associated with the API Key SID will no longer receive VoIP push notifications. ### Authentication Requires `ApiKey` or JWT with role `owner` or `editor`. ### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/push-credentials/pc_a1b2c3d4e5f6g7h8" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "deleted": true } ``` --- ## Error Responses ### Quota Exceeded (`403`) ```json { "statusCode": 403, "message": "Maximum 30 push credentials per workspace. Delete unused credentials first.", "error": "Forbidden" } ``` ### Invalid API Key SID (`400`) ```json { "statusCode": 400, "message": "API Key with SID \"sid-invalid\" not found in this workspace.", "error": "Bad Request" } ``` ### Invalid FCM Service Account (`400`) ```json { "statusCode": 400, "message": "fcm_service_account must be a valid Firebase Service Account JSON with \"type\": \"service_account\".", "error": "Bad Request" } ``` --- # SIP Accounts SIP Accounts are credentials used by SIP-compatible softphones, desk phones, or other devices to register with the Firetell platform. Each SIP account can optionally be assigned an `outbound_caller_id` (a workspace phone number) for making external PSTN calls. ## Endpoints | Method | Endpoint | Description | | -------- | ------------------- | --------------------- | | `GET` | `/sip-accounts` | List all SIP accounts | | `POST` | `/sip-accounts` | Create a SIP account | | `PATCH` | `/sip-accounts/:id` | Update a SIP account | | `DELETE` | `/sip-accounts/:id` | Delete a SIP account | ## The SIP Account Object | Field | Type | Description | | -------------------- | -------------- | -------------------------------------------------------------------------- | | `id` | string | Unique SIP account identifier (prefixed with `si_`) | | `title` | string | Display name of the SIP account | | `username` | string | SIP registration username | | `workspace_id` | string | Workspace identifier | | `domain` | string | SIP domain for registration (e.g., `yourcompany.firetell.app`) | | `outbound_caller_id` | string \| null | Assigned phone number used as Caller ID for external PSTN calls, or `null` | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | :::info The `password` field is never returned in API responses. SIP registration credentials are stored securely with HA1/HA1B digest hashing. ::: --- ## List SIP Accounts ``` GET /sip-accounts ``` Retrieve a paginated list of all SIP accounts in the workspace. ### Authentication Requires `ApiKey` with any scope. ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------------ | ---------------------------------------------- | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `10` | Items per page (min: 1, max: 100) | | `search` | string | — | Search by title or username (case-insensitive) | | `sort_field` | string | `created_at` | Sort field. Allowed: `created_at`, `title` | | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/sip-accounts?page=1&limit=10" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "data": [ { "id": "si_01h8abcdef0123456789", "title": "Reception Desk Phone", "username": "reception01", "workspace_id": "yourcompany", "domain": "yourcompany.firetell.app", "outbound_caller_id": "+84901234567", "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-03-20T14:00:00.000Z" }, { "id": "si_01h8abcdef0123456790", "title": "Softphone - John", "username": "john01", "workspace_id": "yourcompany", "domain": "yourcompany.firetell.app", "outbound_caller_id": null, "created_at": "2026-02-10T10:00:00.000Z", "updated_at": "2026-02-10T10:00:00.000Z" } ], "meta": { "total": 2, "page": 1, "limit": 10, "total_pages": 1 } } ``` --- ## Create SIP Account ``` POST /sip-accounts ``` Create a new SIP account. The system automatically registers the SIP subscriber credentials in the SIP registrar database. ### Authentication Requires `ApiKey` with `full` scope. ### Request Body | Field | Type | Required | Description | | -------------------- | -------------- | -------- | --------------------------------------------------------------------------------------------------- | | `title` | string | ✅ | Display name. 1–36 characters. | | `username` | string | ✅ | SIP username. 3–30 characters, lowercase letters and numbers only. Must be unique per workspace. | | `password` | string | ✅ | SIP registration password. 6–50 characters. | | `outbound_caller_id` | string \| null | — | Assigned phone number for making external calls. Set to `null` or empty string to disable outbound. | ### Request (Without Outbound Calls) ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/sip-accounts" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Conference Room Phone", "username": "confroom01", "password": "securePass123" }' ``` ### Response ```json { "id": "si_01h8abcdef0123456791", "title": "Conference Room Phone", "username": "confroom01", "workspace_id": "yourcompany", "domain": "yourcompany.firetell.app", "outbound_caller_id": null, "created_at": "2026-07-09T08:00:00.000Z", "updated_at": "2026-07-09T08:00:00.000Z" } ``` ### Request (With Outbound Calls) ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/sip-accounts" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Sales Desk Phone", "username": "sales01", "password": "securePass456", "outbound_caller_id": "+84901234567" }' ``` ### Response ```json { "id": "si_01h8abcdef0123456792", "title": "Sales Desk Phone", "username": "sales01", "workspace_id": "yourcompany", "domain": "yourcompany.firetell.app", "outbound_caller_id": "+84901234567", "created_at": "2026-07-09T08:00:00.000Z", "updated_at": "2026-07-09T08:00:00.000Z" } ``` ### Error Responses **Duplicate username:** ```json { "statusCode": 409, "message": "username confroom01 is exists", "error": "Conflict" } ``` **Phone number not found in workspace:** ```json { "statusCode": 400, "message": "Outbound caller ID phone number '+84999999999' not found or does not belong to this workspace", "error": "Bad Request" } ``` :::tip **SIP Registration Details** After creating a SIP account, configure your SIP device with: - **SIP Server / Registrar:** The `domain` value from the response (e.g., `yourcompany.firetell.app`) - **Username:** The `username` you provided - **Password:** The `password` you provided - **Transport:** UDP (default), TCP, or TLS ::: --- ## Update SIP Account ``` PATCH /sip-accounts/:id ``` Update an existing SIP account. Only include the fields you want to change. The `username` cannot be changed after creation. ### Authentication Requires `ApiKey` with `full` scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ---------------------------------------------------- | | `id` | string | The SIP account ID (e.g., `si_01h8abcdef0123456789`) | ### Request Body All fields are optional: | Field | Type | Description | | -------------------- | -------------- | ---------------------------------------------------------------------------------------------- | | `title` | string | New display name. 1–36 characters. | | `password` | string | New SIP registration password. 6–50 characters. | | `outbound_caller_id` | string \| null | New Outbound Caller ID phone number, or `null` / `""` to remove outbound calling capabilities. | ### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/sip-accounts/si_01h8abcdef0123456789" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Main Reception Phone", "outbound_caller_id": "+84901234567" }' ``` ### Response Returns the updated SIP account object: ```json { "id": "si_01h8abcdef0123456789", "title": "Main Reception Phone", "username": "reception01", "workspace_id": "yourcompany", "domain": "yourcompany.firetell.app", "outbound_caller_id": "+84901234567", "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-08-12T10:00:00.000Z" } ``` ### Remove Outbound Caller ID To disable outbound calling for a SIP account, set `outbound_caller_id` to `null`: ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/sip-accounts/si_01h8abcdef0123456789" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "outbound_caller_id": null }' ``` ### Error Response ```json { "statusCode": 404, "message": "Sip account not found or does not belong to this workspace", "error": "Not Found" } ``` --- ## Delete SIP Account ``` DELETE /sip-accounts/:id ``` Permanently delete a SIP account. The SIP subscriber credentials are also removed from the registrar database, immediately preventing the device from registering. ### Authentication Requires `ApiKey` with `full` scope. :::caution This action is irreversible. Any SIP devices registered with this account will be immediately disconnected and unable to make or receive calls. ::: ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ------------------ | | `id` | string | The SIP account ID | ### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/sip-accounts/si_01h8abcdef0123456789" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response Returns the deleted SIP account object: ```json { "id": "si_01h8abcdef0123456789", "title": "Reception Desk Phone", "username": "reception01", "workspace_id": "yourcompany", "domain": "yourcompany.firetell.app", "outbound_caller_id": "+84901234567", "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-08-12T10:00:00.000Z" } ``` ### Error Response ```json { "statusCode": 404, "message": "Sip account not found or does not belong to this workspace", "error": "Not Found" } ``` --- # SIP Trunks SIP Trunks allow you to connect your existing IP PBX or telephony infrastructure to Firetell's cloud communications network for inbound and outbound voice traffic. SIP Trunking in Firetell consists of two primary resources: 1. **Inbound IP Access Control Lists (ACLs)**: Whitelist IP addresses or CIDR blocks allowed to send inbound SIP calls into Firetell. 2. **Outbound Gateways**: Configure outbound carrier SIP trunks, origination target URIs, protocols, and SIP authentication credentials. --- ## Resource Endpoints ### Inbound IP Access Control Lists (ACLs) | Method | Endpoint | Description | | -------- | --------------------- | -------------------------------- | | `GET` | `/sip-trunks/acls` | List all Inbound ACLs | | `GET` | `/sip-trunks/acls/:id` | Get Inbound ACL details | | `POST` | `/sip-trunks/acls` | Create an Inbound ACL | | `PATCH` | `/sip-trunks/acls/:id` | Update an Inbound ACL | | `DELETE` | `/sip-trunks/acls/:id` | Delete an Inbound ACL | ### Outbound Gateways | Method | Endpoint | Description | | -------- | ------------------------- | ----------------------------- | | `GET` | `/sip-trunks/gateways` | List all Outbound Gateways | | `GET` | `/sip-trunks/gateways/:id` | Get Outbound Gateway details | | `POST` | `/sip-trunks/gateways` | Create an Outbound Gateway | | `PATCH` | `/sip-trunks/gateways/:id` | Update an Outbound Gateway | | `DELETE` | `/sip-trunks/gateways/:id` | Delete an Outbound Gateway | --- # Part 1: Inbound IP Access Control Lists (ACLs) Inbound ACLs control which remote IP addresses or subnets are authorized to send inbound SIP traffic into your Firetell workspace. ## The ACL Object | Field | Type | Description | | ------------- | -------- | -------------------------------------------------------------- | | `id` | string | Unique ACL identifier (prefixed with `acl_`) | | `workspace_id`| string | Workspace identifier | | `title` | string | Descriptive label for this ACL | | `mode` | string | Filter mode (`ip` or `domain`) | | `allowed_ips` | array | List of authorized IPv4/IPv6 addresses or CIDR blocks (max: 5) | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | --- ## List Inbound ACLs ``` GET /sip-trunks/acls ``` ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------------ | ------------------------------------------------- | | `page` | number | `1` | Page number | | `limit` | number | `10` | Items per page (max: 100) | | `search` | string | — | Search by ACL title | | `sort_field` | string | `created_at` | Sort field (`created_at`, `title`) | | `sort_order` | string | `desc` | Sort direction (`asc`, `desc`) | ### Response `200 OK` ```json { "data": [ { "id": "acl_123456789", "workspace_id": "ws_company", "title": "Primary PBX Server", "mode": "ip", "allowed_ips": ["118.69.123.45", "203.162.0.0/24"], "created_at": "2026-07-21T15:00:00.000Z", "updated_at": "2026-07-21T15:00:00.000Z" } ], "meta": { "total": 1, "page": 1, "limit": 10, "total_pages": 1 } } ``` --- ## Create Inbound ACL ``` POST /sip-trunks/acls ``` ### Request Body ```json { "title": "Backup Data Center PBX", "mode": "ip", "allowed_ips": ["14.225.10.12/32"] } ``` ### Response `201 Created` ```json { "id": "acl_987654321", "workspace_id": "ws_company", "title": "Backup Data Center PBX", "mode": "ip", "allowed_ips": ["14.225.10.12/32"], "created_at": "2026-07-21T15:15:00.000Z", "updated_at": "2026-07-21T15:15:00.000Z" } ``` --- ## Update Inbound ACL ``` PATCH /sip-trunks/acls/:id ``` ### Request Body ```json { "title": "HQ Asterisk PBX", "allowed_ips": ["118.69.123.45", "118.69.123.46"] } ``` ### Response `200 OK` ```json { "id": "acl_123456789", "workspace_id": "ws_company", "title": "HQ Asterisk PBX", "mode": "ip", "allowed_ips": ["118.69.123.45", "118.69.123.46"], "updated_at": "2026-07-21T15:20:00.000Z" } ``` --- ## Delete Inbound ACL ``` DELETE /sip-trunks/acls/:id ``` ### Response `200 OK` ```json { "id": "acl_123456789", "workspace_id": "ws_company", "title": "HQ Asterisk PBX" } ``` --- # Part 2: Outbound Gateways Outbound Gateways define carrier trunks and remote SIP endpoints for routing outbound calls from Firetell to external telco networks or private PBX systems. ## The Gateway Object | Field | Type | Description | | --------------------- | -------- | -------------------------------------------------------------------- | | `id` | string | Unique gateway identifier (prefixed with `gw_`) | | `workspace_id` | string | Workspace identifier | | `title` | string | Descriptive label for this gateway | | `outbound_caller_id` | string | Default caller ID number to present when placing calls via this trunk | | `outbound_uris` | array | Array of target SIP destination URIs (1 to 10 targets) | | `outbound_uris[].uri` | string | Destination IP or FQDN host (e.g. `sip.telco.com` or `1.2.3.4`) | | `outbound_uris[].port`| number | SIP port (`1` to `65535`, default `5060`) | | `outbound_uris[].protocol` | string | Transport protocol (`udp`, `tcp`, `tls`) | | `outbound_uris[].priority` | number | Routing priority order (`1` highest to `10`) | | `credential` | object | SIP Digest authentication credentials (if required by trunk provider)| | `credential.username` | string | SIP auth username | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | --- ## List Outbound Gateways ``` GET /sip-trunks/gateways ``` ### Response `200 OK` ```json { "data": [ { "id": "gw_abc123456", "workspace_id": "ws_company", "title": "Viettel Carrier Trunk", "outbound_caller_id": "842471001234", "outbound_uris": [ { "uri": "10.0.1.50", "port": 5060, "protocol": "udp", "priority": 1 } ], "credential": { "username": "vt_trunk_user" }, "created_at": "2026-07-21T15:05:00.000Z", "updated_at": "2026-07-21T15:05:00.000Z" } ], "meta": { "total": 1, "page": 1, "limit": 10, "total_pages": 1 } } ``` --- ## Create Outbound Gateway ``` POST /sip-trunks/gateways ``` ### Request Body ```json { "title": "FPT Telecom Trunk", "outbound_caller_id": "842873009999", "outbound_uris": [ { "uri": "sip.fpttelecom.vn", "port": 5060, "protocol": "udp", "priority": 1 } ], "credential": { "username": "fpt_user", "password": "SecretPassword123" } } ``` ### Response `201 Created` ```json { "id": "gw_xyz789012", "workspace_id": "ws_company", "title": "FPT Telecom Trunk", "outbound_caller_id": "842873009999", "outbound_uris": [ { "uri": "sip.fpttelecom.vn", "port": 5060, "protocol": "udp", "priority": 1 } ], "created_at": "2026-07-21T15:25:00.000Z", "updated_at": "2026-07-21T15:25:00.000Z" } ``` --- ## Update Outbound Gateway ``` PATCH /sip-trunks/gateways/:id ``` ### Request Body ```json { "title": "FPT Telecom Primary Trunk", "outbound_uris": [ { "uri": "sip.fpttelecom.vn", "port": 5061, "protocol": "tls", "priority": 1 } ] } ``` ### Response `200 OK` ```json { "id": "gw_xyz789012", "workspace_id": "ws_company", "title": "FPT Telecom Primary Trunk", "outbound_uris": [ { "uri": "sip.fpttelecom.vn", "port": 5061, "protocol": "tls", "priority": 1 } ], "updated_at": "2026-07-21T15:30:00.000Z" } ``` --- ## Delete Outbound Gateway ``` DELETE /sip-trunks/gateways/:id ``` ### Response `200 OK` ```json { "id": "gw_xyz789012", "workspace_id": "ws_company", "title": "FPT Telecom Primary Trunk" } ``` --- # Teams Teams group agents together for call routing, queue management, and workload distribution. Each team can contain multiple agents, and each agent can belong to multiple teams. Team members can have a role of either `leader` or `member`. ## Endpoints | Method | Endpoint | Description | | -------- | ------------------------------ | ------------------------------ | | `GET` | `/teams` | List all teams | | `GET` | `/teams/:id` | Get team details | | `POST` | `/teams` | Create a team | | `PATCH` | `/teams/:id` | Update a team | | `DELETE` | `/teams/:id` | Delete a team | | `GET` | `/teams/:id/agents` | List agents in a team | | `PUT` | `/teams/:id/agents/:agent_id` | Add an agent to a team | | `DELETE` | `/teams/:id/agents/:agent_id` | Remove an agent from a team | | `GET` | `/teams/:id/suggestion-agents` | Suggest agents not in the team | ## The Team Object | Field | Type | Description | | -------------- | ------ | -------------------------------------------- | | `id` | string | Unique team identifier (prefixed with `te_`) | | `title` | string | Team name | | `workspace_id` | string | Workspace identifier | | `agent_count` | number | Number of agents currently in the team | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | ### Team Agent Object When listing agents within a team, each item contains: | Field | Type | Description | | -------------- | ------ | ---------------------------------------------- | | `team_id` | string | The team ID | | `agent_id` | string | The agent ID | | `workspace_id` | string | Workspace identifier | | `role` | string | Agent's role in the team: `leader` or `member` | | `agent` | object | Populated agent details (see below) | | `created_at` | string | ISO 8601 timestamp when agent joined | | `updated_at` | string | ISO 8601 last update timestamp | The populated `agent` object includes: | Field | Type | Description | | -------------- | -------------- | ------------------ | | `id` | string | Agent ID | | `display_name` | string | Agent display name | | `avatar` | string \| null | Agent avatar URL | | `email` | string \| null | Agent email | | `username` | string | Agent username | | `country_code` | string | Agent country code | --- ## List Teams ``` GET /teams ``` Retrieve a paginated list of all teams in the workspace. ### Authentication Requires `ApiKey` with any scope. ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------------ | --------------------------------------------------------- | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `10` | Items per page (min: 1, max: 100) | | `search` | string | — | Search teams by title | | `sort_field` | string | `created_at` | Sort field. Allowed: `created_at`, `title`, `agent_count` | | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/teams?page=1&limit=10&sort_field=agent_count&sort_order=desc" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "data": [ { "id": "te_a1b2c3d4e5f6a7b8c9d0e1f2", "title": "Support Team", "workspace_id": "yourcompany", "agent_count": 12, "created_at": "2026-01-10T09:00:00.000Z", "updated_at": "2026-03-20T14:00:00.000Z" }, { "id": "te_b2c3d4e5f6a7b8c9d0e1f2a3", "title": "Sales Team", "workspace_id": "yourcompany", "agent_count": 8, "created_at": "2026-01-12T11:00:00.000Z", "updated_at": "2026-02-15T10:30:00.000Z" } ], "meta": { "total": 5, "page": 1, "limit": 10, "total_pages": 1 } } ``` --- ## Get Team ``` GET /teams/:id ``` Retrieve details of a specific team. ### Authentication Requires `ApiKey` with any scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------- | | `id` | string | The team ID (e.g., `te_a1b2c3d4e5f6a7b8c9d0e1f2`) | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/teams/te_a1b2c3d4e5f6a7b8c9d0e1f2" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "id": "te_a1b2c3d4e5f6a7b8c9d0e1f2", "title": "Support Team", "workspace_id": "yourcompany", "agent_count": 12, "created_at": "2026-01-10T09:00:00.000Z", "updated_at": "2026-03-20T14:00:00.000Z" } ``` ### Error Response ```json { "statusCode": 404, "message": "Team not found or does not belong to this workspace", "error": "Not Found" } ``` --- ## Create Team ``` POST /teams ``` Create a new team in the workspace. ### Authentication Requires `ApiKey` with `full` scope. :::caution Each workspace is limited to a maximum of **100 teams**. Exceeding this limit will return a `402 Payment Required` error. ::: ### Request Body | Field | Type | Required | Description | | ------- | ------ | -------- | --------------------------- | | `title` | string | ✅ | Team name. 1–36 characters. | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/teams" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Technical Support" }' ``` ### Response ```json { "id": "te_c3d4e5f6a7b8c9d0e1f2a3b4", "title": "Technical Support", "workspace_id": "yourcompany", "agent_count": 0, "created_at": "2026-07-09T08:00:00.000Z", "updated_at": "2026-07-09T08:00:00.000Z" } ``` ### Error Response **Team limit reached:** ```json { "statusCode": 402, "message": "You have reached the maximum limit of 100 team. Please contact support" } ``` --- ## Update Team ``` PATCH /teams/:id ``` Update a team's title. ### Authentication Requires `ApiKey` with `full` scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ----------- | | `id` | string | The team ID | ### Request Body | Field | Type | Required | Description | | ------- | ------ | -------- | ------------------------------- | | `title` | string | ✅ | New team name. 1–36 characters. | ### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/teams/te_a1b2c3d4e5f6a7b8c9d0e1f2" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Premium Support" }' ``` ### Response Returns the updated team object: ```json { "id": "te_a1b2c3d4e5f6a7b8c9d0e1f2", "title": "Premium Support", "workspace_id": "yourcompany", "agent_count": 12, "created_at": "2026-01-10T09:00:00.000Z", "updated_at": "2026-07-09T09:30:00.000Z" } ``` ### Error Response ```json { "statusCode": 400, "message": "Team not found or does not belong to this workspace", "error": "Bad Request" } ``` --- ## Delete Team ``` DELETE /teams/:id ``` Permanently delete a team. All agent memberships in this team will be automatically removed. ### Authentication Requires `ApiKey` with `full` scope. :::caution This action is irreversible. All agents will be removed from the team, but the agent accounts themselves will not be affected. ::: ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ----------- | | `id` | string | The team ID | ### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/teams/te_a1b2c3d4e5f6a7b8c9d0e1f2" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response Returns the deleted team object: ```json { "id": "te_a1b2c3d4e5f6a7b8c9d0e1f2", "title": "Support Team", "workspace_id": "yourcompany", "agent_count": 12, "created_at": "2026-01-10T09:00:00.000Z", "updated_at": "2026-03-20T14:00:00.000Z" } ``` --- ## List Team Agents ``` GET /teams/:id/agents ``` Retrieve a paginated list of agents that belong to a specific team, including their role and populated agent details. ### Authentication Requires `ApiKey` with any scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ----------- | | `id` | string | The team ID | ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------------ | --------------------------- | | `page` | number | `1` | Page number | | `limit` | number | `10` | Items per page (max: 100) | | `sort_field` | string | `created_at` | Sort field | | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/teams/te_a1b2c3d4e5f6a7b8c9d0e1f2/agents?page=1&limit=10" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "data": [ { "team_id": "te_a1b2c3d4e5f6a7b8c9d0e1f2", "agent_id": "ag_7f3a2b1c9d4e5f6a8b0c1d2e", "workspace_id": "yourcompany", "role": "leader", "agent": { "id": "ag_7f3a2b1c9d4e5f6a8b0c1d2e", "display_name": "John Doe", "avatar": "https://s3.amazonaws.com/bucket/yourcompany/avatars/abc123.webp", "email": "john@example.com", "username": "johndoe", "country_code": "US" }, "created_at": "2026-02-01T10:00:00.000Z", "updated_at": "2026-02-01T10:00:00.000Z" }, { "team_id": "te_a1b2c3d4e5f6a7b8c9d0e1f2", "agent_id": "ag_9a8b7c6d5e4f3a2b1c0d9e8f", "workspace_id": "yourcompany", "role": "member", "agent": { "id": "ag_9a8b7c6d5e4f3a2b1c0d9e8f", "display_name": "Jane Doe", "avatar": null, "email": "jane@example.com", "username": "janedoe", "country_code": "VN" }, "created_at": "2026-02-05T14:30:00.000Z", "updated_at": "2026-02-05T14:30:00.000Z" } ], "meta": { "total": 3, "page": 1, "limit": 10, "total_pages": 1 } } ``` --- ## Add Agent to Team ``` PUT /teams/:id/agents/:agent_id ``` Add an agent to a team. If the agent is already in the team, their role will be updated. The team's `agent_count` is automatically recalculated. ### Authentication Requires `ApiKey` with `full` scope. ### Path Parameters | Parameter | Type | Description | | ---------- | ------ | ------------------- | | `id` | string | The team ID | | `agent_id` | string | The agent ID to add | ### Request ```bash curl -X PUT "https://{workspace_id}.firetell.app/api/v1/teams/te_a1b2c3d4e5f6a7b8c9d0e1f2/agents/ag_7f3a2b1c9d4e5f6a8b0c1d2e" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response Returns the team-agent membership with populated agent details: ```json { "team_id": "te_a1b2c3d4e5f6a7b8c9d0e1f2", "agent_id": "ag_7f3a2b1c9d4e5f6a8b0c1d2e", "workspace_id": "yourcompany", "role": "member", "agent": { "id": "ag_7f3a2b1c9d4e5f6a8b0c1d2e", "display_name": "John Doe", "avatar": null, "email": "john@example.com", "username": "johndoe", "country_code": "US" }, "created_at": "2026-07-09T08:00:00.000Z", "updated_at": "2026-07-09T08:00:00.000Z" } ``` :::tip This endpoint uses **upsert** behavior — if the agent is already in the team, the request will succeed and update the membership rather than returning an error. ::: ### Error Response ```json { "statusCode": 400, "message": "Agent not found or does not belong to this workspace", "error": "Bad Request" } ``` --- ## Remove Agent from Team ``` DELETE /teams/:id/agents/:agent_id ``` Remove an agent from a team. The team's `agent_count` is automatically recalculated. The agent account itself is not affected. ### Authentication Requires `ApiKey` with `full` scope. ### Path Parameters | Parameter | Type | Description | | ---------- | ------ | ---------------------- | | `id` | string | The team ID | | `agent_id` | string | The agent ID to remove | ### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/teams/te_a1b2c3d4e5f6a7b8c9d0e1f2/agents/ag_7f3a2b1c9d4e5f6a8b0c1d2e" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response Returns the removed team-agent membership with populated agent details: ```json { "team_id": "te_a1b2c3d4e5f6a7b8c9d0e1f2", "agent_id": "ag_7f3a2b1c9d4e5f6a8b0c1d2e", "workspace_id": "yourcompany", "role": "member", "agent": { "id": "ag_7f3a2b1c9d4e5f6a8b0c1d2e", "display_name": "John Doe", "avatar": null, "email": "john@example.com", "username": "johndoe", "country_code": "US" }, "created_at": "2026-02-01T10:00:00.000Z", "updated_at": "2026-02-01T10:00:00.000Z" } ``` --- ## Suggest Agents ``` GET /teams/:id/suggestion-agents ``` Get a list of agents who are **not yet members** of the specified team. Useful for building "Add Agent" UI with search functionality. ### Authentication Requires `ApiKey` with any scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ----------- | | `id` | string | The team ID | ### Query Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------ | | `search` | string | Search by agent display name, email, or username | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/teams/te_a1b2c3d4e5f6a7b8c9d0e1f2/suggestion-agents?search=john" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response Returns up to 20 agents not in the team: ```json { "items": [ { "id": "ag_d4e5f6a7b8c9d0e1f2a3b4c5", "username": "johnsmith", "domain": "yourcompany.firetell.app", "display_name": "John Smith", "email": "johnsmith@example.com", "avatar": null, "state": "available", "is_active": true, "workspace_id": "yourcompany", "country_code": "US", "created_at": "2026-03-01T08:00:00.000Z", "updated_at": "2026-03-01T08:00:00.000Z" } ] } ``` :::info This endpoint returns a maximum of **20 results** and does not support pagination. Use the `search` parameter to narrow down results. ::: ### Error Response ```json { "statusCode": 404, "message": "Team not found or does not belong to this workspace", "error": "Not Found" } ``` --- # Voice Agents Voice Agents are AI-powered bots that handle customer calls autonomously using real-time speech processing. They integrate LLM models with speech-to-speech and speech-to-text (live transcriptions) capabilities to have natural conversations with callers. Voice Agents use a **versioned settings** model — you can create multiple versions of your agent's configuration, test them, and publish the best one for production. Agents can also execute **Tools (Function Calling)** to perform telephony actions (transfers, hangups, SMS) or query external APIs via Webhooks in real time. ## Endpoints | Method | Endpoint | Description | | -------- | ------------------------------------ | ------------------------------------- | | `GET` | `/voice-agents` | List all voice agents | | `GET` | `/voice-agents/:id` | Get voice agent details with settings | | `POST` | `/voice-agents` | Create a voice agent | | `PATCH` | `/voice-agents/:id` | Update voice agent name | | `DELETE` | `/voice-agents/:id` | Delete a voice agent | | `GET` | `/voice-agents/:id/versions` | List all setting versions | | `PATCH` | `/voice-agents/:id/settings` | Update settings for a version | | `PATCH` | `/voice-agents/:id/publish` | Publish a settings version | | `POST` | `/voice-agents/:id/version` | Create a new settings version | | `DELETE` | `/voice-agents/:id/version/:version` | Delete a settings version | | `GET` | `/voice-agents/:id/tools` | List all tools configured for agent | | `GET` | `/voice-agents/:id/tools/:toolId` | Get a specific tool | | `POST` | `/voice-agents/:id/tools` | Create a custom webhook tool | | `PATCH` | `/voice-agents/:id/tools/:toolId` | Update a tool | | `DELETE` | `/voice-agents/:id/tools/:toolId` | Delete a custom tool | | `GET` | `/voice-agents/models` | List available AI models | | `GET` | `/voice-agents/voices` | List available TTS voices | ## The Voice Agent Object | Field | Type | Description | | ------------------- | ------ | -------------------------------------------------- | | `id` | string | Unique voice agent identifier | | `name` | string | Voice agent name | | `workspace_id` | string | Workspace identifier | | `country_code` | string | Country code inherited from workspace | | `published_version` | number | Currently published settings version | | `settings` | object | The settings object (included in detail responses) | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | ### Voice Agent Settings Object | Field | Type | Description | | --------------------------- | -------------- | ----------------------------------------------- | | `id` | string | Unique settings record ID | | `voice_agent_id` | string | Parent voice agent ID | | `workspace_id` | string | Workspace identifier | | `provider` | string | AI provider (e.g., `openai`, `cartesia`) | | `model_id` | string | AI model ID (e.g., `gpt-realtime-2.1`) | | `voice_id` | string \| null | TTS voice ID (e.g., `alloy`, `echo`, `shimmer`) | | `greeting_message` | string | Initial greeting when call connects | | `instructions` | string | System prompt / behavior instructions | | `temperature` | number | Model creativity (default: `0.8`) | | `enable_noise_reduction` | boolean | Background noise reduction enabled | | `max_call_duration_seconds` | number | Maximum call duration (0–1800 seconds) | | `tools_id` | string[] | IDs of enabled tools/functions for this version | | `version` | number | Settings version number | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | ### The Voice Agent Tool Object | Field | Type | Description | | ----------------- | ------------------------------- | ----------------------------------------------------------------------------------- | | `id` | string | Unique tool identifier (e.g., `vat_abc123`) | | `voice_agent_id` | string | Parent voice agent ID | | `workspace_id` | string | Workspace identifier | | `name` | string | Function calling name (lowercase alphanumeric with underscores, e.g. `check_order`) | | `description` | string | Description of what the tool does (used by LLM to decide when to call it) | | `tool_type` | string | `builtin` (native telephony actions) or `custom` (external HTTP webhook) | | `webhook_url` | string \| null | Target endpoint URL for custom webhook tools | | `webhook_method` | string | HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`). Default: `POST` | | `webhook_headers` | object | Custom key-value HTTP headers sent with the webhook request | | `parameters` | Record\ | Parameter schemas expected by the tool function | | `required_params` | string[] | Array of required parameter names | | `is_active` | boolean | Whether this tool is currently enabled for execution | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | #### Tool Parameter Schema | Field | Type | Description | | ------------- | -------- | --------------------------------------------------------------------------- | | `type` | string | Parameter type: `string`, `number`, `integer`, `boolean`, `array`, `object` | | `description` | string | Description explaining the parameter's purpose to the LLM | | `enum` | string[] | Optional array of allowed values | | `required` | boolean | Whether this parameter is mandatory | --- ## List Voice Agents ``` GET /voice-agents ``` Retrieve a paginated list of all voice agents in the workspace. ### Authentication Requires `ApiKey` with any scope. ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------------ | ----------------------------------------- | | `page` | number | `1` | Page number | | `limit` | number | `10` | Items per page (max: 100) | | `search` | string | — | Search by name | | `sort_field` | string | `created_at` | Sort field. Allowed: `created_at`, `name` | | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/voice-agents?page=1&limit=10" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "data": [ { "id": "va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c", "name": "Support Assistant", "workspace_id": "yourcompany", "country_code": "US", "published_version": 2, "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-03-20T14:00:00.000Z" } ], "meta": { "total": 3, "page": 1, "limit": 10, "total_pages": 1 } } ``` --- ## Get Voice Agent ``` GET /voice-agents/:id ``` Retrieve a voice agent with its settings. By default returns the latest settings version. Optionally specify a version number. ### Authentication Requires `ApiKey` with any scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ------------------ | | `id` | string | The voice agent ID | ### Query Parameters | Parameter | Type | Description | | --------- | ------ | ---------------------------------------------------------------------- | | `version` | number | Specific settings version to retrieve. Defaults to the latest version. | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/voice-agents/va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "id": "va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c", "name": "Support Assistant", "workspace_id": "yourcompany", "country_code": "US", "published_version": 2, "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-03-20T14:00:00.000Z", "settings": { "id": "vas_a1b2c3d4e5f6a7b8c9d0e1f2", "voice_agent_id": "va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c", "workspace_id": "yourcompany", "model_id": "gpt-realtime", "voice_id": "alloy", "greeting_message": "Thank you for calling Acme Corp. This is Support Assistant. How may I help you today?", "instructions": "You are a helpful customer support agent for Acme Corp...", "temperature": 0.8, "enable_noise_reduction": true, "max_call_duration_seconds": 180, "tools_id": [], "version": 2, "created_at": "2026-02-01T10:00:00.000Z", "updated_at": "2026-03-20T14:00:00.000Z" } } ``` ### Error Response ```json { "statusCode": 404, "message": "Voice Agent not found", "error": "Not Found" } ``` --- ## Create Voice Agent ``` POST /voice-agents ``` Create a new voice agent with initial instructions. A default settings version (v1) is automatically created with sensible defaults. ### Authentication Requires `ApiKey` with `full` scope. :::caution Each workspace is limited to a maximum of **30 voice agents**. ::: ### Request Body | Field | Type | Required | Description | | -------------- | ------ | -------- | ----------------------------------------------------------- | | `name` | string | ✅ | Voice agent name. 3–30 characters. | | `instructions` | string | ✅ | System prompt / behavior instructions. 10–3,500 characters. | | `conversation` | string | — | AI conversation ID from instruction generation flow. | | `response_id` | string | — | AI response ID from instruction generation flow. | ### Default Settings When a voice agent is created, the following defaults are applied to the initial settings: | Setting | Default Value | | --------------------------- | -------------------------------------------------- | | `model_id` | `gpt-realtime` | | `voice_id` | `null` (system default) | | `temperature` | `0.8` | | `enable_noise_reduction` | `true` | | `max_call_duration_seconds` | `180` (3 minutes) | | `greeting_message` | Auto-generated from workspace title and agent name | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/voice-agents" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Sales Bot", "instructions": "You are a sales assistant for Acme Corp. Help customers learn about our products and pricing. Always be professional and helpful. If a customer wants to speak with a human agent, transfer the call immediately." }' ``` ### Response ```json { "id": "va_d4e5f6a7-b8c9-d0e1-f2a3-b4c5d6e7f8a9", "name": "Sales Bot", "workspace_id": "yourcompany", "country_code": "US", "published_version": 1, "created_at": "2026-07-09T08:00:00.000Z", "updated_at": "2026-07-09T08:00:00.000Z" } ``` ### Error Response ```json { "statusCode": 403, "message": "You have reached the maximum limit of 30 voice agents. Please contact support", "error": "Forbidden" } ``` --- ## Update Voice Agent ``` PATCH /voice-agents/:id ``` Update a voice agent's name. To update settings (instructions, model, voice, etc.), use the [Update Settings](#update-voice-agent-settings) endpoint. ### Authentication Requires `ApiKey` with `full` scope. ### Request Body | Field | Type | Required | Description | | ------ | ------ | -------- | -------------------------- | | `name` | string | — | New name. 3–30 characters. | ### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/voice-agents/va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Premium Support Bot" }' ``` ### Response Returns the full voice agent object with current settings (same format as [Get Voice Agent](#get-voice-agent)). --- ## Delete Voice Agent ``` DELETE /voice-agents/:id ``` Permanently delete a voice agent and all its settings versions. ### Authentication Requires `ApiKey` with `full` scope. :::caution This action is irreversible. The voice agent and all its versioned settings will be permanently deleted. ::: ### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/voice-agents/va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response Returns the deleted voice agent object. --- ## Settings Version Management Voice agents support **versioned settings**, allowing you to iterate on configurations and roll back if needed. ### List Versions ``` GET /voice-agents/:id/versions ``` List all settings versions for a voice agent, sorted by version number (newest first). ### Authentication Requires `ApiKey` with any scope. #### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/voice-agents/va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c/versions" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` #### Response ```json { "data": [ { "version": 3, "updated_at": "2026-07-09T09:00:00.000Z" }, { "version": 2, "updated_at": "2026-06-15T14:30:00.000Z" }, { "version": 1, "updated_at": "2026-01-15T08:30:00.000Z" } ], "meta": { "total": 3, "page": 1, "limit": 10, "total_pages": 1 } } ``` --- ### Update Voice Agent Settings ``` PATCH /voice-agents/:id/settings ``` Update the settings of a specific version. You must specify the `version` number of the settings you want to modify. ### Authentication Requires `ApiKey` with `full` scope. #### Request Body All fields are optional except `version`: | Field | Type | Required | Description | | --------------------------- | -------- | -------- | -------------------------------------------- | | `version` | number | ✅ | The settings version to update (min: 1) | | `greeting_message` | string | — | Initial greeting. Max 250 characters. | | `instructions` | string | — | System prompt. 10–3,500 characters. | | `model_id` | string | — | AI model ID. 1–100 characters. | | `voice_id` | string | — | TTS voice ID. 1–100 characters. | | `enable_noise_reduction` | boolean | — | Enable noise reduction | | `max_call_duration_seconds` | number | — | Max call duration. 0–1,800 seconds (30 min). | | `functions` | string[] | — | Array of tool/function IDs to enable | #### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/voice-agents/va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c/settings" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "version": 2, "instructions": "Updated instructions for the voice agent...", "voice_id": "shimmer", "max_call_duration_seconds": 300, "enable_noise_reduction": true }' ``` #### Response Returns the full voice agent object with the updated settings version. --- ### Publish a Version ``` PATCH /voice-agents/:id/publish ``` Publish a settings version to make it the active configuration used for live calls. ### Authentication Requires `ApiKey` with `full` scope. #### Request Body | Field | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------- | | `version` | number | ✅ | The version number to publish (min: 1) | #### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/voice-agents/va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c/publish" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "version": 3 }' ``` #### Response Returns the full voice agent object with `published_version` updated. --- ### Create a New Version ``` POST /voice-agents/:id/version ``` Create a new settings version by cloning the currently published version. The new version gets the next sequential version number. ### Authentication Requires `ApiKey` with `full` scope. :::info - You must have at least one **published** version before creating a new version. - A maximum of **20 versions** are kept. Older versions are automatically cleaned up. ::: #### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/voice-agents/va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c/version" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` #### Response ```json { "version": 4, "updated_at": "2026-07-09T10:00:00.000Z" } ``` #### Error Responses ```json { "statusCode": 400, "message": "You must publish one version before creating another", "error": "Bad Request" } ``` --- ### Delete a Version ``` DELETE /voice-agents/:id/version/:version ``` Delete a specific settings version. You cannot delete the currently published version. ### Authentication Requires `ApiKey` with `full` scope. #### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/voice-agents/va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c/version/3" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` #### Response ```json { "version": 3, "updated_at": "2026-07-09T09:00:00.000Z" } ``` #### Error Response ```json { "statusCode": 400, "message": "Version has been Published, You cannot delete", "error": "Bad Request" } ``` --- ## Voice Agent Tools (Function Calling) Voice Agent Tools allow your AI agent to take concrete actions during live phone conversations using **Function Calling**. There are two categories of tools: 1. **Built-in Telephony Tools**: Native actions executed directly on the Firetell telephony switch (e.g. hang up, transfer call, send SMS). 2. **Custom Webhook Tools**: External API integrations. When the AI decides to query an order, check account balance, or book an appointment, Firetell dispatches an HTTP request to your webhook URL and provides the JSON response back to the LLM to speak naturally to the caller. --- ### Built-in Telephony Tools Reference Built-in tools are pre-configured in every workspace and executed directly on the Firetell telephony switch: | Tool Name | Parameters | Description | | :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | | `transfer_call` | `to` _(string, required)_: Phone number (`"+14155552671"`), agent extension (`"agent-ag_005"`), or team name (`"team-te_001"`).
`reason` _(string, optional)_: Brief reason for transfer. | Transfer the current call to another destination (external phone number, agent extension, or queue). | | `end_call` | `reason` _(string, optional)_: Brief reason for ending the call (e.g. `"Conversation completed"`). | Gracefully terminates the phone call when the conversation is finished or caller requests to hang up. | | `hold_call` | `action` _(string, required, enum: `["hold", "resume"]`)_: Whether to put on hold or resume. | Place the caller on hold (or resume call) while looking up information or consulting team members. | | `collect_digits` | `prompt` _(string, required)_: Prompt to say before collecting digits.
`max_digits` _(string, required)_: Max digits (1–20).
`timeout_seconds` _(string, optional, default: `10`)_: Seconds to wait. | Ask the caller to enter DTMF digits using their keypad (e.g. account numbers, OTP, PIN). | | `send_dtmf` | `digits` _(string, required)_: DTMF digits string (e.g. `"1234#"` or `"0"`). | Send DTMF keypad tones to the remote end (useful for navigating IVR menus). | --- ### Custom Webhook Tools Architecture When an agent invokes a Custom Webhook Tool, the call flow works as follows: ``` [Caller speaks: "Where is order ORD-9988?"] │ ▼ [AI Voice Agent (LLM)] (Determines it needs to call check_order) │ ▼ [Firetell AI Gateway] │ HTTP POST ▼ [Your Webhook Endpoint] https://api.yourcompany.com/tools/orders │ │ Returns JSON: │ { "status": "Shipped", "eta": "Tomorrow by 2 PM" } ▼ [Firetell AI Gateway] (Feeds response back to AI Model) │ ▼ [AI speaks: "Your order has shipped and will arrive tomorrow by 2 PM!"] ``` #### 1. Webhook Request Sent by Firetell When a custom tool is triggered, Firetell makes an HTTP request to your configured `webhook_url`: ##### HTTP Headers: ```http Content-Type: application/json User-Agent: Firetell-Voice-Agent-Webhook/1.0 X-Workspace-Id: ws_f82a1c4b9d0e X-Call-Id: call_1787334135262_26bqsku X-Caller-Number: +14155552671 X-Caller-Name: John%20Doe X-Called-Number: +18005550199 X-Voice-Agent-Id: va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c X-Voice-Agent-Name: Customer%20Support%20Agent ``` _(Plus any custom headers defined in `webhook_headers`, such as `Authorization: Bearer YOUR_SECRET`)_ ##### Request Body (for `POST`, `PUT`, `PATCH`): ```json { "event": "tool_call", "timestamp": "2026-08-22T01:45:00.000Z", "workspace_id": "ws_f82a1c4b9d0e", "call_id": "call_1787334135262_26bqsku", "caller": { "number": "+14155552671", "name": "John Doe" }, "called_number": "+18005550199", "voice_agent": { "id": "va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c", "name": "Customer Support Agent" }, "tool": { "id": "vat_9b2e4a1f", "name": "check_order_status" }, "arguments": { "order_id": "ORD-9988" } } ``` :::note For `GET` requests, arguments are passed as URL query parameters (e.g. `?order_id=ORD-9988`). ::: #### 2. Expected Webhook Response Your webhook should respond with **HTTP 200 OK** and a JSON object or string containing the result: ```json { "order_id": "ORD-9988", "status": "Shipped", "carrier": "FedEx", "tracking_number": "123456789012", "estimated_delivery": "Tomorrow by 2:00 PM" } ``` #### 3. Execution Constraints & Best Practices: - **Timeout**: Firetell waits up to **8.0 seconds** for webhook responses before timing out. For optimal real-time conversational latency, ensure your webhook responds within **500–1500 ms**. - **Max Response Size**: Maximum **100 KB** per response payload. - **Error Fallback**: If your webhook returns HTTP 4xx/5xx or times out, Firetell returns an error message to the LLM so it can politely inform the caller (e.g., _"I'm having trouble looking up that order right now. Let me connect you with a representative."_). --- ## Tool Management Endpoints ### List Tools for a Voice Agent ``` GET /voice-agents/:id/tools ``` Retrieve a list of all tools (both built-in and custom) available for a specific voice agent. #### Authentication Requires `ApiKey` with any scope. #### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------------ | ----------------------------------------- | | `page` | number | `1` | Page number | | `limit` | number | `50` | Items per page (max: 100) | | `search` | string | — | Search tools by name or description | | `sort_field` | string | `created_at` | Sort field. Allowed: `created_at`, `name` | | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | #### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/voice-agents/va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c/tools" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` #### Response ```json { "data": [ { "id": "vat_9b2e4a1f", "voice_agent_id": "va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c", "workspace_id": "yourcompany", "name": "check_order_status", "description": "Check order shipping status and estimated delivery time by order ID", "tool_type": "custom", "webhook_url": "https://api.yourdomain.com/tools/check-order", "webhook_method": "POST", "webhook_headers": { "Authorization": "Bearer sec_live_998877" }, "parameters": { "order_id": { "type": "string", "description": "The customer order reference code (e.g. ORD-12345)", "required": true } }, "required_params": ["order_id"], "is_active": true, "created_at": "2026-03-01T10:00:00.000Z", "updated_at": "2026-03-01T10:00:00.000Z" } ], "meta": { "total": 1, "page": 1, "limit": 50, "total_pages": 1 } } ``` --- ### Get a Specific Tool ``` GET /voice-agents/:id/tools/:toolId ``` Retrieve details of a specific tool. #### Authentication Requires `ApiKey` with any scope. #### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/voice-agents/va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c/tools/vat_9b2e4a1f" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` --- ### Create a Custom Tool ``` POST /voice-agents/:id/tools ``` Create a new custom webhook tool for a voice agent. #### Authentication Requires `ApiKey` with `full` scope. #### Request Body | Field | Type | Required | Description | | ----------------- | ------------------------------- | -------- | ------------------------------------------------------------------------------------ | | `name` | string | ✅ | Function name (lowercase alphanumeric with underscores, regex `/^[a-z][a-z0-9_]*$/`) | | `description` | string | ✅ | Clear description for the LLM to know when and how to call this tool (5–1000 chars) | | `webhook_url` | string | — | Target HTTPS endpoint for the webhook | | `webhook_method` | string | — | HTTP method: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. Default: `POST` | | `webhook_headers` | Record\ | — | Custom request headers (e.g. authorization tokens) | | `parameters` | Record\ | — | JSON schema definition of input arguments expected from the LLM | | `required_params` | string[] | — | List of mandatory parameter keys | | `is_active` | boolean | — | Whether the tool is active immediately (default: `true`) | #### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/voice-agents/va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c/tools" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "book_appointment", "description": "Book a service technician appointment for a customer given a date and time slot", "webhook_url": "https://api.yourdomain.com/appointments/book", "webhook_method": "POST", "webhook_headers": { "Authorization": "Bearer sec_live_token" }, "parameters": { "service_type": { "type": "string", "description": "Type of service needed (e.g. internet_repair, router_installation)" }, "preferred_date": { "type": "string", "description": "Date in YYYY-MM-DD format" }, "time_slot": { "type": "string", "description": "Morning (9am-12pm) or Afternoon (1pm-5pm)", "enum": ["morning", "afternoon"] } }, "required_params": ["service_type", "preferred_date", "time_slot"], "is_active": true }' ``` #### Response ```json { "id": "vat_e81c02ab", "voice_agent_id": "va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c", "workspace_id": "yourcompany", "name": "book_appointment", "description": "Book a service technician appointment for a customer given a date and time slot", "tool_type": "custom", "webhook_url": "https://api.yourdomain.com/appointments/book", "webhook_method": "POST", "webhook_headers": { "Authorization": "Bearer sec_live_token" }, "parameters": { "service_type": { "type": "string", "description": "Type of service needed (e.g. internet_repair, router_installation)" }, "preferred_date": { "type": "string", "description": "Date in YYYY-MM-DD format" }, "time_slot": { "type": "string", "description": "Morning (9am-12pm) or Afternoon (1pm-5pm)", "enum": ["morning", "afternoon"] } }, "required_params": ["service_type", "preferred_date", "time_slot"], "is_active": true, "created_at": "2026-08-22T01:45:00.000Z", "updated_at": "2026-08-22T01:45:00.000Z" } ``` --- ### Update a Tool ``` PATCH /voice-agents/:id/tools/:toolId ``` Update properties of an existing tool. :::note - For **Custom Tools**: All fields including `name`, `description`, `webhook_url`, `webhook_headers`, and `parameters` can be updated. - For **Built-in Tools**: Only `is_active` and `description` can be modified. ::: #### Authentication Requires `ApiKey` with `full` scope. #### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/voice-agents/va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c/tools/vat_e81c02ab" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "description": "Book or reschedule a service technician appointment", "is_active": true }' ``` --- ### Delete a Custom Tool ``` DELETE /voice-agents/:id/tools/:toolId ``` Permanently remove a custom webhook tool. Built-in telephony tools cannot be deleted (use `is_active: false` to disable them instead). #### Authentication Requires `ApiKey` with `full` scope. #### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/voice-agents/va_7f3a2b1c-9d4e-5f6a-8b0c-1d2e3f4a5b6c/tools/vat_e81c02ab" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` #### Response ```json { "id": "vat_e81c02ab", "name": "book_appointment", "deleted": true } ``` --- ## Catalog Endpoints ### List AI Models ``` GET /voice-agents/models ``` List available AI models that can be used for voice agents. ### Authentication Requires `ApiKey` with any scope. #### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/voice-agents/models" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` --- ### List TTS Voices ``` GET /voice-agents/voices ``` List available speech-to-speech voices, filtered by the workspace's country code. ### Authentication Requires `ApiKey` with any scope. #### Query Parameters | Parameter | Type | Description | | ---------- | ------ | -------------------------------------------- | | `model_id` | string | Filter voices by model. Max 100 characters. | | `provider` | string | Filter by voice provider. Max 50 characters. | | `page` | number | Page number | | `limit` | number | Items per page | | `search` | string | Search voices by name | #### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/voice-agents/voices?provider=openai" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` --- # Webhooks Configure webhook endpoints to receive real-time HTTP `POST` notifications when events occur in your workspace — such as incoming calls, call state changes, call endings, and contact updates. For details on payload format, event types, retry policy, and signature verification, see the [Webhooks Overview](/docs/webhooks/overview). ## Endpoints | Method | Endpoint | Description | | -------- | -------------------------- | -------------------------- | | `GET` | `/webhooks` | List all webhook endpoints | | `GET` | `/webhooks/:id` | Get webhook details | | `POST` | `/webhooks` | Create a webhook endpoint | | `POST` | `/webhooks/:id/test-event` | Send a test event | | `PATCH` | `/webhooks/:id` | Update a webhook endpoint | | `DELETE` | `/webhooks/:id` | Delete a webhook endpoint | ## The Webhook Object | Field | Type | Description | | -------------- | -------- | --------------------------------------------------------------------- | | `id` | string | Unique webhook identifier (prefixed with `wh-`) | | `workspace_id` | string | Workspace identifier | | `title` | string | Display name for the webhook | | `url` | string | The URL that receives event notifications | | `events` | string[] | Array of event names this webhook is subscribed to | | `headers` | object[] | Custom HTTP headers sent with each delivery | | `is_active` | boolean | Whether the webhook is active | | `secret` | string | Signing secret for signature verification (only returned on creation) | | `created_at` | string | ISO 8601 creation timestamp | | `updated_at` | string | ISO 8601 last update timestamp | ### Webhook Header Object | Field | Type | Description | | ------- | ------ | ------------------------------- | | `key` | string | Header name (1–35 characters) | | `value` | string | Header value (1–255 characters) | ### Available Event Names For complete payload structures and JSON schemas for each event, see the [Webhook Event Catalog](/docs/webhooks/event-catalog). | Event | Description | | ---------------------- | ----------------------------------------------------------------- | | `call.created` | A new call has been initiated | | `call.answered` | A call was answered by an agent or user | | `call.ended` | A call has ended | | `call.recording.ready` | A call recording audio file has finished processing, uploaded, and is ready | | `call.transcription.completed` | Real-time call transcription and Named Entity Recognition (NER) completed | | `call.transcription.summary` | AI post-call executive summary, customer sentiment analysis, and action items | | `contact.created` | A new contact was created in the workspace | | `contact.updated` | An existing contact was updated | | `contact.deleted` | A contact was deleted | | `agent.state` | An agent's presence state was changed (available, busy, away, etc) | | `agent.created` | A new workspace agent account was created | | `agent.updated` | An existing workspace agent account was updated | | `agent.deleted` | A workspace agent account was deleted | | `workspace.credit.updated` | Workspace credit balance was updated (e.g., call charge deduction, top-up) | | `message.received` | An inbound SMS/MMS message was received | | `message.sent` | An outbound SMS/MMS message was accepted by carrier and sent | | `message.delivered` | An outbound SMS/MMS message was confirmed delivered to recipient | | `message.failed` | An outbound SMS/MMS message failed to send or deliver | | `message.undelivered` | An outbound SMS/MMS message could not be confirmed delivered | --- ## List Webhooks ``` GET /webhooks ``` Retrieve a paginated list of all webhook endpoints in the workspace. ### Authentication Requires `ApiKey` with any scope. ### Query Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------------ | --------------------------------- | | `page` | number | `1` | Page number (min: 1) | | `limit` | number | `10` | Items per page (min: 1, max: 100) | | `search` | string | — | Search by title | | `sort_field` | string | `created_at` | Sort field. Allowed: `created_at` | | `sort_order` | string | `desc` | Sort order: `asc` or `desc` | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/webhooks?page=1&limit=10" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "data": [ { "id": "wh-7f3a2b1c9d4e5f6a8b0c1d2e", "workspace_id": "yourcompany", "title": "Call Events - Production", "url": "https://api.example.com/webhooks/firetell", "events": ["call.created", "call.ended"], "headers": [ { "key": "X-Custom-Token", "value": "my-secret-token" } ], "is_active": true, "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-03-20T14:00:00.000Z" }, { "id": "wh-9a8b7c6d5e4f3a2b1c0d9e8f", "workspace_id": "yourcompany", "title": "Contact Sync", "url": "https://crm.example.com/hooks/firetell", "events": ["contact.created", "contact.updated", "contact.deleted"], "headers": [], "is_active": true, "created_at": "2026-02-10T10:00:00.000Z", "updated_at": "2026-02-10T10:00:00.000Z" } ], "meta": { "total": 2, "page": 1, "limit": 10, "total_pages": 1 } } ``` :::info The `secret` field is not included in list responses. It is only returned when creating a webhook. ::: --- ## Get Webhook ``` GET /webhooks/:id ``` Retrieve details of a specific webhook endpoint. ### Authentication Requires `ApiKey` with `full` scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ---------------------------------------------------- | | `id` | string | The webhook ID (e.g., `wh-7f3a2b1c9d4e5f6a8b0c1d2e`) | ### Request ```bash curl -X GET "https://{workspace_id}.firetell.app/api/v1/webhooks/wh-7f3a2b1c9d4e5f6a8b0c1d2e" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response ```json { "id": "wh-7f3a2b1c9d4e5f6a8b0c1d2e", "workspace_id": "yourcompany", "title": "Call Events - Production", "url": "https://api.example.com/webhooks/firetell", "events": ["call.created", "call.ended"], "headers": [ { "key": "X-Custom-Token", "value": "my-secret-token" } ], "is_active": true, "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-03-20T14:00:00.000Z" } ``` ### Error Response ```json { "statusCode": 404, "message": "item not found", "error": "Not Found" } ``` --- ## Create Webhook ``` POST /webhooks ``` Create a new webhook endpoint. A signing secret is automatically generated and returned in the response — **save it securely**, as it will not be retrievable again. ### Authentication Requires `ApiKey` with `full` scope. :::caution Each workspace is limited to a maximum of **20 webhook endpoints**. Exceeding this limit will return a `403 Forbidden` error. ::: ### Request Body | Field | Type | Required | Description | | --------- | -------- | -------- | ------------------------------------------------------------------------------------ | | `title` | string | ✅ | Display name. 1–100 characters. | | `url` | string | ✅ | Webhook URL. Must be a valid HTTPS URL. Max 255 characters. | | `events` | string[] | ✅ | Array of event names to subscribe to. At least 1 event required. | | `headers` | object[] | — | Custom HTTP headers (max 3). Each with `key` (1–35 chars) and `value` (1–255 chars). | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/webhooks" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Call Events - Production", "url": "https://api.example.com/webhooks/firetell", "events": ["call.created", "call.answered", "call.ended"], "headers": [ { "key": "X-Custom-Token", "value": "my-secret-token" } ] }' ``` ### Response ```json { "id": "wh-d4e5f6a7b8c9d0e1f2a3b4c5", "workspace_id": "yourcompany", "title": "Call Events - Production", "url": "https://api.example.com/webhooks/firetell", "events": ["call.created", "call.answered", "call.ended"], "headers": [ { "key": "X-Custom-Token", "value": "my-secret-token" } ], "is_active": true, "secret": "whse-a1b2c3d4e5f6a7b8c9d0e1", "created_at": "2026-07-09T08:00:00.000Z", "updated_at": "2026-07-09T08:00:00.000Z" } ``` :::caution **Save the `secret` value immediately.** The secret (prefixed with `whse-`) is only returned once during creation. You will need it to verify webhook signatures using HMAC-SHA256. ::: ### Error Response **Webhook limit reached:** ```json { "statusCode": 403, "message": "We currently only support up to 20 webhooks with your plan. You've reached your limit.", "error": "Forbidden" } ``` --- ## Send Test Event ``` POST /webhooks/:id/test-event ``` Send a simulated test event to verify your webhook endpoint is working correctly. The test payload is sent immediately to the configured URL with a valid signature. ### Authentication Requires `ApiKey` with `full` scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------- | | `id` | string | The webhook ID | ### Request Body | Field | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------------------------------------------------------------- | | `event_name` | string | ✅ | The event type to simulate. Must be one of the events the webhook is subscribed to. | ### Request ```bash curl -X POST "https://{workspace_id}.firetell.app/api/v1/webhooks/wh-7f3a2b1c9d4e5f6a8b0c1d2e/test-event" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "event_name": "call.created" }' ``` ### Success Response ```json { "success": true, "status": 200, "status_text": "OK", "latency_ms": 142, "message": "Webhook test event with respond success" } ``` ### Failure Responses **Endpoint returned non-success status:** ```json { "success": false, "status": 500, "status_text": "Internal Server Error", "latency_ms": 85, "error": "Internal Server Error", "message": "Webhook endpoint did not respond success. status >= 300" } ``` **Endpoint unreachable:** ```json { "success": false, "status": 0, "latency_ms": 0, "error": "ECONNREFUSED", "message": "Webhook endpoint did not respond" } ``` **Event not subscribed:** ```json { "statusCode": 400, "message": "This endpoint is not currently subscribed to this event", "error": "Bad Request" } ``` ### Test Payload Example When simulating a `call.created` event, the following test payload is sent to your URL: ```json { "event": "call.created", "data": { "workspace_id": "yourcompany", "call_id": "test_call_id_1720512000000", "direction": "inbound", "caller": { "name": "Caller_name", "number": "Caller_number" }, "callee": { "name": "Callee_name", "number": "Callee_number" }, "created_at": "2026-07-09T08:00:00.000Z" } } ``` :::tip Use the test event endpoint during development to verify your webhook handler is correctly parsing payloads and validating signatures before going live. ::: --- ## Update Webhook ``` PATCH /webhooks/:id ``` Update an existing webhook endpoint's configuration. All fields in the request body are required — this endpoint replaces the webhook configuration entirely. ### Authentication Requires `ApiKey` with `full` scope. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------- | | `id` | string | The webhook ID | ### Request Body | Field | Type | Required | Description | | --------- | -------- | -------- | ----------------------------------------------------------- | | `title` | string | ✅ | Display name. 1–100 characters. | | `url` | string | ✅ | Webhook URL. Must be a valid HTTPS URL. Max 255 characters. | | `events` | string[] | ✅ | Array of event names. At least 1 event required. | | `headers` | object[] | — | Custom HTTP headers (max 3). | ### Request ```bash curl -X PATCH "https://{workspace_id}.firetell.app/api/v1/webhooks/wh-7f3a2b1c9d4e5f6a8b0c1d2e" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Call Events - Updated", "url": "https://api.example.com/webhooks/firetell-v2", "events": ["call.created", "call.ended"], "headers": [] }' ``` ### Response Returns the updated webhook object: ```json { "id": "wh-7f3a2b1c9d4e5f6a8b0c1d2e", "workspace_id": "yourcompany", "title": "Call Events - Updated", "url": "https://api.example.com/webhooks/firetell-v2", "events": ["call.created", "call.ended"], "headers": [], "is_active": true, "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-07-09T09:30:00.000Z" } ``` :::info Updating a webhook does **not** regenerate the signing secret. The original secret remains valid. ::: ### Error Response ```json { "statusCode": 404, "message": "item not found", "error": "Not Found" } ``` --- ## Delete Webhook ``` DELETE /webhooks/:id ``` Permanently delete a webhook endpoint. Event notifications will immediately stop being delivered to this URL. ### Authentication Requires `ApiKey` with `full` scope. :::caution This action is irreversible. Any events that occur after deletion will not be delivered to this endpoint. ::: ### Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------- | | `id` | string | The webhook ID | ### Request ```bash curl -X DELETE "https://{workspace_id}.firetell.app/api/v1/webhooks/wh-7f3a2b1c9d4e5f6a8b0c1d2e" \ -H "Authorization: ApiKey sk-YOUR_API_KEY" ``` ### Response Returns the deleted webhook object: ```json { "id": "wh-7f3a2b1c9d4e5f6a8b0c1d2e", "workspace_id": "yourcompany", "title": "Call Events - Production", "url": "https://api.example.com/webhooks/firetell", "events": ["call.created", "call.ended"], "headers": [], "is_active": true, "created_at": "2026-01-15T08:30:00.000Z", "updated_at": "2026-03-20T14:00:00.000Z" } ``` ### Error Response ```json { "statusCode": 404, "message": "item not found", "error": "Not Found" } ``` --- ## Webhook Delivery ### Signature Verification Every webhook delivery includes an `X-Webhook-Signature` header containing an HMAC-SHA256 signature of the request body, signed with your webhook's `secret`. ```javascript const crypto = require("crypto"); function verifyWebhookSignature(rawBody, signature, secret) { const expected = crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); } // In your Express handler: app.post("/webhooks/firetell", (req, res) => { const signature = req.headers["x-webhook-signature"]; const isValid = verifyWebhookSignature( JSON.stringify(req.body), signature, "whse-your-webhook-secret", ); if (!isValid) { return res.status(401).send("Invalid signature"); } // Process the event... res.status(200).send("OK"); }); ``` ### Delivery Headers Each webhook delivery includes the following headers: | Header | Description | | --------------------- | ------------------------------------------------------------------------------ | | `Content-Type` | Always `application/json` | | `X-Webhook-Signature` | HMAC-SHA256 signature of the request body | | `X-Powered-By` | Firetell service identifier | | `User-Agent` | Firetell webhook delivery agent (e.g., `region@firetell.shared.webhook/1.0.2`) | | Custom headers | Any headers you configured on the webhook endpoint | ### Timeout & Retries - **Timeout:** 5 seconds per delivery attempt - **Redirect:** Not followed (`maxRedirects: 0`) - Your endpoint must respond with a status code **below 300** to be considered successful - See [Webhooks Overview](/docs/webhooks/overview) for the retry policy --- # API Reference This document provides a detailed description of the classes, methods, streams, and types exposed by `firetell_flutter_sdk`. --- ## FiretellClient The main client managing session initialization, REST call dispatch, SSE event streaming, and active call lifecycle. ### Constructor ```dart FiretellClient({ required String jwt, required String domain, }) ``` | Parameter | Type | Description | |-----------|------|-------------| | `jwt` | `String` | Agent JWT authentication token | | `domain` | `String` | Workspace API domain (e.g. `yourcompany.firetell.app`) | --- ### Properties #### `ready` - **Type:** `Future` - **Description:** Resolves when workspace metadata is fetched, SSE stream is connected, and the session is ready. #### `isConnected` - **Type:** `bool` - **Description:** Returns `true` if the SSE event stream is currently connected. #### `activeCalls` - **Type:** `Map` - **Description:** Map of currently active calls, keyed by call ID. #### `iceServers` - **Type:** `List>` - **Description:** ICE servers from workspace metadata (or defaults). #### `baseUrl` - **Type:** `String` - **Description:** Workspace base URL. #### `jwt` - **Type:** `String` - **Description:** Current JWT token. #### `session` - **Type:** `Session?` - **Description:** Current session info. `null` before authentication or after logout. --- ### Event Streams | Stream | Type | Description | |--------|------|-------------| | `onCallRing` | `Stream` | Incoming call ring notification (from SSE) | | `onCallOffer` | `Stream` | Incoming call with WebRTC ready to answer | | `onCallCreated` | `Stream>` | New call created in workspace | | `onCallStarted` | `Stream>` | Call ringing at destination | | `onCallAnswered` | `Stream>` | Call answered | | `onCallEnded` | `Stream>` | Call ended | | `onCallCanceled` | `Stream>` | Call canceled before answer | | `onAgentState` | `Stream>` | Agent state changed | | `onSession` | `Stream` | Session state changed | | `onError` | `Stream` | Error event | | `onConnectionState` | `Stream` | SSE connection state changes | --- ### Methods #### `makeOutboundCall()` ```dart Future makeOutboundCall({ required String to, String? from, bool isVideo = false, }) ``` Creates a `Call`, sets up WebRTC media, gathers Full ICE candidates, calls the REST API, connects the per-call WebSocket, and sends the SDP offer. #### `getPhoneNumbers()` ```dart Future> getPhoneNumbers({int page = 1, int limit = 100}) Future getPhoneNumbersResponse({int page = 1, int limit = 100}) ``` Fetch phone numbers (DIDs) accessible by the authenticated agent/team from `GET /api/v1/call-center/phone-numbers`. Use the returned number as `from` (Caller ID) in `makeOutboundCall`. #### `handlePushIncomingCall()` ```dart Future handlePushIncomingCall(CallRingParams params) ``` Creates a `Call` from push notification parameters and connects the signaling WebSocket. The call is **not** auto-answered — call `call.accept()` when the user taps answer. #### `sendTransfer()` ```dart Future sendTransfer(String callId, String target, {String? reason}) ``` Transfer a call via WebSocket (if active) or REST API fallback. #### `logout()` ```dart Future logout({String? deviceId}) ``` Unregisters the device push token on the backend (`POST /api/v1/me/logout`), hangs up all active calls, closes the SSE stream, and cleans up the local session. Defaults to `DeviceIdHelper.getOrCreate()` if `deviceId` is omitted. #### `destroy()` Fully destroys the client immediately without server communication. --- ## Call Represents a single VoIP call session with dedicated WebSocket signaling and WebRTC peer connection. ### Constructor ```dart Call({ required List> iceServers, CallOptions options = const CallOptions(), }) ``` ### Properties | Property | Type | Description | |----------|------|-------------| | `callId` | `String?` | Unique call identifier | | `to` | `String` | Destination number/extension | | `from` | `String` | Caller number/extension | | `fromName` | `String` | Caller display name | | `callState` | `CallState` | Current call state | | `active` | `bool` | Whether the call is currently active | | `isVideo` | `bool` | Whether the call includes video | | `isCameraOff` | `bool` | Whether local camera transmission is disabled/muted | | `isMuted` | `bool` | Whether the microphone is muted | | `isSpeakerOn` | `bool` | Whether audio is routed to speakerphone (vs earpiece) | | `isHold` | `bool` | Whether the call is on hold | ### Event Streams | Stream | Type | Description | |--------|------|-------------| | `onStateChange` | `Stream<({CallState state, String? reason, Map? data})>` | Call state changes | | `onLocalStream` | `Stream` | Local camera/microphone media stream | | `onRemoteStream` | `Stream` | Remote audio/video stream | | `onMuteChange` | `Stream` | Microphone mute state changes | | `onCameraChange` | `Stream` | Camera state changes (`true` = off, `false` = on) | | `onSpeakerChange` | `Stream` | Speakerphone state changes | ### Methods #### `accept()` ```dart Future accept() ``` Accept an incoming call — sets up WebRTC media (audio and video if SDP contains video), creates SDP answer, gathers Full ICE candidates, and sends the answer via WebSocket. #### `reject()` ```dart Future reject() ``` Reject an incoming call via WebSocket. #### `rejectViaHttp()` ```dart Future rejectViaHttp({ required String baseUrl, required String callToken, }) ``` Reject via fast HTTP endpoint (~50ms). Preferred for push-triggered reject when no WebSocket is connected. #### `hangup()` ```dart Future hangup() ``` End the active call. Sends `call.hangup` via WebSocket and cleans up WebRTC resources. #### `mute()` / `unmute()` / `toggleMute()` ```dart Future mute() Future unmute() Future toggleMute() ``` #### `muteVideo()` / `unmuteVideo()` / `toggleCamera()` ```dart Future muteVideo() Future unmuteVideo() Future toggleCamera() ``` Mute or resume local video track transmission and notify remote peer via `call.camera`. #### `switchCamera()` ```dart Future switchCamera() ``` Switch between the front-facing and rear-facing device cameras. #### `setSpeakerphoneOn()` / `toggleSpeaker()` ```dart Future setSpeakerphoneOn(bool enable) Future toggleSpeaker() ``` Toggle audio output between the device speakerphone (loudspeaker) and earpiece. #### `onhold()` / `unhold()` ```dart Future onhold() Future unhold() ``` Hold/unhold via SDP renegotiation (changes transceiver direction, re-gathers ICE, sends new SDP). #### `sendDTMF()` ```dart void sendDTMF(String digit) ``` Send a DTMF digit (`0-9`, `*`, `#`, `A-D`). #### `transfer()` ```dart Future transfer(String target, {String? reason}) ``` Transfer the call to another target (extension, phone number, team ID, SIP account). #### `connectSignaling()` ```dart Future connectSignaling(String wsUrl, String callToken) ``` Connect the per-call WebSocket and authenticate with `call_token`. Must complete within 3 seconds. #### `destroy()` ```dart void destroy({bool sendHangup = true}) ``` Clean up all resources (WebSocket, WebRTC, streams). --- ## Models ### `Session` ```dart class Session { final String sessionId; final String username; final String displayName; final String domain; final int expiresAt; // Unix timestamp in milliseconds } ``` ### `CallOptions` ```dart class CallOptions { final String to; final String from; final String fromName; final String? fromAvatar; final bool isVideo; final bool isTransfer; final String? transferReason; } ``` ### `CallRingParams` ```dart class CallRingParams { final String callId; final String callToken; final String? wsUrl; final String callerNumber; final String callerName; final String? callerAvatar; final String calleeNumber; final String calleeName; final bool isTransfer; final String? transferReason; final bool isVideo; final int ringTimeoutSecs; } ``` Factory constructors: ```dart CallRingParams.fromFcmData(Map data) CallRingParams.fromApnsPayload(Map payload) CallRingParams.fromMap(Map map) ``` ### `MakeCallResponse` ```dart class MakeCallResponse { final String callId; final String callToken; final String wsUrl; } ``` --- ## Enums ### `CallState` ```dart enum CallState { none, initiated, trying, ringing, answered, active, onHold, ended, error, cancel } ``` ### `SseConnectionState` ```dart enum SseConnectionState { connecting, connected, disconnected, reconnecting, error } ``` --- # Changelog All notable changes to the [`firetell_flutter_sdk`](https://pub.dev/packages/firetell_flutter_sdk) package are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- ## [1.1.0] - 2026-09-14 ### Added - **Mobile Video Call Support**: - Full video calling support matching the core capabilities of the browser SDK (excluding screen sharing for mobile ergonomics). - Camera controls on `Call`: - `isCameraOff` boolean property to check current camera transmission status. - `muteVideo()` to disable local video tracks and broadcast state to remote peer via `call.camera`. - `unmuteVideo()` to re-enable local video tracks and broadcast state to remote peer via `call.camera`. - `toggleCamera()` convenience method to toggle camera on/off. - `switchCamera()` to switch between front-facing and rear-facing device cameras using `Helper.switchCamera`. - `onCameraChange` broadcast stream to listen for local camera mute/unmute state changes. - SDP video auto-detection: - Automatically detects video media description (`m=video`) in remote SDP offers received via WebSocket signaling (`call.offer`). - Sets `isVideo = true` in `Call.accept()` if SDP offer contains video, ensuring matching audio/video media negotiation. - WebSocket signaling updates: - Added `call.camera` message handler in `_handleWsMessage` to synchronize camera state events between peers. - **Example Application Video Calling**: - Created dedicated full-screen `VideoCallScreen` (`video_call_screen.dart`) with local Picture-in-Picture preview, remote video feed rendering, camera switch button, camera mute/unmute toggle, microphone mute, speakerphone toggle, and call duration badge. - Added dedicated Video Call action button on the dial pad alongside the audio call button. - Updated in-app incoming call dialog to show video calling indicators when `call.isVideo` is true. - Exported `package:flutter_webrtc/flutter_webrtc.dart` from package library entry point for seamless UI renderer integration (`RTCVideoRenderer`, `RTCVideoView`, `RTCVideoViewObjectFit`). - **Platform Permissions**: - Added `android.permission.CAMERA` and `NSCameraUsageDescription` setup guidance in documentation and README. --- ## [1.0.3] - 2026-09-14 ### Added - **Speakerphone Control**: - Added `isSpeakerOn` boolean property to `Call` to check current speaker status. - Added `setSpeakerphoneOn(bool enable)` to route audio between device loudspeaker and earpiece. - Added `toggleSpeaker()` convenience method to toggle speakerphone on/off. - Added `onSpeakerChange` broadcast stream to listen for speaker state changes. - Automatically resets speakerphone to earpiece on call cleanup. - **Phone Numbers (DIDs) & Caller ID Support**: - Added `PhoneNumber`, `PhoneNumberCapabilities`, and `PhoneNumbersResponse` models. - Added `client.getPhoneNumbers({page, limit})` and `client.getPhoneNumbersResponse({page, limit})` to fetch agent/team accessible phone numbers for outbound Caller ID (`from`). - Updated example `DialpadScreen` to automatically fetch available DIDs and provide an interactive Caller ID dropdown selector when dialing outbound. - **Backend Device Push Token Cleanup on Logout**: - Upgraded `FiretellClient.logout({deviceId})` to an asynchronous method that calls `POST /api/v1/me/logout` with `device_id` to unregister push tokens on the server, ensuring the device stops receiving VoIP incoming calls after logout. - Automatically resolves `deviceId` from `DeviceIdHelper.getOrCreate()` if not explicitly specified. - **Example Application**: - Added a dedicated Speaker/Earpiece toggle button with live state feedback to the active call screen. - Added dynamic Caller ID selector on the Dial Pad screen. - Streamlined logout flow via `await client.logout()`. --- ## [1.0.2] - 2026-09-14 ### Changed - **Upgraded Dependencies**: - Updated to modern packages: `flutter_callkit_incoming: ^3.1.5`, `flutter_webrtc: ^1.6.2`, `web_socket_channel: ^3.0.3`, `http: ^1.6.0`, `uuid: ^4.6.0`, `shared_preferences: ^2.5.5`, `device_info_plus: ^13.2.0`. - **flutter_callkit_incoming 3.x Compatibility**: - Adapted `CallKitHandler` and example handlers to support `flutter_callkit_incoming 3.x` sealed class event hierarchy (`CallEventActionCallAccept`, `CallEventActionCallDecline`, etc.). - Configured `missedCallNotification:` and `callingNotification:` in `CallKitParams`. - Resolved `CallEvent` naming conflict with SDK's internal enum via explicit namespace aliasing. ### Fixed - **WebRTC Local Description Handling**: - Fixed `localDescription` getter error in `flutter_webrtc` by utilizing asynchronous `await pc.getLocalDescription()`. - **Linter & Code Health**: - Fixed `unawaited_return_in_try_block` lint in WebSocket connect handshake. - Replaced unhandled `catchError` with clean `unawaited` try/catch block in `FiretellClient`. - Added unnamed `library;` directive for Dart 3 library doc comment compliance. - Renamed documentation directory to `doc/` and main markdown file to `README.md` per pub.dev package layout guidelines. --- ## [1.0.1] - 2026-09-14 ### Fixed - **iOS VoIP Push Token Registration**: - Differentiated Apple PushKit VoIP tokens (`PKPushRegistry`) from standard Firebase FCM tokens. - Used `FlutterCallkitIncoming.getDevicePushTokenVoIP()` to obtain the raw 64-character hex APNs VoIP token on iOS for `POST /api/v1/me/devices/voip-push-token`. - Maintained FCM token registration for standard notification push (`POST /api/v1/me/devices/notification-push-token`) to handle background call cancellations (`call.canceled`) and endings (`call.ended`) across both iOS and Android. - Updated FCM token refresh listener to automatically synchronize updated tokens based on the running platform. --- ## [1.0.0] - 2026-09-14 ### Added - **Core WebRTC Calling**: - `FiretellClient` class for managing sessions, tokens, device registrations, and SSE event streaming. - `Call` class implementing full WebRTC lifecycle (`makeCall`, `accept`, `reject`, `hangup`, `hold`/`unhold`, `mute`/`unmute`, `sendDTMF`). - Native WebSocket per-call signaling protocol using event-based messages (`session.connect`, `call.offer`, `call.answer`, `call.ice_candidate`, `call.hangup`). - Full ICE candidate gathering before sending SDP offer/answer for maximum NAT traversal reliability. - **VoIP Push Notification & Cold-Start Support**: - `PushTokenService` for registering device VoIP push tokens (`fcm_voip`, `apns_voip`, `fcm_data`) with backend REST API. - `DeviceIdHelper` providing persistent unique device UUID identification across app restarts (`SharedPreferences`). - `IceServerCache` for offline and cold-start STUN/TURN server caching. - `CallKitHandler` and `ColdStartCallHandler` patterns enabling instant incoming call connection from locked screen or killed app state using push payload credentials (`ws_url` and `call_token`). - **Reactive Event Streams**: - Stream-based client events: `onCallOffer`, `onCallAccepted`, `onCallEnded`, `onCallRejected`, `onCallRinging`. - Granular call state streams: `onStateChanged`, `onRemoteStream`, `onLocalStream`, `onEvent`. - **Comprehensive Example App**: - Complete Flutter demo app covering login, push token registration, incoming call notification with CallKit, active call UI, and audio stream routing. - **Documentation**: - Internal architecture, WebRTC call flow diagrams, and VoIP push integration guides under `doc/`. --- # Installation & Setup The `firetell_flutter_sdk` package enables native iOS and Android apps to make and receive VoIP calls using WebRTC, with full support for push notifications, CallKit (iOS), and ConnectionService (Android). - **Source:** [`firetell_flutter_sdk`](https://github.com/firetellcom/firetell-flutter-sdk) - **Platform:** iOS 13.0+ / Android API 24+ - **Language:** Dart 3.0+, Flutter 3.10+ --- ## 1. Install via Git Add to your `pubspec.yaml`: ```yaml dependencies: firetell_flutter_sdk: git: url: https://github.com/firetellcom/firetell-flutter-sdk.git ref: main ``` Then run: ```bash flutter pub get ``` --- ## 2. Android Setup Add the following permissions to `android/app/src/main/AndroidManifest.xml`: ```xml ``` For Firebase Cloud Messaging, follow the [Firebase Flutter setup guide](https://firebase.google.com/docs/flutter/setup) and add `google-services.json` to `android/app/`. --- ## 3. iOS Setup Add to `ios/Runner/Info.plist`: ```xml NSMicrophoneUsageDescription Firetell needs microphone access for VoIP calls UIBackgroundModes voip audio fetch remote-notification ``` In Xcode, enable the following capabilities: - **Push Notifications** - **Background Modes** → Voice over IP, Audio, AirPlay, and Picture in Picture, Background fetch, Remote notifications For Apple VoIP push (PushKit), configure your Apple VoIP Services Certificate (`.p12` or `.p8` Auth Key) in the **Firetell Console** under *Workspace Settings → Push Notifications*. For regular FCM notifications, add your `GoogleService-Info.plist` to `ios/Runner/`. --- ## 4. Import the SDK ```dart import 'package:firetell_flutter_sdk/firetell_flutter_sdk.dart'; ``` This single import gives you access to all public classes: | Class | Description | |-------|-------------| | `FiretellClient` | Main client — authentication, SSE events, call management | | `Call` | Per-call WebRTC + WebSocket signaling | | `CallRingParams` | Incoming call notification payload | | `CallOptions` | Outbound call configuration | | `Session` | Authenticated session info | | `PushTokenService` | Register VoIP/notification push tokens | | `CallKitHandler` | Bridge between `flutter_callkit_incoming` and SDK | | `DeviceIdHelper` | Native device ID (iOS/Android) | | `IceServerCache` | Local cache for workspace ICE servers | --- ## ⚠️ Security Warning > [!WARNING] > **NEVER hardcode your Master API Key (`sk-...`) in mobile app code.** > > Mobile apps can be decompiled. Embedding your master API Key gives anyone full access to your workspace. To authenticate mobile clients safely: 1. Keep your Master API Key (`sk-...`) on your **backend server**. 2. When a user logs in, your backend generates a temporary **Agent JWT Token**. 3. Pass the JWT token to the mobile app (e.g., via your login API response). 4. Use the JWT to initialize `FiretellClient`. For JWT generation details, see the [Client Authentication Guide](/docs/sdks/javascript/authentication) (the JWT format is the same across all SDKs). --- ## Dependencies The SDK depends on the following packages (installed automatically): | Package | Purpose | |---------|---------| | [`flutter_webrtc`](https://pub.dev/packages/flutter_webrtc) | WebRTC peer connection & media | | [`web_socket_channel`](https://pub.dev/packages/web_socket_channel) | Per-call WebSocket signaling | | [`http`](https://pub.dev/packages/http) | REST API & SSE stream | | [`uuid`](https://pub.dev/packages/uuid) | Session ID generation | | [`shared_preferences`](https://pub.dev/packages/shared_preferences) | Device ID & ICE server cache | | [`device_info_plus`](https://pub.dev/packages/device_info_plus) | Native device ID | | [`flutter_callkit_incoming`](https://pub.dev/packages/flutter_callkit_incoming) | CallKit (iOS) & ConnectionService (Android) UI | --- # Quickstart Guide This guide walks through the core workflows of the Firetell Flutter SDK: connecting, making outbound calls, handling incoming calls (foreground + push), and controlling active calls. --- ## Step 1: Initialize the Client Create a `FiretellClient` with your agent JWT and workspace domain. Wait for the `ready` future to resolve before making calls. ```dart import 'package:firetell_flutter_sdk/firetell_flutter_sdk.dart'; final client = FiretellClient( jwt: 'your-agent-jwt-token', domain: 'yourcompany.firetell.app', ); try { final session = await client.ready; print('Authenticated as: ${session.username}'); } catch (e) { print('Initialization failed: $e'); } ``` During initialization, the SDK: 1. Decodes the JWT to extract agent identity 2. Fetches workspace metadata (ICE servers, WS URLs) via REST 3. Connects the SSE real-time event stream 4. Caches ICE servers locally for push-originated calls --- ## Step 2: Listen to Real-time Events (SSE) Subscribe to streams on the client to respond to incoming calls, agent status updates, and connection state changes. ```dart // Connection state client.onConnectionState.listen((state) { print('SSE: ${state.name}'); // connecting, connected, disconnected, reconnecting }); // Incoming call ring notification (shows caller info before WebRTC connects) client.onCallRing.listen((CallRingParams params) { print('Incoming: ${params.callerName} (${params.callerNumber})'); // → Show Ringing UI & play ringtone }); // Incoming call WebRTC offer ready to answer client.onCallOffer.listen((Call call) { print('Call offer ready: ${call.callId}'); // → Show Answer / Decline buttons }); // Agent state changes in the workspace client.onAgentState.listen((data) { print('Agent ${data['username']} is now ${data['state']}'); }); // Errors client.onError.listen((error) { print('Client error: $error'); }); ``` --- ## Step 3: Make an Outbound Call Use `makeOutboundCall()` to initiate a call. The SDK handles WebRTC setup, Full ICE gathering, REST API call, and WebSocket signaling automatically. ```dart final call = await client.makeOutboundCall( to: '+84901234567', // Phone number, extension, or SIP URI from: '84281234567', // Optional: outbound caller ID isVideo: false, ); // Monitor call lifecycle call.onStateChange.listen((event) { print('State: ${event.state}'); // initiated → ringing → active → ended if (event.state == CallState.ended || event.state == CallState.error || event.state == CallState.cancel) { print('Call terminated. Reason: ${event.reason}'); } }); // Remote audio stream (plays automatically for audio calls) call.onRemoteStream.listen((stream) { if (stream != null) { print('Remote audio stream received'); } }); ``` --- ## Step 4: Handle Incoming Calls ### Foreground (SSE) When the app is in the foreground, incoming calls arrive via the SSE stream: ```dart client.onCallRing.listen((params) { // Show incoming call UI with caller info showIncomingCallUI(params.callerName, params.callerNumber); }); client.onCallOffer.listen((call) async { // User taps "Answer" await call.accept(); // Audio is now flowing ✅ // User taps "Decline" // await call.reject(); }); ``` ### Background / Killed (VoIP Push) When the app is in the background or terminated, calls arrive via push notifications: ```dart // main.dart — top-level background handler @pragma('vm:entry-point') Future _backgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(); if (message.data['event'] == 'call.ring') { final params = CallRingParams.fromFcmData(message.data); // Show native CallKit (iOS) / full-screen notification (Android) await FlutterCallkitIncoming.showCallkitIncoming(CallKitParams( id: params.callId, nameCaller: params.callerName, handle: params.callerNumber, extra: params.toMap(), // ← stores ws_url + call_token for answer handler )); } } ``` When the user answers from the lock screen: ```dart // Uses ws_url + call_token from push payload directly — no FiretellClient needed FlutterCallkitIncoming.onEvent.listen((event) async { if (event?.event == Event.actionCallAccept) { final extra = (event!.body as Map)['extra'] as Map; final params = CallRingParams.fromMap(extra); final iceServers = await IceServerCache.load(); // cached from last session final call = Call(iceServers: iceServers); call.callId = params.callId; await call.connectSignaling(params.wsUrl!, params.callToken); await call.accept(); // Audio flows ✅ } }); ``` For complete push notification setup, see [VoIP Push Notifications](voip-push). --- ## Step 5: Call Controls ```dart // Mute / Unmute await call.mute(); await call.unmute(); await call.toggleMute(); // Hold / Unhold (SDP renegotiation) await call.onhold(); await call.unhold(); // DTMF digits call.sendDTMF('1'); call.sendDTMF('#'); // Transfer await call.transfer('+1987654321', reason: 'Customer request'); // Hang up await call.hangup(); ``` --- ## Step 6: Register Push Tokens Register push tokens after login so the backend can send VoIP push notifications for incoming calls: ```dart final deviceId = await DeviceIdHelper.getOrCreate(); final fcmToken = await FirebaseMessaging.instance.getToken(); await PushTokenService.registerVoipPushToken( baseUrl: client.baseUrl, jwt: client.jwt, pushToken: fcmToken!, deviceId: deviceId, platform: Platform.isIOS ? 'ios' : 'android', ); ``` --- ## Step 7: Logout ```dart // Unregister push tokens + cleanup await PushTokenService.logout( baseUrl: client.baseUrl, jwt: client.jwt, deviceId: await DeviceIdHelper.getOrCreate(), ); // Destroy client (hangs up active calls, closes SSE) client.destroy(); ``` --- # VoIP Push Notifications This guide covers how to wake your Flutter app for incoming calls when it is in the background or completely terminated, using Firebase Cloud Messaging (FCM) on Android and APNs VoIP push on iOS. --- ## Architecture ``` Caller dials → Firetell Backend → Push Notification │ ┌──────────────────┴──────────────────┐ ▼ ▼ FCM (Android) APNs VoIP (iOS) │ │ ▼ ▼ Background Handler PushKit Delegate │ │ ▼ ▼ flutter_callkit_incoming Native CallKit UI (full-screen notification) (lock screen ring) │ │ └──────────────────┬──────────────────┘ ▼ User answers / declines │ ┌──────────────────┴──────────────────┐ ▼ ▼ Answer: Decline: Call(ws_url, call_token) rejectViaHttp(call_token) → connectSignaling() → POST /calls/{id}/reject → accept() → ~50ms ✅ → audio flows ✅ ``` **Key insight:** The push payload contains `ws_url` and `call_token` — everything needed to connect the call **directly**, without initializing a full `FiretellClient` or fetching workspace metadata. --- ## Setup ### 1. Firebase Configuration ```bash flutter pub add firebase_core firebase_messaging flutterfire configure ``` ### 2. Register Push Tokens Call this **after** `client.ready` resolves (after login): ```dart import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; import 'package:firetell_flutter_sdk/firetell_flutter_sdk.dart'; Future registerPushTokens(FiretellClient client) async { // Request permission (required on iOS) await FirebaseMessaging.instance.requestPermission( alert: true, badge: true, sound: true, criticalAlert: true, ); final deviceId = await DeviceIdHelper.getOrCreate(); final isIOS = !kIsWeb && Platform.isIOS; // 1. Obtain VoIP Push Token // - iOS: Apple PushKit token (raw 64-character hex string) via flutter_callkit_incoming // - Android: FCM High-Priority Data message token String? voipToken; if (isIOS) { try { final token = await FlutterCallkitIncoming.getDevicePushTokenVoIP(); if (token.isNotEmpty) voipToken = token; } catch (e) { debugPrint('Failed to get iOS VoIP token: $e'); } } else { voipToken = await FirebaseMessaging.instance.getToken(); } // Register VoIP Push Token (triggers call.ring pushes) if (voipToken != null && voipToken.isNotEmpty) { await PushTokenService.registerVoipPushToken( baseUrl: client.baseUrl, jwt: client.jwt, pushToken: voipToken, deviceId: deviceId, platform: isIOS ? 'ios' : 'android', ); } // 2. Register Notification Token (for call.canceled / call.ended background dismissals) final fcmToken = await FirebaseMessaging.instance.getToken(); if (fcmToken != null && fcmToken.isNotEmpty) { await PushTokenService.registerNotificationPushToken( baseUrl: client.baseUrl, jwt: client.jwt, notificationToken: fcmToken, deviceId: deviceId, platform: isIOS ? 'ios' : 'android', ); } } ``` ### 3. Background Message Handler Add a **top-level function** in `main.dart`: ```dart @pragma('vm:entry-point') Future _backgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(); final event = message.data['event'] as String?; if (event == 'call.ring') { final params = CallRingParams.fromFcmData(message.data); await _showNativeIncomingCall(params); } else if (event == 'call.canceled' || event == 'call.ended') { final callId = message.data['call_id'] as String?; if (callId != null) { await FlutterCallkitIncoming.endCall(callId); } } } void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); FirebaseMessaging.onBackgroundMessage(_backgroundHandler); runApp(const MyApp()); } ``` ### 4. Show Native Incoming Call UI ```dart Future _showNativeIncomingCall(CallRingParams params) async { await FlutterCallkitIncoming.showCallkitIncoming(CallKitParams( id: params.callId, nameCaller: params.callerName.isNotEmpty ? params.callerName : params.callerNumber, handle: params.callerNumber, type: params.isVideo ? 1 : 0, duration: params.ringTimeoutSecs * 1000, extra: params.toMap(), // ← CRITICAL: stores ws_url + call_token ios: const IOSParams( supportsVideo: false, maximumCallGroups: 1, maximumCallsPerCallGroup: 1, audioSessionMode: 'voiceChat', audioSessionActive: true, audioSessionPreferredSampleRate: 44100.0, audioSessionPreferredIOBufferDuration: 0.005, ), android: const AndroidParams( isShowFullLockedScreen: true, ), notification: const NotificationParams( showNotification: true, isShowMissedCallNotification: true, // Automatically triggers missed call alert on timeout ), )); } ``` :::caution The `extra: params.toMap()` is **critical**. It stores the push payload (including `ws_url` and `call_token`) inside the `CallKitParams` so the answer handler can retrieve them later. ::: ### 5. Handle Answer / Decline (Cold Start) Register a **global** CallKit event listener in `main()` **before** `runApp()`: ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); FirebaseMessaging.onBackgroundMessage(_backgroundHandler); // Global listener — captures answer/decline even on cold start FlutterCallkitIncoming.onEvent.listen((CallEvent? event) async { if (event == null) return; switch (event.event) { case Event.actionCallAccept: await _handleAnswer(event); case Event.actionCallDecline: await _handleDecline(event); default: break; } }); runApp(const MyApp()); } ``` #### Answering ```dart Future _handleAnswer(CallEvent event) async { final body = event.body as Map?; final extra = body?['extra'] as Map?; if (extra == null) return; final params = CallRingParams.fromMap(extra); final iceServers = await IceServerCache.load(); // cached workspace TURN servers final call = Call( iceServers: iceServers, options: CallOptions( to: params.calleeNumber, from: params.callerNumber, fromName: params.callerName, ), ); call.callId = params.callId; // Connect WS + accept — uses ws_url + call_token from push payload await call.connectSignaling(params.wsUrl!, params.callToken); await call.accept(); // Audio flows ✅ — navigate to call screen } ``` #### Declining ```dart Future _handleDecline(CallEvent event) async { final body = event.body as Map?; final extra = body?['extra'] as Map?; if (extra == null) return; final callId = body?['id']?.toString(); final callToken = extra['call_token'] as String?; final wsUrl = extra['ws_url'] as String?; if (callId != null && callToken != null && wsUrl != null) { // Convert wss://host/ws → https://host final uri = Uri.parse(wsUrl); final baseUrl = '${uri.scheme == 'wss' ? 'https' : 'http'}://${uri.host}'; final call = Call(iceServers: []); call.callId = callId; await call.rejectViaHttp(baseUrl: baseUrl, callToken: callToken); // Rejected in ~50ms ✅ } } ``` --- ## Push Payload Format ### `call.ring` ```json { "event": "call.ring", "call_id": "cl_abc123", "call_token": "eyJhbGci...", "ws_url": "wss://ws_abc.firetell.app/ws", "from": { "number": "+84901234567", "name": "Nguyen Van A", "avatar": "https://..." }, "to": { "number": "1001", "name": "Agent Smith" }, "is_video": false, "is_transfer": false } ``` ### `call.canceled` / `call.ended` ```json { "event": "call.canceled", "call_id": "cl_abc123" } ``` --- ## ICE Server Caching The SDK caches workspace ICE servers (including TURN credentials) to local storage. On cold start, push-originated calls use the cached servers for better connectivity. ``` First login: FiretellClient → fetch metadata → ICE servers (with TURN) → IceServerCache.save() → SharedPreferences ✅ Cold start push answer: IceServerCache.load() → cached TURN servers ✅ → Call(iceServers: cachedServers) → connect → accept Never logged in (no cache): IceServerCache.load() → default STUN servers (Google, Cloudflare) ``` --- ## Logout — Unregister Tokens ```dart await PushTokenService.logout( baseUrl: client.baseUrl, jwt: client.jwt, deviceId: await DeviceIdHelper.getOrCreate(), ); ``` This removes the device's push tokens server-side so it no longer receives incoming call pushes. --- # API Reference This document provides a detailed description of the classes, methods, events, and types exposed by `@firetell/firetell-client-sdk`. --- ## FiretellClient The `FiretellClient` manages session initialization, REST call dispatch, per-call Native WebSockets, and real-time background EventSource (SSE) stream subscriptions. ### Constructor ```typescript new FiretellClient(jwt: string, domain: string) ``` | Parameter | Type | Default | Description | | :-------- | :------- | :------ | :--------------------------------------------------------------------------- | | `jwt` | `string` | — | The Agent JWT authentication token. | | `domain` | `string` | — | Your full Firetell workspace API domain (e.g. `"yourcompany.firetell.app"`). | --- ### Properties #### `ready` - **Type:** `Promise` - **Description:** Resolves when the workspace metadata is fetched, the SSE event stream is established, and the agent session is ready. #### `connected` - **Type:** `boolean` - **Description:** Returns `true` if a call WebSocket or event stream is active. #### `activeCalls` - **Type:** `Map` - **Description:** A map of active call instances keyed by their `callId`. #### `isWebRTCSupport` - **Type:** `boolean` - **Description:** Returns `true` if the current environment supports WebRTC (specifically `RTCPeerConnection`). #### `sdkVersion` - **Type:** `string` - **Description:** The version string of the current SDK (`"1.0.0"`). --- ### Methods #### `makeCall()` - **Signature:** `makeCall(call: Call, sdp: RTCSessionDescription): Promise` - **Description:** Initiates an outbound call via HTTP REST (`POST /api/v1/call-center/calls`), receives a scoped `call_token`, and opens a native WebSocket for WebRTC signaling. #### `superviseCall()` - **Signature:** `superviseCall(callId: string, mode: "listen" | "whisper" | "barge"): Promise` - **Description:** Initiates call supervision for supervisors via HTTP REST, receives a scoped `call_token`, and connects to the WebRTC audio stream. #### `getSessionInfo()` - **Signature:** `getSessionInfo(): ISession | null` - **Description:** Returns the active session details if ready; otherwise returns `null`. #### `getPhoneNumbers()` - **Signature:** `getPhoneNumbers(options?: { page?: number; limit?: number }): Promise` - **Description:** Returns the list of accessible DID phone numbers assigned to the agent or agent's teams. #### `getConversations()` - **Signature:** `getConversations(query?: IListConversationsQuery): Promise` - **Description:** Returns paginated SMS conversation threads (SMS Inbox) accessible by the agent. Supports filtering by `status`, `assigned_to`, `assigned_team_id`, `unread_only`, and keyword `search`. #### `startConversation()` - **Signature:** `startConversation(payload: IStartConversationPayload): Promise` - **Description:** Initiates a new conversation thread or sends an initial outbound SMS/MMS to a customer. #### `getConversation()` - **Signature:** `getConversation(conversationId: string): Promise` - **Description:** Returns details of a specific conversation thread. #### `updateConversation()` - **Signature:** `updateConversation(conversationId: string, payload: IUpdateConversationPayload): Promise` - **Description:** Updates conversation metadata (assigns/reassigns agent, assigns team, or changes status between `open` and `closed`). #### `markConversationAsRead()` - **Signature:** `markConversationAsRead(conversationId: string): Promise` - **Description:** Marks all inbound messages in a conversation as read and resets `unread_count` to `0`. #### `getConversationMessages()` - **Signature:** `getConversationMessages(conversationId: string, query?: IListMessagesQuery): Promise` - **Description:** Fetches historical messages for a conversation thread in chronological order. #### `sendConversationMessage()` - **Signature:** `sendConversationMessage(conversationId: string, payload: ISendMessagePayload): Promise` - **Description:** Dispatches an outbound SMS/MMS reply to the customer in an existing conversation thread. #### `getDeviceId()` / `FiretellClient.getOrCreateDeviceId()` - **Signature:** `client.getDeviceId(storageKey?: string): string` / `FiretellClient.getOrCreateDeviceId(storageKey?: string): string` - **Description:** Generates or retrieves a unique persistent browser Device ID string stored in `localStorage` (e.g. `"web_550e8400-e29b-41d4-a716-446655440000"`). #### `logout()` - **Signature:** `logout(): void` - **Description:** Ends active calls, closes the SSE event stream, disconnects WebSockets, and clears session state. #### `destroy()` - **Signature:** `destroy(): void` - **Description:** Performs an immediate local cleanup of WebSockets, SSE streams, call objects, and event listeners. --- ### Client Events Subscribe to these events using `client.events.on(eventName, handler)`. | Event (Enum / String) | Payload | Description | | :--------------------------------------------------------- | :------------------------------------------------------ | :--------------------------------------------------------------------------------------------- | | `EClientEventName.SESSION` (`"session"`) | `ISession \| null` | Fired when a session is ready (payload is `ISession`) or destroyed (payload is `null`). | | `EClientEventName.ERROR` (`"error"`) | `{ code: number, message: string }` | Fired when an error occurs in the connection or signaling layer. | | `EClientEventName.CALL_OFFER` (`"call.offer"`) | `Call` | Fired when an incoming call offer is received. | | `EClientEventName.MESSAGE_RECEIVED` (`"message.received"`) | `IMessageReceivedEvent` | Fired via SSE when a new inbound SMS message is received from a customer. | | `EClientEventName.MESSAGE_SENT` (`"message.sent"`) | `IMessageSentEvent` | Fired via SSE when an outbound message is dispatched (allows collision avoidance between agents)| | `EClientEventName.MESSAGE_UPDATED` (`"message.updated"`) | `IMessageUpdatedEvent` | Fired via SSE when carrier delivery status transitions (`sent`, `delivered`, `failed`). | | `EClientEventName.CONVERSATION_UPDATED` (`"conversation.updated"`) | `IConversation` | Fired via SSE when a conversation's status, assignment, or metadata updates. | | `EClientEventName.AGENT_STATE` (`"agent.state"`) | `{ event, workspace_id, data, timestamp }` | Fired via SSE when an agent's presence state changes (`available`, `busy`, `away`, `offline`). | | `EClientEventName.AGENT_STATE_FORCED` (`"agent.state.forced"`) | `{ event, workspace_id, data, timestamp }` | Fired via SSE when an agent's state is force-changed by a supervisor. | | `EClientEventName.AGENT_CREATED` (`"agent.created"`) | `{ event, workspace_id, data, timestamp }` | Fired via SSE when a new agent account is created in the workspace. | | `EClientEventName.AGENT_UPDATED` (`"agent.updated"`) | `{ event, workspace_id, data, timestamp }` | Fired via SSE when an agent account's details or avatar are updated. | | `EClientEventName.AGENT_DELETED` (`"agent.deleted"`) | `{ event, workspace_id, data, timestamp }` | Fired via SSE when an agent account is deleted. | | `EClientEventName.CONTACT_CREATED` (`"contact.created"`) | `{ event, workspace_id, data, timestamp }` | Fired via SSE when a new contact is created in the workspace. | | `EClientEventName.CONTACT_UPDATED` (`"contact.updated"`) | `{ event, workspace_id, data, timestamp }` | Fired via SSE when a contact is updated. | | `EClientEventName.CONTACT_DELETED` (`"contact.deleted"`) | `{ event, workspace_id, data, timestamp }` | Fired via SSE when a contact is deleted. | --- ## Call The `Call` class manages WebRTC media streams (`RTCPeerConnection`), local/remote audio/video tracks, and call control actions (hangup, answer, hold, mute, DTMF). ### Constructor ```typescript new Call(client: FiretellClient, options: CallOptions) ``` | Parameter | Type | Description | | :-------- | :--------------- | :--------------------------------------- | | `client` | `FiretellClient` | Active instance of `FiretellClient`. | | `options` | `CallOptions` | Configuration object for the call setup. | #### `CallOptions` Interface | Field | Type | Required | Description | | :----------- | :-------- | :------- | :------------------------------------------------------------- | | `to` | `string` | ✅ | Target phone number or agent extension. | | `from` | `string` | ❌ | Caller identity / number on incoming call offers. | | `number` | `string` | ❌ | Outbound Caller ID DID phone number. | | `isVideo` | `boolean` | ❌ | Set to `true` to request video media track. Default: `false`. | --- ### Call Methods #### `start()` - **Signature:** `start(): Promise` - **Description:** Starts an outbound call. Requests media permissions, gathers full ICE candidates, and calls `client.makeCall()`. #### `accept()` - **Signature:** `accept(): Promise` - **Description:** Accepts an incoming call offer. Sets up local media, generates SDP answer, and connects the call. #### `reject()` - **Signature:** `reject(): Promise` - **Description:** Rejects an incoming call offer and notifies the server. #### `hangup()` - **Signature:** `hangup(): Promise` - **Description:** Terminates the active call and closes the WebRTC peer connection and WebSocket. #### `sendMute()` - **Signature:** `sendMute(muted: boolean): void` - **Description:** Mutes or unmutes local audio tracks and notifies server. #### `sendDTMF()` - **Signature:** `sendDTMF(digit: string, duration?: number): Promise` - **Description:** Sends DTMF tones (`0-9`, `*`, `#`, `A-D`). --- ### Call Events Subscribe to these events on a `Call` instance using `call.on(eventName, handler)`. | Event (Enum / String) | Payload | Description | | :--------------------------------------------------------- | :------------------------------------------------------ | :--------------------------------------------------------------------------------------------- | | `ECallEventName.STATE` (`"state"`) | `{ state: ECallState, reason?: string, status?: number }` | Fired when the call's signaling state changes (`TRYING`, `RINGING`, `ACTIVE`, `ENDED`, etc.). | | `ECallEventName.MUTE` (`"mute"`) | `{ muted: boolean }` | Fired when the call is muted or unmuted. | | `ECallEventName.REMOTE_STREAM` (`"remote_stream"`) | `{ stream: MediaStream }` | Fired when the remote audio/video stream is successfully connected via WebRTC. | | `ECallEventName.ERROR` (`"error"`) | `Error` | Fired when an error occurs during WebRTC negotiation or media gathering. | #### `ECallState` Enum - `INITIATED`: Call is initializing. - `TRYING`: SIP 100 Trying received from the server. - `RINGING`: SIP 180/183 Ringing received. - `ACTIVE`: Media is connected and active (mapped from `ANSWERED`). - `HELD`: Call is put on hold. - `ENDED`: Call terminated normally. - `CANCEL`: Call was cancelled before answering. - `ERROR`: Call failed. --- # Client Authentication To authorize frontend WebRTC clients safely, your backend server must generate a temporary **Agent JWT Token** representing that agent, which is then passed to the frontend client to initialize the `FiretellClient`. > [!WARNING] > **NEVER hardcode your Master API Key (`sk-...`) in frontend client-side code.** > > Exposing your master API Key gives anyone full access to provision phone numbers, configure billing, and read call records for your entire workspace. --- ## Generating the Agent JWT Token (Backend) To generate the JWT token on your backend server using your Firetell API Key credentials, you must sign the token using the **HS256** algorithm with your API Key's secret (`sk-...`) as the signing key. ### How to Obtain API Key Credentials from Firetell Console To generate Agent JWT tokens, your backend needs both the **API Key SID** and the **API Key Secret**: 1. Log in to the [Firetell Console](https://console.firetell.com). 2. Select your workspace from the dashboard. 3. Navigate to **Workspace Settings** → **API Keys** in the left sidebar. 4. Click **Create API Key** (or select an existing key). 5. Copy both of the following credentials: - **API Key SID** (starts with `sid-`, e.g. `sid-your_api_key_sid`): The public key identifier, used as the `iss` (Issuer) claim in your JWT. - **API Key Secret** (starts with `sk-`, e.g. `sk-your_api_key_secret`): The secret key, used as the signing secret for HS256. :::caution The **API Key Secret** (`sk-...`) is **only shown once** at the time of creation. Store it securely on your backend server (e.g., in environment variables) and never share it or commit it to version control. ::: ### JWT Claims & Structure - **Algorithm:** `HS256` - **Signing Key (Secret):** Your Firetell API Key secret (starts with `sk-`, e.g. `sk-your_api_key_secret`) - **Standard Claims:** - `iss` (Issuer): Your API Key SID (starts with `sid-`, e.g. `sid-your_api_key_sid`) - `sub` (Subject): Depends on the audience type (see below) - `aud` (Audience): `'agent-api'` or `'client-api'` - `exp` (Expiration): The expiration timestamp (e.g. 24 hours from generation) - **Custom Claims:** - `domain`: Your full workspace domain (e.g. `"yourcompany.firetell.app"`) #### Audience Types | Audience | `sub` requirement | Use case | | --- | --- | --- | | `agent-api` | Must be the **username of an existing, active agent** in your workspace | WebRTC agent clients (call center agents, support reps) | | `client-api` | Can be **any identifier** you define (e.g. user ID, session ID) | End-user clients (customers, anonymous callers) | ### Code Examples #### Agent API Token Generate a token for a call center agent. The `sub` must match an existing, active agent username in your workspace. ##### Node.js (using `jsonwebtoken`) ```javascript const jwt = require("jsonwebtoken"); const apiKeySid = "sid-your_api_key_sid"; // From Firetell Console (API Key SID) const apiKeySecret = "sk-your_api_key_secret"; // From Firetell Console (API Key Secret) const workspaceDomain = "yourcompany.firetell.app"; // Your full workspace domain const agentUsername = "agent_123"; // Must be an existing, active agent username const token = jwt.sign( { domain: workspaceDomain, }, apiKeySecret, { algorithm: "HS256", expiresIn: "24h", issuer: apiKeySid, subject: agentUsername, audience: "agent-api", }, ); console.log(token); // Send this token to the frontend client SDK ``` ##### Python (using `PyJWT`) ```python import jwt from datetime import datetime, timedelta api_key_sid = 'sid-your_api_key_sid' api_key_secret = 'sk-your_api_key_secret' workspace_domain = 'yourcompany.firetell.app' agent_username = 'agent_123' # Must be an existing, active agent username payload = { 'domain': workspace_domain, 'iss': api_key_sid, 'sub': agent_username, 'aud': 'agent-api', 'exp': datetime.utcnow() + timedelta(hours=24) } token = jwt.encode(payload, api_key_secret, algorithm='HS256') print(token) # Send this token to the frontend client SDK ``` --- #### Client API Token Generate a token for an end-user client. The `sub` can be any custom identifier you define. ##### Node.js (using `jsonwebtoken`) ```javascript const jwt = require("jsonwebtoken"); const apiKeySid = "sid-your_api_key_sid"; const apiKeySecret = "sk-your_api_key_secret"; const workspaceDomain = "yourcompany.firetell.app"; const userId = "user_abc"; // Any custom identifier you define const token = jwt.sign( { domain: workspaceDomain, }, apiKeySecret, { algorithm: "HS256", expiresIn: "24h", issuer: apiKeySid, subject: userId, audience: "client-api", }, ); console.log(token); // Send this token to the frontend client ``` ##### Python (using `PyJWT`) ```python import jwt from datetime import datetime, timedelta api_key_sid = 'sid-your_api_key_sid' api_key_secret = 'sk-your_api_key_secret' workspace_domain = 'yourcompany.firetell.app' user_id = 'user_abc' # Any custom identifier you define payload = { 'domain': workspace_domain, 'iss': api_key_sid, 'sub': user_id, 'aud': 'client-api', 'exp': datetime.utcnow() + timedelta(hours=24) } token = jwt.encode(payload, api_key_secret, algorithm='HS256') print(token) # Send this token to the frontend client ``` #### PHP (using `firebase/php-jwt`) ```php $workspaceDomain, 'iss' => $apiKeySid, 'sub' => $agentUsername, 'aud' => 'agent-api', 'exp' => time() + (24 * 60 * 60) // e.g. token expires in 24 hours ]; $token = JWT::encode($payload, $apiKeySecret, 'HS256'); echo $token; // Send this token to the frontend client SDK ``` --- # Changelog All notable changes to the [`@firetell/firetell-client-sdk`](https://www.npmjs.com/package/@firetell/firetell-client-sdk) package are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- ## [1.2.4] - 2026-09-09 ### Added - **Camera Mute/Unmute (Toggle Camera)**: - Added `call.muteVideo()`, `call.unmuteVideo()`, and `call.toggleCamera()` methods on `Call`. - Added `call.isCameraOff: boolean` property tracking local camera state. - Added `ECallEventName.CAMERA` (`"camera"`) event emitted locally and on remote camera state updates via signaling. - **Native Screen Sharing (`getDisplayMedia` + `replaceTrack`)**: - Added `call.startScreenShare()`, `call.stopScreenShare()`, and `call.toggleScreenShare()` on `Call`. - Seamlessly replaces video track on the active `RTCRtpSender` without SDP renegotiation or call interruption. - Automatically restores the original camera video track when the user stops sharing screen. - Added `call.isScreenSharing: boolean` property tracking active screen share state. - Added `ECallEventName.SCREEN_SHARE` (`"screenShare"`) event emitted locally and on remote screen share updates. - **SDP Video Auto-Detection in `call.offer` & `accept()`**: - `Call.handleWsMessage`: Automatically falls back to inspecting the remote SDP offer for active video media sections (`/m=video [1-9]/`) if the explicit `is_video` boolean flag is omitted in the `call.offer` payload, ensuring `call.isVideo` is reliably set on the callee side. - `Call.accept()`: Evaluates `this.remoteDescription.sdp` to guarantee `this.isVideo` is true before setting up local media, prompting the browser for both Camera and Microphone permissions (`{ video: true, audio: true }`). - **Resilient Remote Stream Track Assembly**: - Improved `RTCPeerConnection.ontrack` in `_setupWebrtcMedia`: Automatically creates a `MediaStream` and attaches tracks incrementally if `event.streams[0]` is missing, ensuring both remote audio and video tracks are emitted via `ECallEventName.REMOTE_STREAM`. - **Caller Metadata Continuity**: - Added support for `from_avatar` and `transfer_reason` fields in `call.offer` event processing so callee avatar and transfer context remain intact through WebRTC session negotiation. ### Fixed - **Callee Video Negotiation**: - Fixed an issue where incoming video calls defaulted to audio-only on the callee side due to missing `is_video` flag in signaling WebSocket payloads. - **Example Demo Web App**: - Fixed `hideIncomingBanner` reference error on call dismissal. - Added dynamic `updateVideoUI()` helper to automatically bind and display local/remote video elements when tracks arrive. --- ## [1.2.3] - 2026-09-04 ### Changed - **`call.transfer()` simplified** (**Breaking**): - Signature changed from `transfer(targetUsername, teamId?, reason?)` to `transfer(target, reason?)`. - `target` now accepts: agent username, extension number (e.g. `"100"`), team ID (`te_...`), or SIP account ID (`si_...`). - Removed `teamId` parameter — target type is auto-resolved server-side via prefix-based resolution. - **`FiretellClient.sendTransfer()` simplified** (**Breaking**): - Signature changed from `sendTransfer(callId, targetUsername, teamId?, reason?)` to `sendTransfer(callId, target, reason?)`. - HTTP fallback body changed from `{ target_username, team_id, reason }` to `{ target, reason }`. - **Per-call WebSocket payloads cleaned up**: - Removed redundant `call_id` from all WS event payloads (`call.hangup`, `call.answer`, `call.reject`, `call.transfer`, `call.dtmf`, `call.mute`, `call.hold`, `call.unhold`). The per-call WS connection inherently binds the socket to a specific call, making `call_id` unnecessary. - `call.transfer` payload field renamed from `to` to `target` for consistency with the HTTP API. --- ## [1.2.2] - 2026-09-03 ### Added - **`getPhoneNumbers()` REST API on `FiretellClient`**: - Added `public async getPhoneNumbers(options?: { page?: number; limit?: number }): Promise` to query phone numbers (DIDs) accessible by the authenticated agent and their shared teams. - Automatically sends the agent's JWT to `GET /api/v1/call-center/phone-numbers`. - Added `API_ENDPOINTS.PHONE_NUMBERS` (`"/api/v1/call-center/phone-numbers"`). - Added `IClientPhoneNumber` and `IClientPhoneNumbersResponse` interfaces exporting phone number details (`id`, `number`, `title`, `country_code`, `dial_code`, `status`, `capabilities`, `enable_outbound`, `shared_teams_id`). - **Static & Named SDK Version Exports**: - Added static `FiretellClient.VERSION` property on the class for direct, synchronous version access without instantiation. - Exported `SDK_VERSION` constant directly from package root (`import { SDK_VERSION } from '@firetell/firetell-client-sdk'`). - Added `"./package.json": "./package.json"` to the package `exports` map for direct version inspection by package managers and bundlers. --- ## [1.2.1] - 2026-08-22 ### Added - **`transfer_reason` in `call.ring` Event & `Call` Instance**: - Added `transfer_reason?: string` to `ICallRingParams` payload in the `call.ring` SSE event. - Exposed `call.transferReason?: string` on the `Call` instance when a transferred call session is initialized. - Allows frontends, Webphones, and Console agents to display the contextual transfer reason provided by Voice AI agents or human agents during ringing. --- ## [1.2.0] - 2026-08-20 ### Added - **Dedicated Per-Call SSE Event Stream (`Call.connectCallEventStream`)**: - Automatically establishes a dedicated SSE connection (`/stream?call_id=`) for each active call session (`call.start()`, `call.accept()`, `call.joinSession()`, and `client.createCallSession()`). - Subscribes to the Per-Call channel for real-time speech-to-text transcriptions and recording events without flooding the global workspace event stream. - Automatically disconnects and cleans up the stream upon call termination (`call.destroy()`). - **Call-Scoped Real-Time Transcription Events & APIs**: - Added `ECallEventName.TRANSCRIPTION`, `TRANSCRIPTION_STARTED`, `TRANSCRIPTION_DIALOGUE`, and `TRANSCRIPTION_COMPLETED`. - Added ergonomic helper listener methods on `Call`: `call.onDialogue((dialogue) => ...)` and `call.onTranscription((event) => ...)`. - Exported TypeScript interfaces: `ITranscriptionStartedEvent`, `ITranscriptionDialogueEvent`, `ITranscriptionCompletedEvent`, and `TranscriptionEvent`. - **Call-Scoped Real-Time Recording Events & APIs**: - Added `ECallEventName.RECORDING`, `RECORDING_STARTED`, `RECORDING_COMPLETED`, and `RECORDING_READY`. - Added ergonomic helper listener method on `Call`: `call.onRecording((event) => ...)`. - Exported TypeScript interfaces: `ICallRecordingStartedEvent`, `ICallRecordingCompletedEvent`, `ICallRecordingReadyEvent`, and `CallRecordingEvent`. - **Public Getters on `FiretellClient`**: - Added `client.getBaseUrl()` and `client.getJwt()` getters. ### Changed & Improved - **Strict Single-Emission & Call-Scoped Event Architecture**: - Removed duplicate event emissions across `FiretellClient._initEventStream()` and `Call._handleSseMessage()`. - Decoupled per-call transcription events from `client.events`, strictly routing all live subtitles, chunks, and summaries directly to the owning `Call` instance (`call.on(...)`). --- ## [1.1.7] - 2026-08-17 ### Added - **`FiretellClient.stopSupervision()`**: Added helper method to terminate an ongoing call supervision session (cleans up WebRTC call session and calls REST API `DELETE /v1/call-center/calls/:id/supervision`). - **Call State Monotonicity Guard**: Added automatic state ranking guard in `FiretellClient` to prevent late/out-of-order `call.created` events from overriding or regressing active call state machine. --- ## [1.1.6] - 2026-08-17 ### Added - **`SseStreamClient` (Header-Based SSE Streaming)**: Integrated lightweight, zero-dependency SSE stream client with `Authorization: Bearer ` HTTP header support via `fetch` and `ReadableStream`, completely eliminating JWT token exposure in SSE stream URLs and server access logs. - **SSE Stream Exports**: Exported `SseStreamClient`, `SseMessageEvent`, and `SseStreamConfig` from SDK entry point. --- ## [1.1.5] - 2026-08-17 ### Added - **`FiretellClient.startSupervision()`**: Added high-level helper method to initiate call supervision (`listen`, `whisper`, `barge`) and automatically establish WebRTC audio session in one call. - **`Call.joinSession()`**: Added method to connect signaling and send WebRTC offer using pre-generated `call_token` and `ws_url`. - **`ISupervisionResponse.ws_url`**: Added required `ws_url` field to `ISupervisionResponse` interface to support dedicated per-call WebSocket connections. --- ## [1.1.4] - 2026-08-15 ### Fixed - **Call Creation**: Renamed `_createCallSession` to `createCallSession`. --- ## [1.1.3] - 2026-08-14 ### Added - **Real-Time Call Lifecycle Event Enums**: Added `EClientEventName.CALL_CREATED`, `EClientEventName.CALL_STARTED`, and `EClientEventName.CALL_ANSWERED`. - **Active Call State Synchronization**: Added automatic transition of active calls to `ECallState.ACTIVE` / `ANSWERED` when receiving `call.answered` over the SSE stream. --- ## [1.1.2] - 2026-08-14 ### Added - **Client Event Enum Values (`CALL_ENDED` & `CALL_CANCELED`)**: Added `EClientEventName.CALL_ENDED` and `EClientEventName.CALL_CANCELED`. - **SSE Stream Listeners for Early Call Cancellation**: Automatically cleans up `Call` instances and dismisses incoming call ringing UI modals when calls are answered by another agent or canceled before WebSocket connection completion. --- ## [1.1.1] - 2026-08-12 ### Fixed - **Inbound Call DTLS Setup Role Validation**: Fixed `Answerer must use either active or passive value for setup attribute` error during callee-initiated unhold (`Call.unhold()`). - **Optimized Full ICE Gathering**: Reduced hard safety timeout to 6s, and implemented a 3s fallback timer verifying presence of STUN Public IP (`typ srflx`) or Relay (`typ relay`) candidates. --- ## [1.1.0] - 2026-08-12 ### Fixed - **DTLS SSL Role Preservation on Renegotiation**: Preserves established DTLS `a=setup` role (`active` / `passive`) in `setRemoteDescription()` across renegotiation answers from Media Server. - **Incoming Call Accept Error Safety**: Added `try...catch` block around `Call.accept()` to guarantee proper media cleanup and `ECallState.ERROR` state emission if microphone access or WebRTC answer creation fails. --- ## [1.0.9] - 2026-08-12 ### Fixed - **Call Unhold Event Signaling**: Fixed WS event name emitted during `Call.unhold()` from `"call.hold"` to `"call.unhold"`. - **WebRTC Remote SDP Parsing**: Added `extractSdpInit()` helper in `Call.ts` to safely parse SDP payloads received in WebSocket messages. - **ICE Server Fallback**: Configured `DEFAULT_ICE_SERVERS` (Google STUN + Cloudflare STUN) as default fallback. --- ## [1.0.0] - 2026-01-01 ### Added - Initial release of `@firetell/firetell-client-sdk`. - WebRTC audio/video calls via native per-call WebSocket signaling. - JWT-based session authentication (`session.connect`). - Outbound call support (`Call.start()`) and incoming call handling (`Call.accept()`, `Call.reject()`). - Call controls: hold/unhold, mute/unmute, DTMF, transfer. - Auto-reconnect with exponential backoff. - Agent presence notifications (`workspace.agent.state`). - Full ICE gathering (non-Trickle) for Media Server compatibility. - Multi-format builds: CJS, ESM, IIFE with TypeScript declarations. --- # Installation & Setup The `@firetell/firetell-client-sdk` library allows you to build WebRTC-based voice and video calling features directly in web browsers and Node.js environments. 🎮 **Live Interactive Demo**: [https://developers.firetell.com/firetell-client-sdk/example/](https://developers.firetell.com/firetell-client-sdk/example/) This SDK is a multi-format package supporting: - **ESM** (ECMAScript Modules) for modern browsers and bundlers (Vite, Webpack, etc.) - **CJS** (CommonJS) for Node.js backend integration - **IIFE** (Immediately Invoked Function Expression) for direct inclusion via script tags (CDN) --- ## 1. Install via npm If you are using a package manager (such as `npm`, `yarn`, or `pnpm`) in a bundled browser or Node.js environment, install the package: ```bash npm install @firetell/firetell-client-sdk ``` --- ## 2. Importing the SDK ### In Browser (ESM) If you are using modern JavaScript/TypeScript bundlers (e.g. Vite, React, Angular, Vue, Next.js): ```typescript import { FiretellClient, Call, ECallState, } from "@firetell/firetell-client-sdk"; // Initialize the client with the full domain name const client = new FiretellClient("YOUR_AGENT_JWT", "yourcompany.firetell.app"); ``` ### In Node.js (CommonJS) If you are running in a Node.js environment that uses CommonJS syntax: ```javascript const { FiretellClient } = require("@firetell/firetell-client-sdk"); // Initialize the client with the full domain name const client = new FiretellClient("YOUR_AGENT_JWT", "yourcompany.firetell.app"); ``` > [!NOTE] > **Node.js Environment Limitation:** > Node.js does not support WebRTC features (making or receiving calls) due to the lack of native browser APIs like `RTCPeerConnection` or `getUserMedia`. In Node.js environments, the SDK is solely used for connecting to the gateway to receive real-time events via WebSocket connection. --- ## 3. Direct Script Tag (IIFE via CDN) For simple HTML pages or legacy architectures where you do not use a build step or bundler, you can load the SDK directly from a CDN (such as jsDelivr or unpkg). Add this script tag to the `` or `` of your HTML document: ```html ``` --- ## ⚠️ Security Warning: Frontend Auth > [!WARNING] > **NEVER hardcode your Master API Key (`sk-...`) in frontend client-side code.** > > Exposing your master API Key gives anyone full access to provision phone numbers, configure billing, and read call records for your entire workspace. To authenticate frontend clients safely: 1. Keep your master API Key securely on your **backend server**. 2. When an agent logs into your application, your backend should generate or retrieve a temporary **Agent JWT Token** representing that agent. 3. Pass the **Agent JWT Token** to your frontend browser client. 4. Use that JWT token to initialize the `FiretellClient` WebRTC SDK. For a detailed walkthrough and code examples (Node.js, Python, PHP, ...) on how to generate the Agent JWT Token, refer to the [Client Authentication Guide](./authentication.md). --- ## System Requirements To use the WebRTC calling features: - **HTTPS or Localhost:** Web browsers require a secure origin (`https://` or `localhost`) to access microphone and camera devices (`navigator.mediaDevices.getUserMedia`). - **Browser Support:** Modern web browsers with WebRTC support (Google Chrome, Mozilla Firefox, Apple Safari, Microsoft Edge). - **Network Access:** Ensure WebSocket (`wss://`) traffic is allowed on your network to the Firetell signaling domains. - **Node.js Environment Limitation:** Node.js environments do not support native WebRTC APIs, meaning making or receiving audio/video calls is not supported. Use Node.js environments strictly for subscribing to real-time events. --- # Quickstart Guide This guide walks you through the core workflows of the Firetell Client SDK: connecting, listening for SSE events, initiating outbound calls via REST, accepting incoming calls, and controlling active call states. 🎮 **Try the Live Interactive Demo**: [https://developers.firetell.com/firetell-client-sdk/example/](https://developers.firetell.com/firetell-client-sdk/example/) --- ## Step 1: Initialize the Client To connect to the Firetell platform, create a new instance of `FiretellClient`. The constructor takes your agent's JWT token and the workspace domain. Wait for the `ready` promise to resolve before performing actions. ```typescript import { FiretellClient } from "@firetell/firetell-client-sdk"; const token = "your-agent-jwt-token"; const domain = "yourcompany.firetell.app"; // Workspace API domain const client = new FiretellClient(token, domain); try { // Wait for session initialization and SSE event stream setup const session = await client.ready; console.log("Successfully authenticated as:", session.username); } catch (error) { console.error("Initialization failed:", error); } ``` --- ## Step 2: Listen to Realtime Events (SSE) Register event listeners on the client's `events` emitter to respond to incoming calls or teammate status updates via Server-Sent Events (SSE). ```typescript import { EClientEventName, ICallRingParams, Call } from "@firetell/firetell-client-sdk"; // Handle signaling and socket errors client.events.on(EClientEventName.ERROR, (error) => { console.error(`Signaling error [Code ${error.code}]:`, error.message); }); // Track agent presence updates in the workspace via SSE client.events.on(EClientEventName.AGENT_STATE, (payload) => { const data = payload.data || payload; console.log(`Agent ${data.username || data.id} is now ${data.state}`); }); // Track agent lifecycle creation events via SSE client.events.on(EClientEventName.AGENT_CREATED, (payload) => { const data = payload.data || payload; console.log(`New agent account created: ${data.display_name || data.username} (${data.id})`); }); // 1. Instant Ring Alert via SSE (CALL_RING — Triggers Incoming Call Ringing UI) client.events.on(EClientEventName.CALL_RING, (ringData: ICallRingParams) => { console.log("Incoming call ring alert for call_id:", ringData.call_id); console.log("Caller (from):", ringData.from?.name, ringData.from?.number); // "Nguyen Van A", "+84901234567" console.log("Target (to):", ringData.to?.name, ringData.to?.number); // "Support Team", "+842471000000" // -> Display Ringing Screen & Ringtone Popup immediately! }); // 2. WebRTC Call Offer Ready (CALL_OFFER — WebRTC Call object ready for answer) client.events.on(EClientEventName.CALL_OFFER, (call: Call) => { console.log("Incoming WebRTC Call ready to answer:", call.callId); console.log("Caller:", call.from_name || call.from); }); ``` --- ## Step 3: Make an Outbound Call To start a new call, instantiate a `Call` object, subscribe to state/stream events, and invoke `start()`. The SDK automatically calls `POST /api/v1/call-center/calls` to obtain a `call_token`, then opens a per-call native WebSocket for WebRTC signaling. ```typescript import { Call, ECallState } from "@firetell/firetell-client-sdk"; // Create a call configuration const call = new Call(client, { to: "+84901234567", // Destination number or internal extension number: "84281234567", // Outbound Caller ID DID phone number isVideo: false, }); // 1. Monitor call lifecycle states call.on("state", ({ call_id, state, reason }) => { console.log(`Call state updated: ${state}`); // INITIATED → ANSWERED → ENDED if (["ENDED", "CANCEL", "ERROR"].includes(state)) { console.log(`Call terminated. Reason: ${reason || "Normal Hangup"}`); } }); // 2. Attach remote audio stream to HTML element call.on("remoteStream", (stream) => { if (stream) { const remoteAudio = document.getElementById("remoteAudio") as HTMLAudioElement; remoteAudio.srcObject = stream; remoteAudio.play(); } }); // 3. Initiate call await call.start(); ``` --- ## Step 4: Call Supervision (Supervisor Only) Supervisors can monitor, coach, or join active calls using `superviseCall()`: ```typescript // Modes: "listen" (silent), "whisper" (coach), "barge" (3-way) const supervision = await client.superviseCall("cl_123456789", "listen"); console.log("Supervision started:", supervision.status); ``` --- ## Step 5: Handling Concurrent Incoming Calls When an agent receives a 2nd call while already on an active call, choose one of two integration patterns: ### Option A: Call Waiting & Hold ```typescript let activeCall: Call | null = null; client.events.on("call.offer", async (incomingCall: Call) => { if (activeCall && activeCall.active) { // Show Call Waiting UI notification for incomingCall console.log(`Call Waiting from ${incomingCall.from} (${incomingCall.from_name})`); // When Agent clicks "Hold & Answer": await activeCall.hold(); await incomingCall.accept(); activeCall = incomingCall; } else { activeCall = incomingCall; } }); ``` ### Option B: Auto-Reject (Busy Policy) ```typescript client.events.on("call.offer", async (incomingCall: Call) => { if (client.activeCalls.size > 1) { // Reject 2nd call with Busy status (SIP 486) to trigger backend fallback branch await incomingCall.reject(); } }); ``` --- # SDKs Overview Firetell provides official client libraries and SDKs to simplify integration, handle WebRTC calling, and manage WebSocket communication within your applications. Using our SDKs ensures that best practices—such as automatic reconnects, authentication timeouts, and media track management—are handled for you out of the box. --- ## Available SDKs ### JavaScript / TypeScript SDK The `@firetell/firetell-client-sdk` library is compatible with browser environments (using ES Modules or loaded via a CDN script tag) for building WebRTC-based calling applications, and Node.js environments (using CommonJS) for receiving real-time events via WebSocket connection. It enables you to integrate real-time voice, video, and event features directly into your applications. - **Package:** [`@firetell/firetell-client-sdk`](https://www.npmjs.com/package/@firetell/firetell-client-sdk) - **Status:** Active / Stable - **Interactive Live Demo:** [https://developers.firetell.com/firetell-client-sdk/example/](https://developers.firetell.com/firetell-client-sdk/example/) - **Guides:** [Installation & Setup](/docs/sdks/javascript/installation) • [Client Authentication](/docs/sdks/javascript/authentication) • [Quickstart Guide](/docs/sdks/javascript/quickstart) • [API Reference](/docs/sdks/javascript/api-reference) --- ### Flutter SDK The `firetell_flutter_sdk` package enables native iOS and Android apps to make and receive VoIP calls using WebRTC, with full support for push notifications (FCM & APNs), CallKit (iOS), and ConnectionService (Android). - **Package:** [`firetell_flutter_sdk`](https://pub.dev/packages/firetell_flutter_sdk) - **Status:** Active / Stable - **Guides:** [Installation & Setup](/docs/sdks/flutter/installation) • [Quickstart Guide](/docs/sdks/flutter/quickstart) • [VoIP Push Notifications](/docs/sdks/flutter/voip-push) • [API Reference](/docs/sdks/flutter/api-reference) --- ## Handling Concurrent Incoming Calls When multiple incoming calls arrive simultaneously or an incoming call arrives while an agent is currently on an active call, Firetell supports two standard frontend integration patterns: ### Pattern A: Call Waiting & Hold (Multi-Call Management) Allow the agent to receive call waiting notifications, put the current call on hold, and answer the new incoming call: import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```typescript let activeCall: Call | null = null; client.events.on("call.offer", async (incomingCall: Call) => { if (activeCall && activeCall.active) { // 1. Show Call Waiting notification on UI console.log( `Call Waiting from ${incomingCall.from} (${incomingCall.from_name})`, ); // 2. When Agent clicks "Hold & Answer": await activeCall.hold(); await incomingCall.accept(); activeCall = incomingCall; } else { // Single call flow activeCall = incomingCall; } }); ``` ```dart Call? activeCall; client.onCallOffer.listen((incomingCall) async { if (activeCall != null && activeCall!.active) { // 1. Show Call Waiting notification on UI print('Call Waiting from ${incomingCall.from} (${incomingCall.fromName})'); // 2. When Agent taps "Hold & Answer": await activeCall!.onhold(); await incomingCall.accept(); activeCall = incomingCall; } else { activeCall = incomingCall; } }); ``` ### Pattern B: Auto-Reject / Busy (Single-Call Policy) If your call center policy restricts agents to one call at a time: ```typescript client.events.on("call.offer", async (incomingCall: Call) => { if (client.activeCalls.size > 1) { // Auto-reject 2nd call with Busy status (SIP 486 Busy) // The backend Call Flow will immediately execute the fallback branch (Voicemail/Reroute) await incomingCall.reject(); } }); ``` ```dart client.onCallOffer.listen((incomingCall) async { if (client.activeCalls.length > 1) { // Auto-reject 2nd call with Busy status (SIP 486 Busy) // The backend Call Flow will immediately execute the fallback branch await incomingCall.reject(); } }); ``` --- ## Planned SDKs We are actively developing SDKs for additional languages and mobile frameworks to help you build native apps: | Platform / Language | Status | Package Name | Target Environment | | :------------------ | :--------- | :-------------------------- | :----------------- | | **React Native** | 🔜 Planned | `firetell-react-native-sdk` | iOS & Android | If you need an SDK for another language, please reach out to us at [developers@firetell.com](mailto:developers@firetell.com). --- # Webhook Event Catalog Detailed payloads and schemas for all webhook events supported by Firetell. --- ## Call Events ### `call.created` Triggered immediately when a new call (inbound or outbound) is initiated. ```json { "event": "call.created", "data": { "workspace_id": "ws_company", "occurred_at": "2026-07-21T12:00:00.000Z", "attempt": 1, "call_id": "call_abc123", "direction": "inbound", "status": "started", "type": "audio", "from": { "name": "+84901234567", "number": "+84901234567" }, "to": { "name": "Support Line", "number": "+84909876543" }, "started_at": "2026-07-21T12:00:00.000Z", "answered_at": null, "ended_at": null, "duration": 0, "billsec": 0, "hangup_cause": null } } ``` --- ### `call.answered` Triggered whenever a call transitions to answered state. ```json { "event": "call.answered", "data": { "workspace_id": "ws_company", "occurred_at": "2026-07-21T12:00:05.000Z", "attempt": 1, "call_id": "call_abc123", "direction": "inbound", "status": "active", "type": "audio", "from": { "name": "+84901234567", "number": "+84901234567" }, "to": { "name": "Support Line", "number": "+84909876543" }, "started_at": "2026-07-21T12:00:00.000Z", "answered_at": "2026-07-21T12:00:05.000Z", "ended_at": null, "duration": 5, "billsec": 0, "hangup_cause": null } } ``` --- ### `call.ended` Triggered when a call hangs up or fails. ```json { "event": "call.ended", "data": { "workspace_id": "ws_company", "occurred_at": "2026-07-21T12:02:30.000Z", "attempt": 1, "call_id": "call_abc123", "direction": "inbound", "status": "completed", "type": "audio", "from": { "name": "+84901234567", "number": "+84901234567" }, "to": { "name": "Support Line", "number": "+84909876543" }, "started_at": "2026-07-21T12:00:00.000Z", "answered_at": "2026-07-21T12:00:05.000Z", "ended_at": "2026-07-21T12:02:30.000Z", "duration": 150, "billsec": 145, "hangup_cause": "NORMAL_CLEARING" } } ``` --- ### `call.recording.ready` Triggered when audio recording processing completes and the cloud media file URL is ready for playback and download. ```json { "event": "call.recording.ready", "data": { "workspace_id": "ws_company", "occurred_at": "2026-07-21T12:02:35.000Z", "attempt": 1, "recording_id": "rec_call_abc123", "call_id": "call_abc123", "url": "https://storage.firetell.app/workspaces/ws_company/recordings/call_abc123.mp3", "duration": 145, "size_bytes": 1160000, "format": "mp3", "created_at": "2026-07-21T12:02:35.000Z" } } ``` --- ### `call.transcription.completed` Triggered when real-time AI transcription completes and full dialogue text with extracted entities (NER) is compiled. ```json { "event": "call.transcription.completed", "data": { "workspace_id": "ws_company", "occurred_at": "2026-07-21T12:02:36.000Z", "attempt": 1, "call_id": "call_abc123", "dialogues": [ { "id": "diag_01h900000000000000000001", "speaker": "caller", "channel_index": 0, "text": "Hello, I would like to check my order status for #FT10294.", "confidence": 0.98, "duration_ms": 3200, "timestamp": "2026-07-21T12:00:10.000Z" }, { "id": "diag_01h900000000000000000002", "speaker": "agent", "channel_index": 1, "text": "Certainly! I have retrieved order #FT10294 for Alex Rivera. It is scheduled for delivery today with Express Courier.", "confidence": 0.99, "duration_ms": 5400, "timestamp": "2026-07-21T12:00:20.000Z" } ], "entities": [ { "type": "order_id", "value": "#FT10294", "confidence": 0.99 }, { "type": "customer_name", "value": "Alex Rivera", "confidence": 0.98 }, { "type": "carrier", "value": "Express Courier", "confidence": 0.97 } ], "total_dialogues": 2, "completed_at": "2026-07-21T12:02:36.000Z" } } ``` --- ### `call.transcription.summary` Triggered when AI post-call processing completes, providing executive call summary, customer sentiment analysis, and actionable next steps. ```json { "event": "call.transcription.summary", "workspace_id": "ws_company", "timestamp": "2026-07-21T12:02:40.000Z", "data": { "transcription_id": "trans_01h900000000000000000001", "call_id": "call_abc123", "workspace_id": "ws_company", "summary": "Customer inquired about enterprise cloud contact center solutions and pricing.", "sentiment": "positive", "action_items": [ "Send Enterprise pricing sheet to customer", "Schedule live product demo" ], "language": "en" } } ``` --- ## Contact Events ### `contact.created` Triggered whenever a new contact is created in the workspace. ```json { "event": "contact.created", "data": { "workspace_id": "ws_company", "occurred_at": "2026-07-21T12:10:00.000Z", "attempt": 1, "id": "ct_987654321", "address_book_id": "ab_sales_vip", "first_name": "Nguyen", "last_name": "Van A", "company": "Acme Corp", "phone_numbers": [ { "phone_number": "+84901234567", "normalized_number": "84901234567", "type": "mobile", "is_primary": true } ], "emails": [], "assigned_agent_id": "ag_john", "created_at": "2026-07-21T12:10:00.000Z" } } ``` --- ### `contact.updated` Triggered whenever contact details or address book assignment are modified. ```json { "event": "contact.updated", "data": { "workspace_id": "ws_company", "occurred_at": "2026-07-21T12:15:00.000Z", "attempt": 1, "id": "ct_987654321", "address_book_id": "ab_sales_vip", "first_name": "Nguyen", "last_name": "Van A", "company": "Acme Global", "title": "CTO", "updated_at": "2026-07-21T12:15:00.000Z" } } ``` --- ### `contact.deleted` Triggered when a contact is permanently removed. ```json { "event": "contact.deleted", "data": { "workspace_id": "ws_company", "occurred_at": "2026-07-21T12:20:00.000Z", "attempt": 1, "id": "ct_987654321", "address_book_id": "ab_sales_vip" } } ``` --- ## Agent Events ### `agent.state` Triggered whenever an agent's presence state (available, busy, away, offline) changes. ```json { "event": "agent.state", "workspace_id": "ws_company", "data": { "id": "650000000000000000000001", "username": "agent_john", "state": "busy", "reason": "On call", "changed_by": "supervisor_mary" }, "timestamp": "2026-08-03T15:20:00.000Z", "attempt": 1 } ``` ### `agent.created` / `agent.updated` / `agent.deleted` Triggered whenever a workspace agent account is created, updated, or deleted. ```json { "event": "agent.created", "workspace_id": "ws_company", "data": { "id": "650000000000000000000001", "username": "agent_john", "display_name": "John Doe", "email": "john@company.com", "role": "agent", "avatar": "https://cdn.firetell.com/avatars/agent_john.png", "country_code": "US", "is_active": true }, "timestamp": "2026-08-03T15:20:00.000Z", "attempt": 1 } ``` --- ## Workspace & Billing Events ### `workspace.credit.updated` Triggered whenever the workspace credit balance changes (e.g., when an outbound call charge is deducted or when credits are added). ```json { "event": "workspace.credit.updated", "workspace_id": "ws_company", "timestamp": "2026-08-14T11:36:00.000Z", "attempt": 1, "data": { "workspace_id": "ws_company", "credit_balance": 124.85, "currency": "USD", "amount": -0.055, "type": "charge", "description": "Outbound call to destination_number: +12025550143 (duration: 60s / 1 mins, from_number: 13133670411)", "call_id": "call_1786704938", "timestamp": "2026-08-14T11:36:00.000Z" } } ``` --- ## Message (SMS/MMS) Events ### `message.received` Triggered when an incoming SMS or MMS message is received on one of your workspace phone numbers. ```json { "event": "message.received", "workspace_id": "yourcompany", "timestamp": "2026-09-26T00:00:00.000Z", "attempt": 1, "data": { "id": "msg_be52701819fa34a72ae414", "phone_number_id": "pn_99bf875464f1c096f46aff", "direction": "inbound", "from": "+13074295456", "to": "+18647123123", "body": "Hello, I need assistance with my reservation.", "status": "received", "media_urls": [], "created_at": "2026-09-26T00:00:00.000Z" } } ``` --- ### `message.sent` Triggered when an outbound SMS or MMS message has been accepted by the telecommunications carrier and dispatched for delivery. ```json { "event": "message.sent", "workspace_id": "yourcompany", "timestamp": "2026-09-26T00:00:02.000Z", "attempt": 1, "data": { "id": "msg_05f18045cba1452204774f", "phone_number_id": "pn_e71c8caddbe5603f1d0522", "direction": "outbound", "from": "+13074295456", "to": "+18647123123", "body": "Your appointment is confirmed for tomorrow at 10:00 AM.", "status": "sent", "error_code": null, "error_message": null, "created_at": "2026-09-26T00:00:00.000Z", "updated_at": "2026-09-26T00:00:02.000Z" } } ``` --- ### `message.delivered` Triggered when the destination carrier confirms successful delivery of the message to the recipient's handset (Delivery Receipt - DLR). ```json { "event": "message.delivered", "workspace_id": "yourcompany", "timestamp": "2026-09-26T00:00:05.000Z", "attempt": 1, "data": { "id": "msg_05f18045cba1452204774f", "phone_number_id": "pn_e71c8caddbe5603f1d0522", "direction": "outbound", "from": "+13074295456", "to": "+18647123123", "body": "Your appointment is confirmed for tomorrow at 10:00 AM.", "status": "delivered", "error_code": null, "error_message": null, "created_at": "2026-09-26T00:00:00.000Z", "updated_at": "2026-09-26T00:00:05.000Z" } } ``` --- ### `message.failed` Triggered when an outbound SMS/MMS message fails to send or cannot be delivered by the carrier (e.g. invalid destination number, carrier unreachable, or message blocked). ```json { "event": "message.failed", "workspace_id": "yourcompany", "timestamp": "2026-09-26T00:00:04.000Z", "attempt": 1, "data": { "id": "msg_05f18045cba1452204774f", "phone_number_id": "pn_e71c8caddbe5603f1d0522", "direction": "outbound", "from": "+13074295456", "to": "+18647123123", "body": "Your appointment is confirmed for tomorrow at 10:00 AM.", "status": "failed", "error_code": "40001", "error_message": "Destination unreachable: Handset turned off or out of coverage", "created_at": "2026-09-26T00:00:00.000Z", "updated_at": "2026-09-26T00:00:04.000Z" } } ``` --- ### `message.undelivered` Triggered when a delivery outcome could not be definitively confirmed by the carrier within the receipt timeout window. ```json { "event": "message.undelivered", "workspace_id": "yourcompany", "timestamp": "2026-09-26T00:05:00.000Z", "attempt": 1, "data": { "id": "msg_05f18045cba1452204774f", "phone_number_id": "pn_e71c8caddbe5603f1d0522", "direction": "outbound", "from": "+13074295456", "to": "+18647123123", "body": "Your appointment is confirmed for tomorrow at 10:00 AM.", "status": "undelivered", "error_code": null, "error_message": null, "created_at": "2026-09-26T00:00:00.000Z", "updated_at": "2026-09-26T00:05:00.000Z" } } ``` --- # Webhooks Overview Webhooks allow your application to receive real-time HTTP notifications when events occur in your Firetell workspace. ## How Webhooks Work 1. You register a webhook URL via the [Webhooks API](/docs/rest-api/workspace-api/webhooks) or the Firetell Console 2. When an event occurs, Firetell sends an HTTP `POST` request to your URL 3. Your server processes the event and responds with a `200` status code ## Webhook Payload Format All webhook payloads follow a standard envelope structure: ```json { "event": "call.ended", "data": { "workspace_id": "yourcompany", "occurred_at": "2026-07-24T00:56:02.000Z", "attempt": 1, "call_id": "call_abc789", "direction": "inbound", "status": "completed", "type": "audio", "from": { "name": "+84901234567", "number": "+84901234567" }, "to": { "name": "+84909876543", "number": "+84909876543" }, "started_at": "2026-07-24T00:55:00.000Z", "answered_at": "2026-07-24T00:55:02.000Z", "ended_at": "2026-07-24T00:56:02.000Z", "duration": 62, "billsec": 60, "cost": 0.0517, "hangup_cause": "NORMAL_CLEARING" } } ``` ### Event Payload Examples #### `call.created` ```json { "event": "call.created", "data": { "workspace_id": "yourcompany", "occurred_at": "2026-07-24T00:55:00.000Z", "attempt": 1, "call_id": "call_abc789", "direction": "inbound", "status": "started", "type": "audio", "from": { "name": "+84901234567", "number": "+84901234567" }, "to": { "name": "+84909876543", "number": "+84909876543" }, "started_at": "2026-07-24T00:55:00.000Z", "answered_at": null, "ended_at": null, "duration": 0, "billsec": 0, "cost": 0, "hangup_cause": null } } ``` #### `call.recording.ready` ```json { "event": "call.recording.ready", "data": { "workspace_id": "yourcompany", "occurred_at": "2026-07-24T00:56:05.000Z", "attempt": 1, "recording_id": "rec_call_abc789", "call_id": "call_abc789", "url": "https://storage.firetell.app/workspaces/yourcompany/recordings/call_abc789.mp3", "duration": 60, "size_bytes": 1440000, "format": "mp3", "created_at": "2026-07-24T00:56:05.000Z" } } ``` #### `call.transcription.completed` ```json { "event": "call.transcription.completed", "data": { "workspace_id": "yourcompany", "occurred_at": "2026-07-24T00:56:06.000Z", "attempt": 1, "call_id": "call_abc789", "dialogues": [ { "id": "diag_01h900000000000000000001", "speaker": "caller", "channel_index": 0, "text": "Hello, I would like to check my order status for #FT10294.", "confidence": 0.98, "duration_ms": 3200, "timestamp": "2026-07-24T00:55:10.000Z" }, { "id": "diag_01h900000000000000000002", "speaker": "agent", "channel_index": 1, "text": "Certainly! I have retrieved order #FT10294 for Alex Rivera. It is scheduled for delivery today with Express Courier.", "confidence": 0.99, "duration_ms": 5400, "timestamp": "2026-07-24T00:55:20.000Z" } ], "entities": [ { "type": "order_id", "value": "#FT10294", "confidence": 0.99 }, { "type": "customer_name", "value": "Alex Rivera", "confidence": 0.98 }, { "type": "carrier", "value": "Express Courier", "confidence": 0.97 } ], "total_dialogues": 2, "completed_at": "2026-07-24T00:56:06.000Z" } } ``` #### `call.transcription.summary` ```json { "event": "call.transcription.summary", "workspace_id": "yourcompany", "timestamp": "2026-07-24T00:56:12.000Z", "data": { "transcription_id": "trans_01h900000000000000000001", "call_id": "call_abc789", "workspace_id": "yourcompany", "summary": "Customer inquired about enterprise cloud contact center solutions and pricing.", "sentiment": "positive", "action_items": [ "Send Enterprise pricing sheet to customer", "Schedule live product demo" ], "language": "en" } } ``` #### `message.delivered` ```json { "event": "message.delivered", "workspace_id": "yourcompany", "timestamp": "2026-09-26T00:00:05.000Z", "attempt": 1, "data": { "id": "msg_05f18045cba1452204774f", "phone_number_id": "pn_e71c8caddbe5603f1d0522", "direction": "outbound", "from": "+13074295456", "to": "+18647123123", "body": "Hello from Firetell", "status": "delivered", "error_code": null, "error_message": null, "created_at": "2026-09-26T00:00:00.000Z", "updated_at": "2026-09-26T00:00:05.000Z" } } ``` ## Event Types | Event | Description | |-------|------------| | `call.created` | A new call has been initiated | | `call.answered` | A call was answered by an agent or user | | `call.ended` | A call has ended | | `call.recording.ready` | A call recording audio file has finished processing, uploaded, and is ready for playback/download | | `call.transcription.completed` | Real-time call transcription and Named Entity Recognition (NER) completed | | `call.transcription.summary` | AI post-call executive summary, customer sentiment analysis, and action items | | `contact.created` | A new contact was created in the workspace | | `contact.updated` | An existing contact was updated | | `contact.deleted` | A contact was deleted | | `agent.state` | An agent's presence state was changed (available, busy, away, offline) | | `agent.created` | A new workspace agent account was created | | `agent.updated` | An existing workspace agent account was updated | | `agent.deleted` | A workspace agent account was deleted | | `workspace.credit.updated` | Workspace credit balance was updated (e.g. call charge deduction, top-up) | | `message.received` | An inbound SMS/MMS message was received | | `message.sent` | An outbound SMS/MMS message was accepted by the carrier and sent | | `message.delivered` | An outbound SMS/MMS message was confirmed delivered to recipient | | `message.failed` | An outbound SMS/MMS message failed to send or deliver | | `message.undelivered` | An outbound SMS/MMS message could not be confirmed delivered | ## Retry Policy If your endpoint returns a non-`2xx` status code or times out (30 seconds), Firetell will retry the delivery: | Attempt | Delay | |---------|-------| | 1st retry | 1 minute | | 2nd retry | 5 minutes | | 3rd retry | 30 minutes | | 4th retry | 2 hours | After 4 failed retries, the webhook is marked as failed and no further attempts are made. ## Security Validate incoming webhooks by checking the `X-Webhook-Signature` header. This header contains an HMAC-SHA256 signature of the JSON request body, signed with your webhook secret (prefixed with `whse-`). ```javascript const crypto = require('crypto'); function verifySignature(payload, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } // Usage in an Express handler: app.post('/webhooks/firetell', (req, res) => { const signature = req.headers['x-webhook-signature']; const isValid = verifySignature( JSON.stringify(req.body), signature, 'whse-your-webhook-secret' ); if (!isValid) return res.status(401).send('Invalid signature'); // Process event... res.status(200).send('OK'); }); ``` ## Best Practices - **Respond quickly** — Return `200` immediately and process asynchronously - **Handle duplicates** — Use the event ID for idempotency - **Use HTTPS** — Always use HTTPS endpoints in production - **Validate signatures** — Verify the `X-Webhook-Signature` header ---