# Telnyx Other APIs — Full Documentation > Account and billing, video, fax, SETI, OAuth, and platform APIs. Complete page content for the Other APIs section of the Telnyx developer docs (https://developers.telnyx.com). > Root index: https://developers.telnyx.com/llms.txt · Lightweight index for this section: https://telnyx-openapi-ng.s3.us-east-1.amazonaws.com/llms/other-apis.txt ## Account & Billing ### Usage Reports > Source: https://developers.telnyx.com/docs/reporting/usage-reports.md ## Overview The (v2) Usage Reports API is a single endpoint that enables viewing aggregated usage data across all of a customer's Telnyx products. It is built to be flexible and customizable, so that you have programmatic access to view data in a way that makes sense to your business and use cases. It can be used to efficiently monitor usage trends and costs, as well as to directly integrate with your internal systems. ### Query Requirements #### Products A product from the Options response (see next section) must be included. At this time, there is not a way to view usage for all or multiple products at once. Products Supported: * Sip-Trunking (sip-trunking) * Messaging (messaging) * Voice API (call-control) * Wireless (wireless) * Cloud Storage (cloud-storage) * Inference (inference) * Verify 2fa (verify-2fa) * Fax API (fax-api) * WebRTC (webrtc) * Calls per Second (cps) * Conference (conference) * Video API (programmable-video) * Call Control Features (call-control-features) * Customer Service Record (customer-service-record) * Media Streaming (media-streaming) * Noise Suppression (noise-suppression) * Text to Speech (text-to-speech) * Speech to Text (speech-to-text) * Recording (recording) * Forking (forking) * Media Storage (media-storage) * Answering Machine Detection (amd) #### Dates The start and end date parameters should be in the format `YYYY-MM-DDThh:mm:ssTZD` (eg `2022-12-24T19:20:30-05:00`). This allows you to specify the timespan in your local time by including the timezone offset. The `start_date` parameter is inclusive, while the `end_date` parameter is exclusive - e.g. if you wanted data for all of February, the `start_date` would be `2023-02-01T00:00:00-00:00` and the `end_date` would be `2023-03-01T00:00:00-00:00`. Billing usage is calculated using UTC. The maximum range supported is 31 days. Another way to specify the timespan you want is to utilize the date literals spelled out in our developer docs - https://developers.telnyx.com/api#date-literals. This can be achieved by replacing `start_date` & `end_date` with `date_range` like this: `date_range=last_1_weeks`. To see the requested data broken out by day for the timespan selected, there is a `date` dimension available on all products. Additionally, there’s a breakout by hour available using the `date_time` dimension. #### Format Specifying a format is optional. The endpoint returns JSON by default. At this time, the API also supports generating a CSV file from the response by specifying `format=csv` in your query. ## Dimensions & Metrics Options Per Product To view the products with their dimensions and metrics available for reporting, you can make a request to the endpoint below. Metrics are the values you’re interested in, for example, the number of calls attempted or the total cost of your usage. Dimensions are how you want your data broken out, for example, by connection, messaging profile, or the traffic’s direction. Below, you’ll see a sample of the “Options” response for Sip-Trunking. If you already know the product you want and are looking for only its supported dimensions/metrics, you can pass the product parameter as shown in the example. If you want to see all products, simply leave off the product parameter. ##### Request: *Don't forget to update `YOUR_API_KEY` here.* ``` curl --location 'https://api.telnyx.com/v2/usage_reports/options?product=sip-trunking' \ --header 'Authorization: Bearer YOUR_API_KEY ``` ##### Response: ```json { "data": [ { "product": "sip-trunking", "product_dimensions": [ "date", "tn_type", "tags", "billing_group_id", "country_code", "outbound_profile_id", "source_tn_type", "date_time", "connection_id", "bundle_id", "short_duration_call", "source_country_code", "currency", "is_local_calling", "call_type", "direction" ], "product_metrics": [ "connected", "cost", "attempted", "call_sec", "completed", "billed_sec" ], "record_types": null } ] } ``` ## Product Examples ### SIP Trunking #### Example 1: Simple SIP Report Let’s say we’re interested in our sip-trunking usage for the week of January 23rd. And let’s say we want to see the total calls connected versus calls attempted and the total cost. And we need these numbers broken out by direction and country. We first make a query to the Options endpoint listed above to make sure those requirements are all supported and to see how to query for them. These are our requirements shown next to how we’d construct our query for them: Product: sip-trunking product=sip-trunking Date(s): January 23rd - 28th 2024 start_date=2024-01-23T00:00:00-00:00end_date=2024-01-29T00:00:00-00:00 Metrics: calls connected, calls attempted, cotal cost metrics=connected,attempted,cost Broken out by destination country*Note: Since we’re filtering on the direction, we must include it.* dimensions=country_code,direction Outbound only filter[direction]=outbound API Response’s Page Size/Number page[number]=1page[size]=20 ##### Request: ``` curl --location --request GET 'https://api.telnyx.com/v2/usage_reports?product=sip-trunking&start_date=2024-01-23T00:00:00-00:00&end_date=2024-01-29T00:00:00-00:00&metrics=connected,attempted,cost&dimensions=direction,country_code&filter[direction]=outbound&page[number]=1&page[size]=20' \ --header 'Authorization: Bearer YOUR_TELNYX_KEY' ``` ##### Response: From the response below, we can see that during the week in question, our account only had traffic to the US (country code = 1). Our connected versus attempted is 883,930 vs 1,245,811 calls respectively. And the total cost for those outbound calls was 1911.624 (*Note: If you had wanted the currency, that is another dimension that can be added in the query with* `dimensions=currency`). ```json { "data": [ { "country_code": "1", "direction": "outbound", "product": "sip-trunking", "connected": 883930.0, "cost": 1911.6248, "attempted": 1245811.0 } ], "meta": { "page_size": 20, "page_number": 1, "total_results": 1, "total_pages": 1 } } ``` #### Example 2: Short Duration Calls Telnyx enforces automatic surcharges for customer accounts whose short duration calls (greater than 0 and less than or equal to 6 seconds) total 15% or more of their outbound traffic. As someone running SIP traffic, I may want to know if I’m in danger of receiving the surcharge at the end of the month. We have exposed a metric solely for the purpose of this calculation. Product: sip-trunking product=sip-trunking Date(s): January 2024 start_date=2024-01-01T00:00:00-00:00end_date=2024-02-01T00:00:00-00:00 Metrics: calls completed metrics=completed Broken out by SDC (short duration call: true or false)*Note: Since we’re filtering on the direction, we must include it.* dimensions=short_duration_call,direction Outbound only filter[direction]=outbound API Response’s Page Size/Number page[number]=1page[size]=20 ##### Request: ``` curl --location -g --request GET 'https://api.telnyx.com/v2/usage_reports?product=sip-trunking&start_date=2024-01-01T00:00:00-00:00&end_date=2024-02-01T00:00:00-00:00&metrics=completed&dimensions=direction,short_duration_call&filter[direction]=outbound&page[number]=1&page[size]=20' \ --header 'Authorization: Bearer YOUR_TELNYX_KEY' ``` ##### Response: From the response below, we can determine the percentage of our traffic that is currently considered short duration: `414515 / (414515 + 522094) = 44%`. If we wanted to take this a step further, we could see which connections are the biggest offenders by adding “connection_id” to the dimensions list. ```json { "data": [ { "short_duration_call": "false", "direction": "outbound", "product": "sip-trunking", "completed": 522094.0 }, { "short_duration_call": "true", "direction": "outbound", "product": "sip-trunking", "completed": 414515.0 } ] , "meta": { "page_size": 20, "page_number": 1, "total_results": 2, "total_pages": 1 } } ``` ### Messaging #### Example 1: Simple Messaging Report In this example, we’ll put together a simple request to view our Messaging usage for a single day. Product: messaging product=messaging Date(s): Today date_range=today Metrics: total cost, total messages sent, total message parts metrics=cost,count,parts Broken out by Carrier*Note: Since we’re filtering on the direction, we must include it.* dimensions=short_duration_call,direction Outbound only filter[direction]=outbound API Response’s Page Size/Number page[number]=1page[size]=20 ##### Request: ``` curl --location -g --request GET 'https://api.telnyx.com/v2/usage_reports?product=messaging&dimensions=direction,normalized_carrier&metrics=cost,parts,count&date_range=today&filter[direction]=outbound&page[number]=1&page[size]=20' \ --header 'Authorization: Bearer YOUR_TELNYX_KEY' ``` ##### Response (abbreviated): ```json { "data": [ ... { "normalized_carrier": "AT&T", "direction": "outbound", "product": "messaging", "cost": 132.4653, "parts": 33952.0, "count": 18088 }, { "normalized_carrier": "Verizon Wireless", "direction": "outbound", "product": "messaging", "cost": 205.2036, "parts": 48924.0, "count": 25171 } ... ], "meta": { "page_size": 20, "page_number": 1, "total_results": 27, "total_pages": 2 } } ``` #### Example 2: Alphanumeric by MNC/MCC This example is a little more complex, and lays out a monthly Messaging report of our Alphanumeric traffic per country, broken out by MCC (Mobile Country Code) and MNC (Mobile Network Code). Product: messaging product=messaging Date(s): January 2024 start_date=2024-01-01T00:00:00-00:00end_date=2024-02-01T00:00:00-00:00 Metrics: total cost, total message parts metrics=cost,parts Dimensions: mcc, mnc, destination country, status_v2 dimensions=direction,mcc,mnc,country_iso,product_name,status_v2 Filters: alphanumeric, outbound, billable statuses only filter[product_name]=alphanumeric_idfilter[direction]=outboundfilter[status_v2]=deliveredfilter[status_v2]=delivery_failedfilter[status_v2]=delivery_unconfirmed ##### Request: ``` curl --location -g --request GET 'https://api.telnyx.com/v2/usage_reports?start_date=2024-01-01T00:00:00-00:00&end_date=2024-02-01T00:00:00-00:00&product=messaging&metrics=parts,cost&dimensions=direction,mcc,mnc,country_iso,product_name,status_v2&filter[product_name]=alphanumeric_id&filter[direction]=outbound&filter[status_v2]=delivered&filter[status_v2]=delivery_failed&filter[status_v2]=delivery_unconfirmed' \ --header 'Authorization: Bearer YOUR_TELNYX_KEY' ``` ##### Response (abbreviated): ```json { "data": [ ... { "status_v2": "delivered", "mnc": "01", "mcc": "270", "product_name": "alphanumeric_id", "country_iso": "LU", "direction": "outbound", "product": "messaging", "cost": 9.519, "parts": 167.0 }, { "status_v2": "delivered", "mnc": "01", "mcc": "272", "product_name": "alphanumeric_id", "country_iso": "IE", "direction": "outbound", "product": "messaging", "cost": 659.2542, "parts": 18623.0 }, ... ], "meta": { "page_size": 20, "page_number": 1, "total_results": 100, "total_pages": 5 } } ``` --- ### Session Analysis > Source: https://developers.telnyx.com/docs/reporting/session-analysis.md # Session Analysis API Usage Guide Understand the full cost and event tree for any Telnyx AI or voice session — from a single inference call to complex multi-product conversations. --- ## What is Session Analysis? Session Analysis gives you a complete view of costs and events for any usage session on the Telnyx platform: - **Full event tree** — parent events, child events, and their relationships - **Rollup costs** — per-event costs plus cumulative totals across the session - **Product linkages** — how different Telnyx products interacted in a single session This is especially valuable for AI-powered sessions, where a single call can involve multiple Telnyx products — AI Voice Assistant, inference, speech-to-text, text-to-speech, call control, SIP trunking, and more — all layered into one conversation. ### When to Use It | Use Case | Why Session Analysis Helps | | :---- | :---- | | **AI session cost breakdown** | See the full cost tree for an AI assistant call, broken down by inference turns, STT/TTS, and infrastructure costs | | **Troubleshooting** | Trace the complete event chain to debug unexpected call flow or billing issues | | **Usage auditing** | Verify which products and services were consumed during a session | | **Cost optimization** | Identify which components of an AI session drive the most cost and optimize accordingly | --- ## Core Concepts ### Record Types A **record type** identifies the category of usage event for a Telnyx product. Each record type maps to a specific product and has defined relationships to other record types. Example: `ai-voice-assistant` is the record type for AI Voice Assistant sessions, which have `inference` events as children. ### Sessions A **session** is a tree of related usage events. The **root event** is the primary event you're analyzing. **Child events** are related usage that occurred as part of that session. **Example:** An AI Voice Assistant call involves a `call-session` root with a `call-control` child, which in turn has an `ai-voice-assistant` child with `inference` events underneath — each representing one LLM turn in the conversation. ### Relationships Events are connected via **relationships**: | Relationship | Direction | Example | | :---- | :---- | :---- | | `child_of` | Parent → Child | An `ai-voice-assistant` event has `inference` children (one per LLM turn) | | `parent_of` | Child → Parent | An `inference` event has a parent `ai-voice-assistant` session | Relationships are defined via **field mappings** — the child's `local_field` matches the parent's `parent_field`. For example, `inference` events link to their `ai-voice-assistant` parent via the `conversation_id` field. ### Cost Rollup Each event node includes: - **`event_cost`** — Cost of this individual event - **`cumulative_cost`** — This event's cost plus all descendant costs The root event's `cumulative_cost` equals the total session cost. For AI sessions, this means the `call-session` cumulative cost includes everything — the AI assistant, each inference turn, STT, TTS, call control, and SIP trunking — rolled up into one number. --- ## Endpoints ### 1. List All Record Types ``` GET /v2/session_analysis/metadata ``` Returns all available record types, their relationships, and query parameter options. **Response includes:** - `record_types[]` — Array of all record types with their parent/child relationships - `query_parameters` — Valid parameters for session analysis queries - `meta` — Total count and last updated timestamp **Example:** ```shell curl "https://api.telnyx.com/v2/session_analysis/metadata" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### 2. Get Record Type Details ``` GET /v2/session_analysis/metadata/{record_type} ``` Returns metadata for a specific record type with additional details: - Valid child and parent relationships - Example query URLs - Recommended max depth for traversal **Example:** ```shell curl "https://api.telnyx.com/v2/session_analysis/metadata/ai-voice-assistant" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### 3. Get Session Analysis ``` GET /v2/session_analysis/{record_type}/{event_id} ``` Retrieves the full session analysis tree for a specific event. **Path Parameters:** | Parameter | Type | Description | | :---- | :---- | :---- | | `record_type` | string | The record type identifier (see [Record Types Reference](#record-types-reference) below) | | `event_id` | UUID | The event's unique identifier | You can find event IDs in the individual webhooks or detail records for the applicable event. **Which record type should I use?** If you're analyzing a call made with your AI assistant, start with `call-session`. This gives you the full picture — AI assistant sessions, inference events, call control, SIP trunking, and all other products in one tree. Use the metadata endpoint to explore other starting points for different products. **Query Parameters:** | Parameter | Type | Default | Description | | :---- | :---- | :---- | :---- | | `include_children` | boolean | `true` | Include child events in the response | | `max_depth` | integer | `2` | Maximum traversal depth (1-5) | | `expand` | string | `record` | Only `record` (include full record data) or `none` (omit record data) | | `date_time` | ISO 8601 | — | Timestamp to narrow index selection (improves performance) | **Example:** ```shell curl "https://api.telnyx.com/v2/session_analysis/call-session/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d?include_children=true&max_depth=3&date_time=2026-03-17" \ -H "Authorization: Bearer YOUR_API_KEY" ``` --- ## Response Structure ### Top-Level Fields ```json { "session_id": "4c9d22b0-1e4b-11f1-83a0-02420aef51a1", "cost": { "total": "0.009800", "currency": "USD" }, "root": { /* EventNode */ }, "meta": { "event_count": 3, "products": ["ai-voice-assistant", "inference", "callcontrol-cdrs"] } } ``` | Field | Description | | :---- | :---- | | `session_id` | Identifier for the analyzed session | | `cost.total` | Total session cost (sum of all events) | | `cost.currency` | ISO 4217 currency code | | `root` | Root event node (tree structure) | | `meta.event_count` | Total events in the tree | | `meta.products` | Unique products involved in the session | ### Event Node Structure Each event in the tree follows this structure: ```json { "id": "4c9d22b0-1e4b-11f1-83a0-02420aef51a1", "product": "callcontrol-cdrs", "event_name": "callcontrol-cdrs", "relationship": null, "cost": { "event_cost": "0.000000", "cumulative_cost": "0.009800", "currency": "USD" }, "links": { "self": "/v2/session_analysis/call-control/4c9d22b0-...", "records": "/v2/detail_records?record_type=call-control&id=4c9d22b0-..." }, "record": { /* Full detail record */ }, "children": [ /* Array of child EventNodes */ ] } ``` | Field | Description | | :---- | :---- | | `id` | Event identifier | | `product` | Internal product identifier | | `event_name` | Event type name | | `relationship` | How this event relates to its parent (null for root) | | `cost.event_cost` | Cost of this individual event | | `cost.cumulative_cost` | This event + all descendants | | `links.self` | Link to this analysis node | | `links.records` | Link to underlying detail records | | `record` | Full detail record data (varies by record type) | | `children` | Child event nodes | ### Relationship Object For child events, the `relationship` field explains the connection: ```json { "type": "child_of", "via": { "local_field": "conversation_id", "parent_field": "conversation_id" }, "parent_id": "4c9063e0-1e4b-11f1-b789-02420aef51a1" } ``` | Field | Description | | :---- | :---- | | `type` | Relationship type (`child_of` or `parent_of`) | | `via.local_field` | Field on this record that links to parent | | `via.parent_field` | Field on the parent record that's matched | | `parent_id` | Identifier of the parent event | Note: The `via` fields vary by relationship. AI-related linkages often use `conversation_id`, while voice/telephony relationships typically use `telnyx_session_id` or `call_session_id`. --- ## Example: AI Voice Assistant Session A customer makes an inbound call to an AI Voice Assistant using the Telnyx Web Dialer. The assistant handles a multi-turn conversation over ~152 seconds. **Query:** ```shell curl "https://api.telnyx.com/v2/session_analysis/call-session/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d?max_depth=5&include_children=true&date_time=2026-03-17" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Session tree:** ``` call-session (SIP Trunking) event: $0.0000 cumulative: $0.2128 ├── recording event: $0.0051 cumulative: $0.0051 ├── webrtc event: $0.0052 cumulative: $0.0052 ├── sip-trunking event: $0.0052 cumulative: $0.0052 └── call-control event: $0.0052 cumulative: $0.1973 └── ai-voice-assistant event: $0.1800 cumulative: $0.1921 ├── inference event: $0.0010 ├── inference event: $0.0015 ├── inference event: $0.0029 ├── inference event: $0.0033 └── inference event: $0.0034 ``` **Summary:** | Field | Value | | :---- | :---- | | Total session cost | $0.2128 | | Event count | 11 | | Duration | ~152 seconds | | Products involved | siptrunking-cdrs, recording-cdrs, webrtc-cdrs, callcontrol-cdrs, ai-voice-assistant, inference | **Key observations:** - The **call-session** root has $0.00 event cost — it's a container for the session, not a billable event itself. - **ai-voice-assistant** is the dominant cost at $0.18 (billed at $0.06/min for 3 billed minutes). Its cumulative cost of $0.1921 includes 5 inference events underneath. - Each **inference** event represents a single LLM call during the conversation — one per assistant turn. The cost per turn varies based on the model and token count. - **call-control** has a $0.0052 event cost but a $0.1973 cumulative cost — the difference is the ai-voice-assistant subtree rolling up underneath it. - **recording**, **webrtc**, and **sip-trunking** are leaf nodes with no children, so their event cost equals their cumulative cost. ### Understanding the AI Cost Structure In an AI-powered call, costs come from multiple layers: | Layer | Record Type | What You're Billed For | | :---- | :---- | :---- | | **AI Assistant** | `ai-voice-assistant` | Time-based (per minute of assistant usage) | | **LLM Inference** | `inference` | Per-turn (each LLM call is a separate event) | | **Call Control** | `call-control` | Time-based (per minute of call control usage) | | **Transport** | `sip-trunking`, `webrtc` | Time-based (per minute for the voice leg) | | **Add-ons** | `recording`, `speech-to-text`, `text-to-speech` | Per-use or per-minute depending on the product | Session Analysis rolls all of these into one tree, so you can see exactly where cost accumulates — whether it's the AI assistant minute rate, the number of inference turns, or the underlying voice transport. --- ## Common Use Cases ### Debugging High-Cost AI Sessions 1. Query the session with `max_depth=5` to get the full tree 2. Look at `meta.products` to see which products were involved 3. Compare `event_cost` across children — if `ai-voice-assistant` is the driver, check how many inference turns occurred underneath 4. Check `record` fields for rate information (`rate`, `rate_measured_in`) and duration ### Finding Related Events 1. Start from any event in the tree 2. Use `relationship.via` to understand the field linkage 3. Query the parent or children directly using their `id` ### Exporting for Analysis 1. Use `expand=none` to get just the tree structure without full records 2. Flatten the tree programmatically for spreadsheet import 3. Or keep full records and parse specific fields (e.g., `rate`, `duration_sec`) --- ## Best Practices ### Performance - **Provide `date_time`** when you know it — this narrows the index search and improves response time - **Use appropriate `max_depth`** — start with `2`, increase only if needed. AI sessions with inference events typically need `max_depth=4` or `5` to reach the leaf level. - **Use `expand=none`** if you only need cost rollups, not full records ### Querying - **Start from the root event** — the most complete picture comes from the top-level event - **Check metadata first** — `/metadata/{record_type}` shows expected children and recommended depth - **Use `links.records`** to get the underlying detail records for deeper analysis ### Error Handling | Status Code | Meaning | Action | | :---- | :---- | :---- | | 400 | Invalid parameters | Check `record_type` spelling, `event_id` format | | 403 | Forbidden | Verify API key has access to this user's data | | 404 | Event not found | Event may not exist, or `date_time` is incorrect | | 500 | Internal error | Retry with exponential backoff | --- ## Record Types Reference ### AI & Voice Intelligence | Record Type | Product | Description | Common Children | | :---- | :---- | :---- | :---- | | `ai-voice-assistant` | AI Voice Assistant | AI assistant sessions | `inference` | | `inference` | Inference | AI inference events (one per LLM turn) | — | | `inference-speech-to-text` | Inference | STT-specific inference | — | | `inference-text-to-speech` | Inference | TTS-specific inference | — | | `summarization` | Inference | Text summarization | — | | `embedding` | Inference | Vector embeddings | — | | `speech-to-text` | Speech-to-Text | STT transcription records | — | | `text-to-speech` | Text-to-Speech | TTS synthesis records | — | | `amd` | Answering Machine Detection | AMD invocations | — | | `noise-suppression` | Noise Suppression | Audio enhancement records | — | ### Voice & Telephony | Record Type | Product | Description | Common Children | | :---- | :---- | :---- | :---- | | `call-session` | N/A | Root-level record for grouping related voice product usage records | `call-control`, `sip-trunking`, `recording`, `webrtc`, `siprec-client`, `fax-api`, `conference-participant` | | `call-control` | Call Control (Programmable Voice) | Voice API call records | `ai-voice-assistant`, `amd`, `media-streaming`, `speech-to-text`, `text-to-speech`, `noise-suppression` | | `sip-trunking` | SIP Trunking | SIP trunking detail records | — | | `webrtc` | WebRTC | WebRTC call records | — | ### Media & Storage | Record Type | Product | Description | Common Children | | :---- | :---- | :---- | :---- | | `recording` | Call Recording | Call recording CDRs | — | | `media_storage` | Media Storage | Stored media files | — | | `media-streaming` | Media Streaming | Real-time audio streams | — | | `forking` | Media Forking | Forked media records | — | ### Conferencing | Record Type | Product | Description | Common Children | | :---- | :---- | :---- | :---- | | `conference` | Conference | Conference sessions | `conference-participant` | | `conference-participant` | Conference | Individual participant records | — | ### Messaging | Record Type | Product | Description | Common Children | | :---- | :---- | :---- | :---- | | `message` | Messaging | SMS/MMS message records | `ai-messaging-assistant` | ### Video | Record Type | Product | Description | Common Children | | :---- | :---- | :---- | :---- | | `room-session-event` | Programmable Video | Video room sessions | — | | `room-session-participant-event` | Programmable Video | Video participants | — | | `room-session-recording-event` | Programmable Video | Video recordings | — | | `room-session-composition-event` | Programmable Video | Video compositions | — | ### Other Products | Record Type | Product | Description | Common Children | | :---- | :---- | :---- | :---- | | `verify` | Verify (2FA) | Verification attempts | — | | `fax-api` | Fax API | Fax transmission records | — | | `branded-calling` | Branded Calling | Caller ID branding | — | | `siprec-client` | SIPREC | SIPREC recording sessions | — | | `sim_card_usage` | Wireless | SIM card data usage | — | | `customer-service-record` | CSR | Customer service records | — | --- ### On-Demand Reports > Source: https://developers.telnyx.com/docs/reporting/on-demand-reports.md On-Demand Reports lets you query your Telnyx usage data using natural language. Instead of building filters and exporting CSVs, describe the report you want in plain English and the system generates it. --- ## Overview On-Demand Reports uses AI to translate natural language into structured queries against your [Usage Report](/docs/reporting/usage-reports/index) data. You can request specific chart types, date ranges, groupings, and breakdowns — all from a single text input. --- ## Getting started 1. Log in to the [Mission Control Portal](https://portal.telnyx.com). 2. Navigate to **Reporting** > **[On-Demand Reports](https://portal.telnyx.com/#/reporting/on-demand-reports)**. 3. Type a query describing the report you need and submit it. ![On-Demand Reports query interface](/img/on-demand-reports-interface.png) --- ## Example queries - "Daily wireless spend for the last 10 days" - "Number of messages by direction for last week as a pie chart" - "Weekly total calls last quarter" - "Average SIP cost for last 2 weeks as line graph" - "SIP usage for the last month as a table" --- ## What you can do | Capability | Description | |---|---| | **Natural language queries** | Describe reports in plain English instead of building filters manually | | **Chart type selection** | Request bar charts, pie charts, line graphs, or tabular breakdowns | | **Date range control** | Specify relative ("last 10 days") or absolute ("in February") time periods | | **Grouping and breakdowns** | Group data by direction, product, day, week, or other dimensions | --- ## Current limitations - Queries run against **v2 Usage Report data** only. Products and dimensions available in Usage Reports are available here. - **Not a general knowledge assistant** — the system answers questions about your Telnyx usage data, not general questions. - This is the first release. Coverage and capabilities will expand over time. --- ## Video ### Getting Started > Source: https://developers.telnyx.com/docs/video/get-started.md Telnyx Video Rooms are enabled using the Rooms API. You can create as many rooms as needed using this API. To access a Telnyx Video Room, a Client Join Token will first need to be generated in JWT form. You can use our HTTPS API and authenticate using the API Key associated with your Mission Control account under API Keys. Find out more about authenticating with API V2 here. Adding Telnyx Rooms functionality to your app can be done using our JS SDK (mobile SDKs coming soon). To gain access to a live Video Rooms sample app, reach out to our sales team today! Check out the full API reference today! ## Glossary Room Resource representing a virtual place where multiple endpoints using one of Telnyx’s Programmable Video SDKs can connect. Room Session Resource representing a moment where multiple Room Participants were communicating within a given Room. Room Participant Resource representing an endpoint using one of Telnyx’s Programmable Video SDKs to connect to a given Room. JWT JSON Web Token. A standard method for representing claims. Client Join Token (JWT) A JWT token which contains grants allowing in the Room usecase to join a Room. Refresh Token (JWT) A JWT token which permits to obtain a new Client Token with same grants. API Key Secret API Key generated via Portal and used to authenticate Telnyx API calls. Video SDK A library used to provide Video features to your application using Telnyx Video platform. Configuration and usage Telnyx Video is enabled using Video Rooms. A Video Room represents communications session among multiple endpoints using one of Telnyx’s Programmable Video SDKs. Connected users (Participants) can share video and audio Tracks with the Room, and receive video and audio Tracks from other Participants in the Room. You can as many Rooms as you want. For example you could create a long lived Room such as "Daily Standup", or Rooms that you would delete after it's been used like "1-1 with X". To create a Video Room you can use the REST API V2 documented{" "} here. A Video Room can only be joined if the client owns a Client Join Token, you can create it using the REST API V2 documented{" "} here. The Client Join Token is short lived and you will be able to refresh it using the{" "} Refresh Token provided with it when you request for a Client Join Token. Once you have a Video Room and an Client Join Token for it, you can then use our Video SDK on your client side to connect your client to the Room you just created. ## Concepts ### Architecture `Video Rooms` is a platform that enables developers to add audio and video capability to Web, Android, and iOS applications. The platform consists of REST APIs, Client SDKs, and our mission control portal that makes it really easily to capture, stream, record, and render live audio and video. A video application built with Video Rooms has to parts: - __Client:__ Our client side Javascript, iOS, and Android SDKs used to interact with a `Room` instance - __Server:__ Our REST APIs and portal to create/manage room and session, configuring recording, or leverage our `Participants API` to moderator participants in a `Room`. ### Terms Understanding the basic concepts of the video SDK will help you understand how it works. These concepts apply in general across all of our platforms. - A `Room` represents a real time audio/video/screen share session with other people or participants. It is fundamental to building a video application. - `Room State` tracks the state of the room as it changes making it extremely easy to understand what's happened to a `Room`. - `For example`: Room State could change due to a Local Participant has started publishing a stream or because a Remote Participant left. A Stream represents the audio/video media streams that are shared by Participants in a Room - A `Participant` represents a person inside a `Room`. Each `Room` has one `Local Participant` and one or more `Remote Participants`. - A `Stream` represents the audio/video media streams that are shared by `Participants` in a `Room` - A `Stream` is indentified by it's `participantId` and `streamKey` - A `Subscription` is used to subscribe to a `Stream` belonging to a `Remote Participant` ### Dive a bit deeper Dive a little bit deeper into our `Video Rooms` platform to get a better understand of what its capable of, and what you can buid. Learn more about our Client SDKs and Server APIs. ### Client SDKs Our [Javascript SDK API reference](/docs/video/javascript-sdk) which details the API of our SDK including behaviors of the `Room` class and the `Events` that triggers and how they function. - [Rooms Class API Reference](/docs/video/javascript-sdk/room-events#room) - [Room Events Reference](/docs/video/javascript-sdk/room-events#room-events) ### Server APIs - [Rooms](/api-reference/rooms/view-a-list-of-rooms) - manage Rooms - [Client Access Tokens](/api-reference/rooms-client-tokens/create-client-token-to-join-a-room#create-client-token-to-join-a-room) - manage client access tokens needed to interact with a `Room` - [Sessions](/api-reference/room-sessions/view-a-list-of-room-sessions) - manage room sessions, end a session, and mute/unmute/kick all participants in a given session. - [Participants](/api-reference/room-participants/view-a-list-of-room-participants) - search for participants based on a number of filters like `session id` - [Recordings](/api-reference/room-recordings/view-a-list-of-room-recordings) - manage recordings, including bulk delete. - [Compositions](/api-reference/room-compositions/view-a-list-of-room-compositions) - create and manage compositions. --- ### Getting Started > Source: https://developers.telnyx.com/docs/video/javascript-sdk.md The Telnyx Video Client SDK provides all the functionality you need to join and interact with a video room from a browser. ![npm](https://img.shields.io/npm/v/@telnyx/video.svg?color=lightgrey&style=flat) ## Adding Telnyx to your JavaScript client application Include the `@telnyx/video` npm module as a dependency: `npm install @telnyx/video --save` After pasting the above content, Kindly check and remove any new line added Then, import `@telnyx/video` in your application code. ```javascript // main.js import { Room, createLocalParticipant } from '@telnyx/video'; ``` After pasting the above content, Kindly check and remove any new line added Now you are ready to connect to a video room that you created. In order to connect to a video room you will require a client token that has the necessary grants to join the room. ```javascript const room = new Room(roomId, { clientToken: '', localParticipant: createLocalParticipant({ context: JSON.stringify({ name: 'Bob The Builder', id: 1 }), // send data that you can associate with this participant (for e.g., userId for this participant in your DB) }), }); const stateCallback = (state) => { // the state object is immutable and can be easily integrated with most modern UI libraries like React, Vue etc. }; room.on('state_changed', stateCallback); room.connect().then(() => { console.log('You are connected to the room!'); }); ``` After pasting the above content, Kindly check and remove any new line added ## Understanding the state of the video room The `state_changed` event callback contains the state of the SDK at that point in time. This is an immutable object that you can use in most modern UI libraries like React and Vue. The Typescript definition of the State is helpful in understanding the structure of this object. ```javascript type Status = | 'initialized' | 'connecting' | 'connected' | 'disconnecting' | 'disconnected'; interface State { status: Status; localParticipantId: Participant['id']; participants: { [id: string]: Participant; }; streams: { [id: string]: Stream; }; } ``` After pasting the above content, Kindly check and remove any new line added Everytime the state of the SDK changes the `state_changed` callback is invoked with a new immutable state that represents the current state of the SDK. Since most modern UI libraries are able to compare the two immutable states and render only the components that changed it makes it easier to integrate the SDK with them rather than depending on multiple event callbacks. For e.g., based on how we initialized it in the above code example the initial state of the SDK would result in an object give below. Note that the ID of the local participant is unique and are generated based on UUID v4 standard when you create the local participant. ```javascript { status: 'initialized', localParticipantId: "8c3bacb5-2e90-4379-8ee8-d5446213fee9", participants: { "8c3bacb5-2e90-4379-8ee8-d5446213fee9": { id: "8c3bacb5-2e90-4379-8ee8-d5446213fee9", context: "{\"name\":\"Bob The Builder\",\"id\":1}", streams: {}, }, }, streams: {}, }; ``` After pasting the above content, Kindly check and remove any new line added At this point there are no streams being published by the local participant. We will come to that shortly. The participant object follows the TypeScript interface given below. ```javascript interface Participant { id: string; context?: string; streams: { [key: string]: Stream['id']; }; } ``` After pasting the above content, Kindly check and remove any new line added When we called the `room.connect()` method in the example code the state of the SDK will change to the following ... ```javascript { status: 'connecting', localParticipantId: "8c3bacb5-2e90-4379-8ee8-d5446213fee9", participants: { "8c3bacb5-2e90-4379-8ee8-d5446213fee9": { id: "8c3bacb5-2e90-4379-8ee8-d5446213fee9", context: "{\"name\":\"Bob The Builder\",\"id\":1}", streams: {}, }, }, streams: {}, } ``` After pasting the above content, Kindly check and remove any new line added This new state will be available to your application via the `state_changed` callback. Since state is immutable the only difference between the initial state and the new state is the `status` property. If you are using a modern UI library like React you can easily rerender the components to show that the application is connecting to the room. When successfully connected the state changes to ... ```javascript { status: "connected", localParticipantId: "8c3bacb5-2e90-4379-8ee8-d5446213fee9", participants: { "8c3bacb5-2e90-4379-8ee8-d5446213fee9": { id: "8c3bacb5-2e90-4379-8ee8-d5446213fee9", context: "{\"name\":\"Bob The Builder\",\"id\":1}", streams: {}, }, }, streams: {}, } ``` After pasting the above content, Kindly check and remove any new line added Now we are ready to start publishing streams on behalf of the local participant. ## Publishing your local camera and mic stream In order to publish a stream you need to define the constraints of the media. The simplest form of the constraints is ... ```javascript const constraints = { audio: true, video: true }; ``` After pasting the above content, Kindly check and remove any new line added With these constraints the SDK will try to obtain both audio and video from the local participant. In order to publish the stream as the local participant you have to use the `publish` method available to you via the room object. ```javascript room.publish('self', { constraints: { audio: true, video: true }, }); ``` After pasting the above content, Kindly check and remove any new line added The first argument to `publish` is a string that acts as the key that you can use to refer to this specific stream. You can use any valid string for this as long as you are consistent in your application. For e.g. here we're using 'self' for the camera/mic stream but you could use 'presentation' as the key for when you publish video of the screen. We will get to how you can publish your screen in a short while. When you make the request to publish a stream, the browser will ask for the necessary permissions required to access the camera and mic. Once the permissions are acquired the SDK will configure and publish the stream to the room you are connected to. At this point you will receive the new state to the `state_changed` event callback with the newly created stream. ```javascript { status: "connected", localParticipantId: "8c3bacb5-2e90-4379-8ee8-d5446213fee9", participants: { "8c3bacb5-2e90-4379-8ee8-d5446213fee9": { id: "8c3bacb5-2e90-4379-8ee8-d5446213fee9", context: "{\"name\":\"Bob The Builder\",\"id\":1}", streams: { self: "a87dd242-de78-4c19-a09c-23b336c9f25e", }, }, }, streams: { "a87dd242-de78-4c19-a09c-23b336c9f25e": { id: "a87dd242-de78-4c19-a09c-23b336c9f25e", key: "self", // the constraits that was provided by you constraints: { audio: true, video: true, }, bitrate: 256000, // also configurable when you publish a stream audioActive: false, // whether the audio track is being published videoActive: false, // whether the video track is being published source: MediaStream, // an instance of MediaStream that can be used to render video/audio audioTrack: undefined, // this will be an instance of MediaStreamTrack when the track is available videoTrack: undefined, // this will be an instance of MediaStreamTrack when the track is available isSpeaking: false, // whether the audio level of the track is high enough to consider the participant who owns this stream is speaking or not isRemote: false, // whether the stream originates from a remote source or not isPublishing: true, // whether the stream is being published isConfiguring: false, // whether the SDK is currently negotiating the WebRTC connection participantId: "8c3bacb5-2e90-4379-8ee8-d5446213fee9", }, }, } ``` After pasting the above content, Kindly check and remove any new line added The stream object is the most complex object in the state of the SDK. However you don't have to worry about all of these properties at the moment. As you can see the `isPublishing` property of the stream is `true` now. This means that the stream is being published, which involves creating the SDP (Session Description Protocol) and establishing the WebRTC connection. You can't publish another stream of the same key ("self" in our case) until this stream is published or removed (by unpublishing). Once the SDK acquires the media tracks from the camera and mic of the local participant a new state will be returned using the `state_changed` callback. This state will contain the audio and video track from the local media devices. The streams object at this point will look like this ```javascript streams: { "a87dd242-de78-4c19-a09c-23b336c9f25e": { id: "a87dd242-de78-4c19-a09c-23b336c9f25e", key: "self", // the constraits that was provided by you constraints: { audio: true, video: true, }, bitrate: 256000, audioActive: true, videoActive: true, source: MediaStream, audioTrack: MediaStreamTrack, videoTrack: MediaStreamTrack, isSpeaking: false, isRemote: false, isPublishing: true, isConfiguring: true, participantId: "8c3bacb5-2e90-4379-8ee8-d5446213fee9", }, }, ``` After pasting the above content, Kindly check and remove any new line added As you can see at this point you can use the `source` or the media tracks individually from `audioTrack` and `videoTrack` to show the media in your UI. You can also see that the `audioActive` and `videoActive` flags are now true indicating the stream is publishing audio and video respectively. Another property that was updated in the new state is the `isConfiguring` flag. You can ignore this in most use-cases as this indicates if the WebRTC connection is being negotiated. Once the WebRTC connection is negotiated and the stream is successfully published the stream state would look like this ```javascript streams: { "a87dd242-de78-4c19-a09c-23b336c9f25e": { id: "a87dd242-de78-4c19-a09c-23b336c9f25e", key: "self", // the constraints provided by you constraints: { audio: true, video: true, }, bitrate: 256000, audioActive: true, videoActive: true, source: MediaStream, audioTrack: MediaStreamTrack, videoTrack: MediaStreamTrack, isSpeaking: false, isRemote: false, isPublishing: false, isConfiguring: false, participantId: "8c3bacb5-2e90-4379-8ee8-d5446213fee9", }, }, ``` After pasting the above content, Kindly check and remove any new line added At this point the stream will be available to be subscribed by other clients connected to the same room. ## Knowing when a new participant joins the room A video room can have multiple participants and you can get a list of all the participants connected to the room at the time by using the `participants` property in the state. If the participant is not publishing any streams the `streams` property of that participant will be an empty object. Let's imagine that another participant joins the same room from a different browser session with the following context ```javascript const room = new Room(roomId, { clientToken: '', localParticipant: createLocalParticipant({ context: JSON.stringify({ name: 'Oswald', id: 2 }), // send data that you can associate with this participant (for e.g., userId for this participant in your DB) }), }); ``` After pasting the above content, Kindly check and remove any new line added Since the room already contains a participant who is publishing a stream the state once this session is connected would look something like ... ```javascript { status: "connected", localParticipantId: "d42926f5-fe6c-48d5-b24b-7444048fa68e", participants: { "8c3bacb5-2e90-4379-8ee8-d5446213fee9": { id: "8c3bacb5-2e90-4379-8ee8-d5446213fee9", context: "{\"name\":\"Bob The Builder\",\"id\":1}", streams: { self: "a87dd242-de78-4c19-a09c-23b336c9f25e", }, }, "d42926f5-fe6c-48d5-b24b-7444048fa68e": { id: "d42926f5-fe6c-48d5-b24b-7444048fa68e", context: "{\"name\":\"Oswald\",\"id\":2}", streams: {}, }, }, streams: { "a87dd242-de78-4c19-a09c-23b336c9f25e": { id: "a87dd242-de78-4c19-a09c-23b336c9f25e", key: "self", audioActive: true, // whether audio is being published by the remote stream videoActive: true, // whether video is bein published by the remote stream source: MediaStream, audioTrack: undefined, // will be an instance of MediaStreamTrack once subscribed videoTrack: undefined, // will be an instance of MediaStreamTrack once subscribed videoCodec: "vp8", // the codec used by the remote video audioCodec: "opus", // the codec used by the remote audio isRemote: true, isSpeaking: false, // whether the remote participant is speaking or not isConfiguring: false, // whether the WebRTC connection is being negotiated or not subscription: { status: "unsubscribed", // will change to 'subscribed' once the the client subscribes to this stream }, participantId: "8c3bacb5-2e90-4379-8ee8-d5446213fee9", // the ID of the participant this stream belong to }, }, } ``` After pasting the above content, Kindly check and remove any new line added As you can see there is already a participant in the `participants` object that maps to the remote particpant from the previous browser session. This remote participant also has a stream with the key "self". The stream object for "self" has the structure very similar to the one we saw before but has a few new properties. The ones that are particularly interesting are `subscription`, `videoCodec`, and `audioCodec`. The properties for video and audio codec can be used to decide if the browser has the capabilities to decode the video and audio tracks. Then there is `subscription.status`, which is `unpublished` at the moment. ## Subscribing to a remote stream At this point we are ready to subscribe to the remote stream published by the participant. In order to do that we use the `subscribe` method from the room object. ```javascript room.subscribe('a87dd242-de78-4c19-a09c-23b336c9f25e'); ``` After pasting the above content, Kindly check and remove any new line added This will start the WebRTC negotiation to receive the remote stream. Once successful the `subscription.status` will change to `subscribed`. You can use the `source` property, which is an instance of MediaStream to render the media on the browser. If the remote participant unpublishes this stream the SDK will automatically unsubscibe from the stream and the stream will disappear from the state. --- ### Room and Events > Source: https://developers.telnyx.com/docs/video/javascript-sdk/room-events.md Telnyx's video JavaScript SDK lets you manage room events and join/leave notifications. We've made it much easier to add video capability to your web application using our Javascript SDK. The `Room` object and the `Events` that occur in a `Room` is the API you will use to build your video application. We've extensively documented the interface for the `Room` and `Events` objects, below. ## Room ```javascript export type Room = Immutable<{ /// The unique identifier of the Room instance id: string; /// Event listener/handler for various events that are triggered in the room. /// See the Events interface for a list. on(event: E, callback: Events[E]): Unsubscribe; /// Connects a Room instance to the server /// The state of the room, specifically the status of the room, will change while it's connecting, /// if you can listen to these changes with the state_changed event. /// Once the room is connected its local partipant will available in State. connect: () => Promise; /// Disconnect a Room instance from the server /// The status of the state of the room will change while it's disconnecting. disconnect: () => Promise; /// Updates the client token /// The client token is a short lived access token that expires after a time to live in seconds, /// which needs to be refreshed using a refresh token. /// If you are farmilar with how JWT's work you'll understand, head here for [more details](https://jwt.io) /// This was set when the client token was created - the default TTL is 600 seconds or 10 minutes. /// /// Client tokens need to refreshed before they expire otherise the Room instance will automatically /// disconnect. /// It's up to the developer to keep the client token fresh. /// To create a client token please use the `generate_join_client_token` endpoint /// To refresh a token please use `refresh_client_token` endpoint updateClientToken: (clientToken: string) => Promise; /// Gets the state of the Room /// What does the state of the room look like? /* type Status = | 'initialized' | 'connecting' | 'connected' | 'disconnecting' | 'disconnected'; interface State { status: Status; localParticipantId: Participant['id']; participants: { [id: string]: Participant; }; streams: { [id: string]: Stream; }; } */ /// State is important because there can be lot of things changing in a room at any given moment, especially for really large rooms. /// Participants leaving the room, new participants joining, other starting/stopping their video, a moderator kicking /// a participant out. /// It would be a lot to keep track of manually, which is why we have state. /// Don't worry we've abstracted all the accounting, inside our SDK, and provided you with helper methods /// like this one, getParticipant(), and getLocalStreams() to make access simple. getState: () => State; /// Participants and Streams /// There's a two entities that are important to understand /// and as you can see from the State interface above they are big /// part of the State of the Room. /// /// Stream /// A stream represents media stream published by Participant in a Room. /// It's identified by it's participantId and streamKey and typically has a audio track or a video /// track, or both. /// Participants /// There are two type of participants: local and remote. /// Remote Participant - other participants in the room who are not the local participant. /// A local partipant subscribes to streams published by remote participants. /// /// Local Participant - is the person on a mobile device or desktop that joins the room to publish a stream. /// **NOTE**: There can only be one local participant in a room at a given time. /// Gets the local participant for a given Room instance /// You can use this method to find more details about the local participant in the Room /// /// What does a Participant look like? /* export type Participant = Immutable<{ id: string; context?: string; streams: { [key: string]: Stream['id']; }; canReceiveMessages: boolean; origin: 'local' | 'remote' | 'telephony_engine'; }>; */ getLocalParticipant: () => Participant; /// Get the streams associated to the local participant getLocalStreams: () => { [key: Stream['key']]: Stream }; /// Add a stream to a Room /// key - is the unique identifier for the stream based on its realted participant. /// tracks.audio - the audio track /// tracks.video - the video track /// Once the promise is completed you can assume that the stream has started publishing in the room. addStream: ( key: Stream['key'], tracks?: { audio?: MediaStreamTrack; video?: | MediaStreamTrack | { track?: MediaStreamTrack; options?: { enableSimulcast?: boolean } }; } ) => Promise; /// Update an existing stream in a Room /// key - identifier of the stream /// tracks.audio - the audio track /// tracks.video - the video track /// /// Why would you update a Stream? /// If you wanted to mute the audio of a particular stream you would update /// it and set the track to null updateStream: ( key: Stream['key'], tracks?: { audio?: MediaStreamTrack; video?: | MediaStreamTrack | { track?: MediaStreamTrack; options?: { enableSimulcast?: boolean } }; } ) => Promise; /// Remove a stream /// key - identifier of the stream /// Removing a stream removes or unpublishes it from the room. removeStream: (key: string) => Promise; /// Subscriptions /// A subscription is used to subscribed to a stream published by a Remote Participant /// Subscribe to a stream belonging to a remote participant /// participantId - the id of the remote participant /// key - the stream key of the stream /// config.audio - flag to indicate if you'd like to subscribe to the remote streams' audio // config.video - flag to indicate if you'd like to subscribe to the remote streams' video /// When the promise is fullfilled a remote stream is added to state.streams /// which then can be used to render the audio or video of that stream. /// You can access remote participant streams more easily using the getParticipantsStreams // and getParticipantStreams helper methods below. /// /// A developer is not required to subscribe to every remote stream being published in the room instead /// they can choose. This is very useful in situations conserving bandwidth and cpu resources is a priority, /// For example: /// A developer building a vidoe conferencing application and wants to support 100+ participants may only want subscribe /// to 15-20 participants at a time display each set in a seperate page, thus significantly saving bandwith and resources. addSubscription: ( participantId: Participant['id'], key: Stream['key'], config: { audio: boolean; video: boolean } ) => Promise; /// Update an existing subscription /// participantId - the id of the remote participant /// key - the stream key of the stream /// config.audio - flag to indicate if you'd like to subscribe to the remote streams' audio // config.video - flag to indicate if you'd like to subscribe to the remote streams' video /// When a remote participant toggles audio or video on a stream that you're subscribed to /// you'll need to update that subscription. updateSubscription: ( participantId: Participant['id'], key: Stream['key'], config: { audio: boolean; video: boolean } ) => Promise; /// Remove or stop subscribing to a remote participant's stream removeSubscription: ( participantId: Participant['id'], key: Stream['key'] ) => Promise; /// Helper method to easily access all streams for a given remote participant getParticipantStreams: ( participantId: Participant['id'] ) => Map; /// Helper method to easily access a remote participant stream getParticipantStream: ( participantId: Participant['id'], key: Stream['key'] ) => Stream | undefined; /// Provides statistics for a local or remote stream /// It presents the same data you'd get from doing: /// RTCRtpSender.getStats() - https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSender/getStats /// RTCRtpReceiver.getStats() - https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpReceiver/getStats /// using the standard WebRTC APIs but in a much more readable format. getWebRTCStatsForStream: ( participantId: Participant['id'], key: Stream['key'] ) => Promise; /// Send a message to one, more than one, or all participants in the room /// message - a message type, current only 'text' is supported /// receipients? - an array of participants to send the message, if /// null the message is broadcasted to everyone in the room. sendMessage: ( message: Message, recipients?: Array ) => Promise; enableNetworkMetricsReport: ( participantIds: Array, options?: { includeStreams?: boolean } ) => Promise; disableNetworkMetricsReport: ( participantIds?: Array ) => Promise; }>; ``` ## Room events ```javascript export interface Events { /// Triggered when the state of the room changes /// For more /// TODO state_changed: (state: State) => void; /// Triggers on a Room instance when it connects to the server connected: (state: State) => void; /// Triggers on a Room instance when it disconnects from the server disconnected: (state: State) => void; /// Triggered when a remote participant joins the room /// Only triggered for remote participants the local participant does not /// need to be notified that they've joined the room since they've initiated /// that action themselves by connecting to the Room participant_joined: (participant: Participant['id'], state: State) => void; /// Triggered when a participant is leaving the room because they were kicked /// due to a moderator event /// Unlike the joined and left events the local participant can be kicked from the room participant_leaving: ( participant: Participant['id'], reason: 'kicked' | null, state: State ) => void; /// Triggered when a remote participant leaves the room participant_left: (participantId: Participant['id'], state: State) => void; /// Triggered after successfully adding or publishing a stream to the room /// This event is triggered for local and remote streams. /// A local stream is one that's published by the local participant in the Room /// A remote stream is one that's published by a remote participant in the Room stream_published: ( participantId: Participant['id'], key: Stream['key'], state: State ) => void; /// Triggered after successfully unregistering a stream /// This event is triggered for both local and remote streams. /// It triggered when the local or remote participant remove or stops publishing a stream. /// When a remote participant leaves a room it will trigger for any of their related streams. stream_unpublished: ( participantId: Participant['id'], key: Stream['key'], state: State ) => void; /// Triggered when a local stream or a remote stream track has been enabled. /// Notifies consumers about remote stream tracks being enabled. /// For example: when audio is unmuted or video has started on a remote stream. track_enabled: ( participantId: Participant['id'], key: Stream['key'], kind: 'audio' | 'video', state: State ) => void; /// The oposite of track_enabled /// Triggers when a local stream or a remote stream track has been disabled. /// Notifies consumers about remote stream tracks being disabled. /// For example: when audio is muted or video has stopped on a remote stream. track_disabled: ( participantId: Participant['id'], key: Stream['key'], kind: 'audio' | 'video', state: State ) => void; /// Triggered when a track is censored due to a moderator event /// Like the kick moderator event in participant_leaving, both local and remote streams can be /// censored. /// /// Since a Stream consists of an audio and video track, either can be censored by a moderator /// If the audio or video track on given stream is null, effectively nothing happens. track_censored: ( participantId: Participant['id'], key: Stream['key'], kind: 'audio' | 'video', state: State ) => void; /// Triggered when a track is uncensored due to a moderator event /// Opposite of track_censored /// Naturally, a track must be censored to order be uncensored. track_uncensored: ( participantId: Participant['id'], key: Stream['key'], kind: 'audio' | 'video', state: State ) => void; /// Triggered when there is audio activity from a particular stream or participant talking in the Room /// When there's audio activity when a participant speaking this event will trigger with the participant id that's talking and the stream key will be null audio_activity: ( participantId: Participant['id'], key: Stream['key'] | null, state: State ) => void; /// Triggered a subscription to a remote stream is started subscription_started: ( participantId: Participant['id'], key: Stream['key'], state: State ) => void; /// Triggered when the subscription is reconfigured using the updateSubscription method subscription_reconfigured: ( participantId: Participant['id'], key: Stream['key'], state: State ) => void; /// Triggered when subscription is removed or ended for a remote stream /// A subscription can be ended by calling `removeSubscription(ParticipantId,StreamKey)` /// This is also triggered when a remote participant leaves the room. subscription_ended: ( participantId: Participant['id'], key: Stream['key'], state: State ) => void; /// onMessageReceived /// Triggered when a new message is recieved either by one or more participants or broadcasted to all participants /// in the room. /// participantId - sender of the message /// recipients - an array of participants the message was sent to. when null the message was broadcasted to the all participants in the room. message_received: ( participantId: Participant['id'], // the participant that sent the message message: Message, recipients: Array | null, state: State ) => void; network_metrics_report: (networkMetrics: NetworkMetrics) => void; } ``` --- ### Tutorial > Source: https://developers.telnyx.com/docs/video/javascript-sdk/javascript-video-tutorial.md ## In 10 minutes we'll build A simple web app in vanilla javascript to make a video with audio from a caller to a callee. ## Get an API Key > You need a Telnx account to create an API key so you can interact with our Rooms API - If you don't have one please: Sign up for a free account - Navigate to API Keys section and create an API Key by clicking `Create API Key` button. - Copy your API key ## Create a Room Let's use our Rooms API to create a room. __Request__ *Don't forget to update `YOUR_API_KEY` here.* ```bash curl -X POST "https://api.telnyx.com/v2/rooms" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ --data-binary '{ "unique_name": "My room", "max_participants": "10", "webhook_event_url": "https://example.com", "enable_recording": "false" }' ``` __Response__ ```json { "data": { "active_session_id": null, "created_at": "2022-07-29T01:25:51.938184Z", "enable_recording": false, "id": "599e4d1e-d0aa-40ee-867a-a527f2b2dccd", "max_participants": 10, "record_type": "room", "unique_name": "Jons Room 2", "updated_at": "2022-07-29T01:25:51.940294Z", "video_codecs": ["h264", "vp8"], "webhook_event_failover_url": "", "webhook_event_url": "https://example.com", "webhook_timeout_secs": null } } ``` Great, grab the response has an `id` field, this is the id for your newly created Room. ## Generate a client token You'll need a client access token to join the room. To create one use our [REST API `rooms/create` endpoint.](/api-reference/rooms/create-a-room#create-a-room) You'll need to replace ROOM_ID in in the url of the command with your room id, above. As well as YOUR_API_KEY as you have in previous steps. __Request__ ```bash curl -X POST \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ --data '{"refresh_token_ttl_secs":3600,"token_ttl_secs":600}' \ https://api.telnyx.com/v2/rooms/ROOM_ID/actions/generate_join_client_token ``` __Response__ ```json { "data": { "recort_type": "client_token", "refresh_token": "eyJhb***************************", "refresh_token_expires_at": "2022-07-29T02:29:07Z", "token": "eyJhb**********************************", "token_expires_at": "2022-07-29T01:39:07Z" } } ``` The `token` field in the response is the client access token you'll use to join the room. ## 🏁 Checkpoint - run the app At this point you should have the following: - An API Key - A room ID - A room client access token __Let's run the app in a sandbox__ There's a README with instructions inside the sandbox. We can use this sandbox as a template to build out the rest of our application. ## Let's write some code __Basic concepts of the video SDK__ Here are some basic concepts of the SDK that will help you better understand how it works. - A `Room` represents a real time audio/video/screen share session with other people or participants. It is fundamental to building a video application. - A `Participant` represents a person inside a `Room`. Each `Room` has one `Local Participant` and one or more `Remote Participants`. - A `Stream` represents the audio/video media streams that are shared by `Participants` in a `Room` - A `Stream` is indentified by it's `participantId` and `streamKey` - A `Participant` can have one or more `Stream`'s associated with it. - A `Subscription` is used to subscribe to a `Stream` belonging to a `Remote Participant` __Room Events__ There are a few events trigger on a `Room` instance that we'll need to finish building this app that makes a video call. - `connected` - triggers when a room instance has connected to the server - `participant_joined` - triggers when a remote participant joins the room - `stream_published` - triggers when a stream has started being published to the room - `subscription_started` - triggers when subscription to remote stream has started Handling an event looks like this: ```javascript room.on("connected", async () => { ... }); ``` ## 🏁 Checkpoint > It might be helpful to take a look at the [full list of `Events` available on a `Room` instance.](/docs/video/javascript-sdk/room-events) ## Connect to the room and get local media `NOTE: ` We can start implementing our code after the `room.initialize` block inside the sandbox above. Let's connect to the room and use the standard WebRTC API to get the audio and video tracks from the device. ```javascript // connected to the room as the local participant telnyxVideoClient.on("connected", async () => { // use the webrtc api to get media from devices let intercomStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); console.log("got local stream using getUserMedia..."); // Get audio and video tracks from the MediaStream's // since the sdk works with MediaStreamTrack let intercomAudioTrack = intercomStream.getAudioTracks()[0]; let intercomVideoTrack = intercomStream.getVideoTracks()[0]; }); ``` ## Publish the stream and render it We want to a add/publish a stream the room which consists of the audio and video track we received from the user's device, above. We'll give the stream a key of "caller", which is how the stream is identified. ```javascript // add/publish a stream with a key "caller" to the room await telnyxVideoClient.addStream("intercom", { audio: callerAudioTrack, video: callerVideoTrack }); console.log("published local stream to the room..."); ``` __Render the stream to the DOM__ Now, that we have the caller's stream we want to render it to the DOM. The entire `room.connected` block looks like this: ```javascript // connected to the room as the local participant room.on("connected", async () => { // use the webrtc api to get media from devices let callerStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); console.log("got local stream using getUserMedia..."); // Get audio and video tracks from the MediaStream's // since the sdk works with MediaStreamTrack let callerAudioTrack = callerStream.getAudioTracks()[0]; let callerVideoTrack = callerStream.getVideoTracks()[0]; // add/publish the stream with the key "caller" to the room await room.addStream("caller", { audio: callerAudioTrack, video: callerVideoTrack }); console.log("published local stream to the room..."); // render the caller stream to the page/DOM let videoElement = document.getElementById("caller"); videoElement.srcObject = callerStream; }); ``` Great, we're now publishing the local partipants'/caller's stream, to the room which the callee can subscribe to. ## Subscribe to the callee's remote stream As a caller, how do we know when the callee has joined the room or has started publishing a stream? That's where the `participant_joined` and the `stream_published` [events that we went over earlier](#lets-write-some-code), come into play. We need to listen for teh `stream_published` event like this: ```javascript // a stream has been published to the room room.on( "stream_published", async (participantId, streamKey, state) => { ... } ); ``` __Understanding how subscriptions work__ __The `stream_published` event is triggered for both local and remote streams. __ In our case, we want to know when the callee's remote stream starts publishing so we can subscribe to it. We already know that the caller has published a "caller" stream so we can ignore that event. __Subscribing to a stream__ When a stream is published in a room it doesn't mean it's audio and video tracks are accessible, yet. In order to access a remote stream's track we must explicitly subscribe to that stream and wait for the `subscription_started` event. ```javascript // a stream has been published to the room room.on("stream_published", async (participantId, streamKey, state) => { // ignore streams that are published by the local participant // we only care about remote stream from other remote participant let participant = state.participants.get(participantId); if (participant.origin === "local") { return; } // the remote stream is identified using the participantId and streamKey // you need to subscribe to a remote stream in order to access it's `MediaStreamTrack`s await room.addSubscription(participantId, streamKey, { audio: true, video: true }); }); ``` ## Render the callee's remote stream We're almost there! Now, we can listen to the `subscription_started` event and use args from that handler to get the callee's remote stream and render it to the DOM. ```javascript // a subscription to a remote stream has started room.on("subscription_started", (participantId, streamKey, state) => { console.log( `subscription to the: ${participantId} ${streamKey} stream started...` ); // use a helper method to easily access a remote participants' stream let remoteStream = room.getParticipantStream(participantId, streamKey); // create a MediaStream object from the remote stream's track so we can render it // to a video element let remoteMediaStream = new MediaStream([ remoteStream.audioTrack, remoteStream.videoTrack ]); const calleeVideo = document.getElementById("callee"); calleeVideo.srcObject = remoteMediaStream; }); ``` ## Run the app To run the app: - Open the sandbox, the browser here represents the caller's app - Open the app in another tab, this represents the callee's application which you'll need to interact with. __From the caller app__ - Click the call button - You should be prompted by the browser for camera/micrphone access please allow. - You should also see video from your device's camera and hear audio from the microphone - You should see some console log, if things are working property - If you see errors your [client token has expired and you need to regenerate it.](#generate-a-client-token) __From the callee's app__ - Click the call button - Notice in the caller's app a stream subscription was logged - The callee's video/audio stream is rendered. --- ### Android SDK > Source: https://developers.telnyx.com/docs/video/android-client-sdk.md The Telnyx Video Android SDK is simple to use and makes it easy to get started with video calling. With this SDK, you can easily add video calling to your app with just a few lines of code. It provides all the functionality you need to join and interact with a Telnyx Room from an Android application. #### A link to the repo can be found here: Android SDK ## Project structure - SDK project: sdk module, containing all Telnyx SDK components as well as tests. - Demo application: app module, containing a sample demo application utilizing the sdk module. ## Adding the SDK to your Android client application Add Jitpack.io as a repository within your root level build file: ``` allprojects { repositories { ... maven { url 'https://jitpack.io' } } } ``` Add the dependency within the app level build file: ``` dependencies { implementation 'com.github.team-telnyx:telnyx-video-android:' } ``` Tag should be replaced with the release version. Then, import the TelnyxVideo SDK into your application code at the top of the class: ``` import com.telnyx.video.sdk.* ``` The '*' symbol will import the whole SDK which will then be available for use within that class. Remember to add and handle INTERNET, RECORD_AUDIO and ACCESS_NETWORK_STATE permissions in order to properly use the SDK. ``` ``` ## Before connecting to a Room ## Get an API Key You'll need an API key which is associated with your Mission Control Portal account under **API Keys**. You can learn how to do that [here](/development/api-fundamentals/create-api-keys). An API key is your credential to access our API. It allows you to: - to authenicate to the Rest API - to manage your access tokens ## Create a Room to join (if it doesn't exist) In order to join a room you must create it, if it doesn't already exist. See our [Room Rest API](/api-reference/rooms/view-a-list-of-rooms) to create one. There's also additional resources on other endpoints available to perform basic operations on a `Room`. ## Generate an a client token to join a room In order to join a room you must have a client token for that `Room`. The `client token` is short lived and you will be able to refresh it using the `refresh token` provided with it when you request a `client token`. Please see the [docs here](/api-reference/rooms-client-tokens/create-client-token-to-join-a-room#create-client-token-to-join-a-room) to learn how to create a `client token`. Now you are ready to connect to a video room that you previously created using the REST API. ## Connect to Room To connect, you'll need to provide a **participantName** that will identify your user in that room. You'll also need to provide an instance of *ExternalData* that will contain a *username* of type String and an Integer *id*. You will also provide your Android application's context in **context** ``` room = Room( context = context, roomId = UUID.fromString(roomId), roomToken = tokenInfo.token, externalData = ExternalData(id = 1234, username = "Android Participant") enableMessages = false ) ... ... room.connect() ``` ## Publish video/audio stream To publish as video or audio stream, we will need an instance of *PublishConfigHelper* with the *application context*, camera *direction*, *streamKey* (unique for each stream published), and *streamId* (unique for each stream published) ```java //AUDIO publishConfigHelper = PublishConfigHelper( context = requireContext(), direction = CameraDirection.FRONT, streamKey = SELF_STREAM_KEY // a key to identify this stream i.e: "self" streamId = SELF_STREAM_ID // RANDOM id to this stream i.e: "qlkj323kj423" ) publishConfigHelper.createAudioTrack(true, // isTrackEnabled? AUDIO_TRACK_KEY // i.e: "myMic", "002" ) ... ... room.addStream(publishConfigHelper) // This stream is new. addStream() is called. ``` New streams can be created via the PublishConfigHelper class for both audio and video. They can be created together or added later independently. ```java //VIDEO //NOTE: in this case, video is published in the same stream as above publishConfigHelper.setSurfaceView(selfSurfaceRenderer) //Provide SurfaceRenderer publishConfigHelper.createVideoTrack( CapturerConstraints.WIDTH.value,// i.e: 1280 CapturerConstraints.HEIGHT.value,// i.e: 720 CapturerConstraints.FPS.value, //i.e: 30 (fps) true, // isTrackEnabled? VIDEO_TRACK_KEY //i.e: i.e "cameraFeed", "001" ) ... ... room.updateStream(publishConfigHelper) // Stream already created, therefore updateStream is called. ``` Since stream is already created, it is only necessary to add the video track to PublishConfigHelper, and "update" the stream. ## Remove video/audio track To remove a video or audio track, publishConfigHelper has to be modified in order to remove the unwanted track ```java //Considering publishConfigHelper is the same instance as above publishConfigHelper?.let { it.stopCapture() //In case of video, we "stop the capture", update the stream, and release the surface roomsViewModel.updateStream(it) selfSurface?.let { surface -> it.releaseSurfaceView(surface) } } ... ```java publishConfigHelper?.let { it.disposeAudio() //In case of audio, we "dispose" audio and update the stream. roomsViewModel.updateStream(it) } ``` ## Remove stream By removing the stream, we will remove all tracks added to it. ```java room.removeStream(SELF_STREAM_KEY) // a key to identify this stream i.e: "self" ``` ## Video/Audio Observables The Telnyx Video SDK for android works with mutable live data, and all the information you need to build your UI is provided through **observables** that will contain the most up to date information of the state of the room, such as **current participant list**, **talking events**, **stream information**, **participants added**, **participants leaving**, etc ## State Observable ```java room.getStateObservable() // MutableLiveData ``` This observable will provide the current state of a room at any given moment. We will receive a State object that will contain: ```java data class State( val action: String, val status: String, val participants: List, val streams: HashMap, val publishers: List, val subscriptions: List ) ``` **action** -> ``val action: String` is the cause of the latest change of State ```java enum class StateAction(val action: String) { INITIALIZING_ROOM("initializing room"), STATUS_CHANGED("status changed"), ADD_PARTICIPANT("add participant"), REMOVE_PARTICIPANT("remove participant"), ADD_STREAM("add stream"), REMOVE_STREAM("remove stream"), ADD_SUBSCRIPTION("add subscription"), UPDATE_SUBSCRIPTION("update subscription"), REMOVE_SUBSCRIPTION("remove subscription"), PUBLISH("publish"), UNPUBLISH("unpublish"), UPDATE_PUBLISHED_STREAM("update published stream"), AUDIO_ACTIVITY("audio activity") } ``` **status** -> `val status: String` is the status of the Room session, when that action happened ```java enum class Status(val status: String) { INITIALIZED("initialized"), CONNECTING("connecting"), CONNECTED("connected"), DISCONNECTING("disconnecting"), DISCONNECTED("disconnected") } ``` **participants** -> `val participants: List` is the list of participants present in a room. A Participant is a UI representation in variables, for an attendee to the room session. ```java data class Participant( var id: Long, val participantId: String, var externalUsername: String? = null, val isSelf: Boolean, var streams: MutableList = mutableListOf(), var isTalking: String?, var isAudioCensored: Boolean? = false, var audioBridgeId: Long? = null, var canReceiveMessages: Boolean = false ) : Serializable data class ParticipantStream( val publishingId: Long? = null, var streamKey: String? = null, var audioEnabled: StreamStatus = StreamStatus.UNKNOWN, var videoEnabled: StreamStatus = StreamStatus.UNKNOWN, var audioTrack: AudioTrack? = null, var videoTrack: VideoTrack? = null ) enum class StreamStatus(val request: String) { UNKNOWN("unknown"), ENABLED("enabled"), DISABLED("disabled") } ``` **streams** -> `val streams: HashMap` this hash map, contains a track of the currently available streams and the **id** of the **publisher** streaming them. A stream, will contain tracks for video, audio or both. ```java data class Stream( val id: String, val key: String, val participantId: String, val origin: String, var isAudioEnabled: Boolean? = null, var isVideoEnabled: Boolean? = null, var isAudioCensored: Boolean? = null, var isVideoCensored: Boolean? = null ) ``` **publishers** -> `val publishers: List` This will track all publishers in the room. A Publisher is an instance of an attendee or participant sharing some stream content in the room. A single Participant, sharing multiple streams can be also linked to multiple Publisher ids. ```java data class Publisher( val audio_codec: String?, val video_codec: String?, val display: String, // see DisplayParameters.kt val id: Long, val talking: Boolean, val audio_moderated: Boolean? // aka Censored ) data class DisplayParameters( val participantId: String, val telephonyEngineParticipant: Boolean? = null, val external: String? = null, val stream: StreamData? = null, val canReceiveMessages: Boolean? = null ) ``` **subscriptions** -> `val subscriptions: List` Each time a publisher starts streaming information, we will have the option of subscribe/unsubscribe to/from it. This list will track all the subscriptions. ```java data class Subscription( var publisherId: Long, var status: SubscriptionStatus ) enum class SubscriptionStatus(val status: String) { NEVER_REQUESTED("never_requested"), PENDING("pending"), STARTED("started"), PAUSED("paused") } ``` ## Event observables Some observables will have a MutableLiveData of Event. Event is a wrapper of LiveData, and provide the means to ensure we only handle an observable once, no matter how many times we're set to observe it. If the contents have already been handled, we won't get that content again, unless we *peekContent()* instead. ```java /** * Used as a wrapper for data that is exposed via a LiveData that represents an event. */ open class Event(private val content: T) { var hasBeenHandled = false private set // Allow external read but not write /** * Returns the content and prevents its use again. */ fun getContentIfNotHandled(): T? { return if (hasBeenHandled) { null } else { hasBeenHandled = true content } } /** * Returns the content, even if it's already been handled. */ fun peekContent(): T = content } ``` ## Participants Observable ```java room.getParticipantsObservable() // MutableLiveData> ``` This mutable list will be received as soon as we join a Room session, and contains a list of the participants already present in the room, including yourself. The SDK will keep this list updated but won't post the changes, so this observer won't be fired again. See [Participant](#participant-header) By receiving this list we can initialize a recycler adapter in order to show participants: ```java roomsViewModel.getParticipants().observe(viewLifecycleOwner) { participants -> participants.let { participantList -> participantAdapter.setData(participantList) if (participantList.size > 0){ selfParticipantId = participantList[0].participantId selfParticipantHandleId = participantList[0].id } } } ``` ## Joined Room Observable ```java room.getJoinedRoomObservable() // MutableLiveData> ``` This mutable will fire an event as soon as we have connected to a room, and we have retrieved an *initial* list of [Participants](#participant-header) It is useful when we want to update UI as soon as we have joined the room sucessfully. This will be different to Status-CONNECTED that will be issued when we have successfully joined our plugins to handle session audio. See [Status](#status-header). ## Joined Participant Observable ```java room.getJoinedParticipant() //MutableLiveData> ``` We receive the participant that has joined the room after client has already joined. We can add the this reference to the list used in our adapter as: ```java roomsViewModel.getJoinedParticipant().observe(viewLifecycleOwner) { participantJoined -> participantJoined?.let { joinedParticipantEvent -> joinedParticipantEvent.getContentIfNotHandled()?.let { participantsAdapter.addParticipant(it) } } } ``` ## Leaving participant id Observable ```java room.getLeavingParticipantId() //MutableLiveData> ``` We receive the publisherId that has leaved the room, and a reason for its exit ("Left" or "Kicked"). ```java roomsViewModel.getLeavingParticipantId() .observe(viewLifecycleOwner) { participantLeavingId -> participantLeavingId?.let { (id, reason) -> participantAdapter.removeParticipant(id) if (id == selfParticipantHandleId && reason == "kicked") { //It's ourselves, remove from the room. goBack(wasKicked = true) Toast.makeText(requireContext(), "You were kicked!", Toast.LENGTH_LONG).show() } } } } ``` ## Connected to room Observable ```java room.getConnectionStatus() //LiveData ``` Receive true when we have opened a webSocket and connected to the room ```java roomsViewModel.connectedToRoomObservable().observe(this.viewLifecycleOwner) { it?.let { isConnected -> if (isConnected) { buttonCreateRoom.isEnabled = true } } } ``` ## Participant stream changed Observable ```java room.getParticipantStreamChanged() //MutableLiveData> ``` We will receive here an event with the participant that has recently changed its video and/or audio stream status. Mutable list of participants will also be updated with this change, but here the specific individual is received. ## Stream status ```java enum class StreamStatus(val request: String) { UNKNOWN("unknown"), ENABLED("enabled"), DISABLED("disabled") } ``` These status apply for audio, video and shared screen: **_UNKNOWN_** : initial status. We don't have information on whether this participant is sharing audio/video/screen **_ENABLED_** : the participant is sharing audio/video/screen and we can subscribe to a stream for it like: **_DISABLED_** : the participant is not sharing audio/video/screen. If we were subscribed to that participant audio/video/screen we can unsubscribe from it. ## Subscribe to a video stream In order to subscribe to a **video stream**, there are 3 actions that needs to be performed: ```java StreamStatus.ENABLED -> { // This notifies the WebRTC connection we're ready to receive stream information participantTileListener.subscribeTileToStream(model.participantId, "self") itemView.participant_tile_surface.visibility = View.VISIBLE itemView.participant_tile_place_holder.visibility = View.GONE // This ensures surfaces are initialized in an EglContext provided inside WebRTC connection participantTileListener.notifyTileSurfaceId( itemView.participant_tile_surface, model.participantId, "self" ) model.streams.find { it.streamKey == "self" }?.videoTrack?.let { if (viewHolderMap[holder] != it) { // NOTE: keep a map of surfaces to release // Updates only if previous register differs from what we need viewHolderMap[holder]?.removeSink(holder.itemView.participant_tile_surface) holder.itemView.participant_tile_surface.release() viewHolderMap[holder] = it it.addSink(itemView.participant_tile_surface) it.setEnabled(true) } } } ``` 1 Provide the SDK with the same instance of *SurfaceViewRenderer* you want to *init*: ```java room.setParticipantSurface( participantId: String, surface: SurfaceViewRenderer, streamKey: String //Stream key to indentify the webrtc connection to init this surface ) ``` This will use the proper **WebRTC Connection** to provide an *EglContext* for the surface to be *init* 2 Use method *addSink()* to add a *SurfaceViewRenderer* instance to the videoTrack provided for a [Participant](#participant-header), and set that track to enable *videoTrack?.setEnabled(true)* 3 Subscribe to the stream a participant is providing ```java participantTileListener.subscribeTileToStream(model.participantId, "self") // Eventually calls: room.addSubscription( participantId: String, streamKey: String, streamConfig: StreamConfig, ) ``` *participantId* is the participant id that uniquely identifies a single participant in the room *streamKey* i.e: "SharingSubscriptions" "CameraSubscriptions" *streamConfig* whether we want to subscribe to audio/video or both. By default we will attempt both. ## Remove subscription to a *video stream To remove a subscription we need to issue ```java room.removeSubscription(participantId: String, streamKey: String) ``` A good practice when handling surfaces is to make sure you remove this surface properly from the rendering context before eliminating or removing the surface from the UI: ``` // Here, order is important videoTrack.removeSink(surfaceViewRenderer) surfaceViewRenderer.release() ``` ## Participant Talking Observable ```java room.getParticipantTalking() MutableLiveData> ``` We will receive the participant that has updated its talking status, and the stream key. This information is also modified in the [Participants list](#participants-observable) ```java data class Participant( ... var isTalking: String?, // Can either be "talking" or "stopped-talking" ... ) : Serializable ``` ## Stats We provide the method *getWebRTCStatsForStream()* to retrieve WebRTC stats This request brings stats one time only, so if you want to keep receiving stats, you will need to use some sort of runnable or coroutine to recursively request for them such as: ```java mStatsjob = CoroutineScope(Dispatchers.Default).launch { while (isActive) { room.getWebRTCStatsForStream(participantId, streamKey, callback) delay(2000) } ``` We have to provide *participantId*, *streamKey* that is the key that identifies the stream we need the stats from, and *callback* that is an **RTCStatsCollectorCallback** we provide to the WebRTC peer connection in order to retrieve the stats. In Kotlin, method call will look like this ```java room.getWebRTCStatsForStream(participantId, streamKey) { stats -> ... ... } ``` We can later parse the information retrieved to obtain the specific information we go after. In our sample app, you will see the models we use for audio and video, both local and remote ## Remote Video stats WEBRTC's *RTCStatsReport* can be parsed and mapped to RemoteVideoStreamStats ```java data class RemoteVideoStreamStats( val bytesReceived: Int, val frameHeight: Int, val frameWidth: Int, val framesDecoded: Int, val framesDropped: Int, val framesPerSecond: Double, val framesReceived: Int, val packetsLost: Int, val packetsReceived: Int, val totalInterFrameDelay: Double ) : StreamStats() ``` ```java stats.statsMap.values.filter { it.type == "inbound-rtp" } .findLast { it.toString().contains("mediaType: \"video\"") } ?.let { rtcVideoStats -> val videoStreamStats = gson.fromJson( rtcVideoStats.toString(), RemoteVideoStreamStats::class.java ) videoStreamStats?.let { Timber.tag("RoomFragment") .d("ParticipantID: $participantId video STATS: $it") // Proceed to use stats } } ``` ## Local Video stats WEBRTC's *RTCStatsReport* can be parsed and mapped to LocalVideoStreamStats ```java data class LocalVideoStreamStats( val bytesSent: Int, val codecId: String, val frameHeight: Int, val frameWidth: Int, val framesEncoded: Int, val framesPerSecond: Double, val framesSent: Int, val headerBytesSent: Int, val nackCount: Int, val packetsSent: Int, ) : StreamStats() ``` ```java stats.statsMap.values.filter { it.type == "outbound-rtp" } .findLast { it.toString().contains("mediaType: \"video\"") } ?.let { rtcVideoStats -> val videoStreamStats = gson.fromJson( rtcVideoStats.toString(), LocalVideoStreamStats::class.java ) videoStreamStats?.let { Timber.tag("RoomFragment") .d("SelfParticipant video STATS: $it") // Proceed to use stats } } ``` ## Remote audio stats WEBRTC's *RTCStatsReport* can be parsed and mapped to RemoteAudioStreamStats ```java data class RemoteAudioStreamStats( val audioLevel: Double, val bytesReceived: Int, val codecId: String, val headerBytesReceived: Int, val jitter: Double, val packetsLost: Int, val packetsReceived: Int, val totalAudioEnergy: Double, val totalSamplesDuration: Double, val totalSamplesReceived: Int, ) : StreamStats() ``` ```java stats.statsMap.values.filter { it.type == "inbound-rtp" } .findLast { it.toString().contains("mediaType: \"audio\"") } ?.let { rtcStats -> val audioStreamStats = gson.fromJson( rtcStats.toString(), RemoteAudioStreamStats::class.java ) audioStreamStats?.let { // Proceed to use stats } } ``` ## Local audio stats WEBRTC's *RTCStatsReport* can be parsed and mapped to LocalAudioStreamStats ```java data class LocalAudioStreamStats( val bytesSent: Int, val codecId: String, val headerBytesSent: Int, val packetsSent: Int, val retransmittedBytesSent: Int, val retransmittedPacketsSent: Int, ) : StreamStats() ``` ```java stats.statsMap.values.filter { it.type == "outbound-rtp" } .findLast { it.toString().contains("mediaType: \"audio\"") } ?.let { rtcStats -> val audioStreamStats = gson.fromJson( rtcStats.toString(), LocalAudioStreamStats::class.java ) audioStreamStats?.let { // Proceed to use stats } } ``` --- ### iOS SDK > Source: https://developers.telnyx.com/docs/video/ios-client-sdk.md The Telnyx Video iOS SDK provides the functionality you need to join and interact with a video room from an iOS application. #### A link to the repo can be found here: iOS SDK ## If you prefer to jump right in Have a look at our demo app: Telnyx Meet. ## An overview of the Video API These are important concepts to understand. - A `Room` represents a real time audio/video/screen share session with other people or participants. It is fundamental to building a video application. - A `Participant` represents a person inside a `Room`. Each `Room` has one `Local Participant` and one or more `Remote Participants`. - `Room State` tracks the state of the room as it changes making it extremely easy to understand what's happened to a `Room`. - `Room State` could change due to a `Local Participant` has started publishing a stream or because a `Remote Participant`left. - A `Stream` represents the audio/video media streams that are shared by `Participants` in a `Room` - A `Stream` is indentified by it's `participantId` and `streamKey` - A `Participant` can have one or more `Stream`'s associated with it. - A `Subscription` is used to subscribe to a `Stream` belonging to a `Remote Participant` ## API of a Room This should give you high level overview of the Room API and it's functionality. ``` func connect(statusChanged: @escaping (_ status: RoomStatus) -> Void) func disconnect(completion: @escaping () -> Void) func updateClientToken(clientToken: String, completion: () -> Void) func addStream(key: StreamKey, audio: RTCAudioTrack?, video: RTCVideoTrack?, completion: @escaping OnSuccess, onFailed: @escaping OnFailed) func updateStream(key: StreamKey, audio: RTCAudioTrack?, video: RTCVideoTrack?, completion: @escaping OnSuccess, onFailed: @escaping OnFailed) func removeStream(key: StreamKey, completion: @escaping OnSuccess, onFailed: @escaping OnFailed) func addSubscription(participantId: ParticipantId, key: StreamKey, audio: Bool, video: Bool, completion: @escaping OnSuccess, onFailed: @escaping OnFailed) func pauseSubscription(participantId: ParticipantId, key: StreamKey, completion: @escaping OnSuccess, onFailed: @escaping OnFailed) func resumeSubscription(participantId: ParticipantId, key: StreamKey, completion: @escaping OnSuccess, onFailed: @escaping OnFailed) func updateSubscription(participantId: ParticipantId, key: StreamKey, audio: Bool, video: Bool, completion: @escaping OnSuccess, onFailed: @escaping OnFailed) func removeSubscription(participantId: ParticipantId, key: StreamKey, completion: @escaping OnSuccess, onFailed: @escaping OnFailed) func getWebRTCStatsForStream(participantId: ParticipantId, streamKey: StreamKey, completion: @escaping (_ stats: [String: [String: Any]]) -> Void) /// Helpers methods func getState() -> State func getLocalParticipant() throws -> Participant func getLocalStreams() throws -> [StreamKey: Stream] func getParticipantStream(participantId: ParticipantId, key: StreamKey) -> Stream? func getParticipantStreams(participantId: ParticipantId) throws -> [StreamKey: Stream] ``` After pasting the above content, Kindly check and remove any new line added ## Events that are triggered in a Room Here's a list of events that will fire as you make API calls. ``` /// Triggered each time the state is updated. var onStateChanged: ((_ state: State) -> Void)? /// Triggered when connected to a room. var onConnected: (() -> Void)? /// Triggered when disconnects from room / leaves room. var onDisconnected: (() -> Void)? /// Triggered when a remote participant joins the room. var onParticipantJoined: ((_ participantId: ParticipantId, _ participant: Participant) -> Void)? /// Triggered when a remote participant leaves the room. var onParticipantLeft: ((_ participantId: ParticipantId) -> Void)? /// Triggered after successfully registering a stream. var onStreamPublished: ((_ participantId: ParticipantId, _ streamKey: StreamKey) -> Void)? /// Triggered after successfully unregistering a stream. var onStreamUnpublished: ((_ participantId: ParticipantId, _ streamKey: StreamKey) -> Void)? /// Triggered when a local stream or a remote stream track has been enabled. /// Notifies consumers about remote stream tracks being enabled. For example: when audio is unmuted or video has started on a remote stream. var onTrackEnabled: ((_ participantId: ParticipantId, _ streamKey: StreamKey, _ kind: String) -> Void)? /// The oposite of` onTrackEnabled`. Triggers when a local stream or a remote stream track has been disabled. /// Notifies consumers about remote stream tracks being disabled. For example: when audio is muted or video has stopped on a remote stream. var onTrackDisabled: ((_ participantId: ParticipantId, _ streamKey: StreamKey, _ kind: String) -> Void)? /// Triggered when subscribed to a remote participant's stream. var onSubscriptionStarted: ((_ participantId: ParticipantId, _ streamKey: StreamKey) -> Void)? /// Triggered when an ongoing subscription is paused. var onSubscriptionPaused: ((_ participantId: ParticipantId, _ streamKey: StreamKey) -> Void)? /// Triggered when a paused subsription is resumed. var onSubscriptionResumed: ((_ participantId: ParticipantId, _ streamKey: StreamKey) -> Void)? /// Triggered when the subscription is reconfigured. var onSubscriptionReconfigured: ((_ participantId: ParticipantId, _ streamKey: StreamKey) -> Void)? /// Triggered when subscription is ended for a remote participant's stream. /// The subscription can be ended by calling `removeSubscription(ParticipantId,StreamKey)` or when the remote participant leaves. var onSubscriptionEnded: ((_ participantId: ParticipantId, _ streamKey: StreamKey) -> Void)? /// onError /// Triggered when there's an error processing incoming events from the server. var onError: ((_ error: SdkError) -> Void)? ``` After pasting the above content, Kindly check and remove any new line added ## Understanding the state of the Room Everything in the SDK centers around the `Room` object. When the state of the `Room` changes (e.g. a new participant joins or a remote participant starts publishing a stream) the `onStateChanged` event is triggered. The event is invoked with a `state` parameter which contains the current of the state of the `Room`. ``` /// Triggered each time the state is updated. var onStateChanged: ((_ state: State) -> Void)? ``` After pasting the above content, Kindly check and remove any new line added ## Before getting started ## Install the SDK Currently, the Telnyx iOS Video SDK can be installed using CocoaPods. For instructions on that check out our releases repo for iOS ## Get an API Key You'll need an API key which is associated with your Mission Control Portal account under **API Keys**. You can learn how to do that [here](/development/api-fundamentals/create-api-keys). An API key is your credential to access our API. It allows you to: - to authenicate to the Rest API - to manage your access tokens ## Create a Room to join (if it doesn't exist) In order to join a room you must create it, if it doesn't already exist. See our [Room Rest API](/api-reference/rooms-client-tokens/create-client-token-to-join-a-room#create-client-token-to-join-a-room) to create one. There's also additional resources on other endpoints available to perform basic operations on a `Room`. ## Generate an a client token to join a room In order to join a room you must have a client token for that `Room`. The `client token` is short lived and you will be able to refresh it using the `refresh token` provided with it when you request a `client token`. Please see the [docs here](/api-reference/rooms-client-tokens/create-client-token-to-join-a-room#create-client-token-to-join-a-room) to learn how to create a `client token`. ## Code Examples Enough already let's get to the code. Here are some code examples to get your wet feet on how to start building something with the iOS video SDK. ## Participating in a Room ## Connect to a room First, you'll need to create a `Room` instance and then connect to it. Once you're connected to a room, you can start sharing audio/video streams with other participant in the rooms. **Important Note:** This simply creates an instance of a Room in code it does not use the Rooms Rest API to create a room, mentioned above in "Create a Room to join"* ``` // Create an instance of a Room Room.createRoom( id: "92f83cf907b6426197ca6ccc83f3cba3", clientToken: accessToken, context: ["userid": 12345, "username": "jane doe"]) { room in // Once a room is created we can connect to it room.connect { status in } } ``` After pasting the above content, Kindly check and remove any new line added Once the room is connected you've joined the room as it's local participant. You can see this more clearly by, after connecting, get the local participant. **Important Note:** `Room` only has one `Local Participant` but can have multiple `Remote Participant`s. ``` Room.createRoom( id: "92f83cf907b6426197ca6ccc83f3cba3", clientToken: accessToken, context: ["username": "jane doe"]) { room in room.connect { status in let localParticipant = room.getLocalParticipant() } } ``` After pasting the above content, Kindly check and remove any new line added ## What is Room context? Context is any details you want to include about the `LocalParticipant` of the `Room`. For instance, let's say you want to use `context` to identify a `Participant` with fields from an external system. You could pass a `userId` and `username` as context, like the code snippet above. These details will be available to all `RemoteParticipant`s in the Room, when they are notified about your presence in the `Room`. ## Working with local media Publishing audio and/or video from your camera or microphone works by using `MediaDevices`. `MediaDevices` is a helper class that we provide for you to make it easy to grab local media from your device. ``` let stream = MediaDevices.shared().getUserMedia(audio:true, video:true) // If you want to run your app in a simulator provide a video file name to MediaDevices and it wil be used as the source for the cameraTrack. The video needs to be added to your Main.bundle, for things to work properly. let cameraTrack = stream.videoTracks.first let microphoneTrack = stream.audioTracks.first ``` After pasting the above content, Kindly check and remove any new line added ## Setting the quality of the local video You can set the camera resolution and fps using `MediaDevices`. ``` #if !targetEnvironment(simulator) guard let camera = RTCCameraVideoCapturer.captureDevices().first(where: { $0.position == MediaDevices.shared().cameraPosition }) else { return } // Choose a suitable resolution/capture format guard let captureFormat = RTCCameraVideoCapturer.supportedFormats(for: camera).sorted { (f1, f2) -> Bool in let width1 = CMVideoFormatDescriptionGetDimensions(f1.formatDescription).width let width2 = CMVideoFormatDescriptionGetDimensions(f2.formatDescription).width return width1 < width2 }.first else { return } // Choose a suitable fps let fps = captureFormat.videoSupportedFrameRateRanges.sorted { return $0.maxFrameRate < $1.maxFrameRate }.first! // Set the resolution and fps to `Mediadevices`. MediaDevices.shared().set(format: captureFormat, fps: Int(fps.maxFrameRate)) #endif ``` After pasting the above content, Kindly check and remove any new line added **Note: If you choose highest resolution and fps, the local video stream will lag if you have poor internet / bandwith** ## Publishing a stream Once you have tracks from say, a local media device like video from a camera and/our audio from your micrphone you can use those to create a `Stream` and publish it in the `Room`. ``` room.connect { status in let cameraTrack: RTCVideoTrack let microphoneTrack: RTCAudioTrack // The onStreamPublished will trigger once the stream has started publishing in the room room.onStreamPublished = { participantId, streamKey in } room.addStream( key: "camera/mic", audio: microphoneTrack, video: cameraTrack){ } } ``` After pasting the above content, Kindly check and remove any new line added ## Unpublishing a stream If you can longer want to continue publishing a stream you can `unpublish` it. Naturally the stream you want to unpublish must be added already. ``` room.connect { status in // onStreamUnpublished will trigger once the stream has been unpublished room.onStreamUnpublished = { participantId, streamKey in } room.removeStream(key: "camera/mic") { } } ``` After pasting the above content, Kindly check and remove any new line added ## Working with Remote Participants and Streams ## A remote participant who joins or leaves the room When a remote participant joins a a room you will be notified with the `Room.onParticipantJoined` event. And similiarly with `Room.onParticipantLeaving` when a remote participant leaves. You can use these events to keep track of participants in the room. ``` room.connect { room.onParticipantJoined = { participantId in // This event will trigger when a remote participant joins the room } } ``` After pasting the above content, Kindly check and remove any new line added ## Remote participants already in the room When you connect to a `Room` there may already be remote participants in the `Room`. To understand who is in the `Room` after you connect use the `onParticipantJoined` event. ``` room.connect { room.onParticipantJoined = { participantId in // The event triggers for remote participants who are already in the room, just like it does for a new remote participant that joins the room. } } ``` After pasting the above content, Kindly check and remove any new line added ## Display a remote participant's media In order to understand how to display a remote participants' media let's review on subscriptions work in the API. It might be helpful to review the Video API overview to get a better understanding of how a `Room` is modeled, especially `Stream` and `Subscription`. **Major 🔑 about Subscriptions** In order to display media from a remote stream you need to subscribe to it. It's important to understand that your `Room` doesn't automatically subscribe to a remote stream being published. It's your **choice to decide whether to subscribe to a given stream.** ## Subscribing to a stream So let's say the app your building has two users - let's them call them Alice and Bob. First, Alice joins the `Room` and starts publishing a stream with audio from her microhone and video from her camera like this: *NOTE: The app on Alice's device runs the following code...* ``` room.connect{ status in // Let's assume that we have the tracks for Alice's camera and microphone already // Alice starts publishing a stream in the room room.addStream( key: "self", audio: microphoneTrack, video: cameraTrack){ } } ``` After pasting the above content, Kindly check and remove any new line added Bob wants to get Alice's stream so he can display it. In order to do so he needs to subscribe to Alice's stream. *NOTE: The app on Bob's device runs the following code...* ``` room.connect { status in // onStreamPublished event is triggered notifying him that Alice's stream is being published room.onStreamPublished = { participantId, streamKey in // Bob subscribes to Alice's stream room.addSubscription( participantId: participantId, key: streamKey, audio: true, video: true ) } // onSubscriptionStarted triggers when the subscription to Alice's stream has started room.onSubscritionStarted = { participantId, streamKey in // Bob needs to fetch the stream so he can display it let aliceStream = room.getParticipantStream(participantId: participantId, key: streamKey) // Alice's stream has a key of 'self' which has the audio track from her microphone and a video from her device's camera. Bob can use these tracks and display them as Alice in his app. let aliceCameraTrack = aliceStream.videoTrack let aliceMicrophoneTrack = aliceStream.audioTrack } } ``` After pasting the above content, Kindly check and remove any new line added ## Handling remote streams that are already publishing in the Room After you connect to a Room there may be remote participants already in the `Room` who are publishing streams in the Room. To deal with that use the `onStreamPublished` event: ``` room.connect { status in // After connecting to a room the onStreamPublished event will trigger for remote stream // that are already being published in the room // The onStreamPublished event will trigger room.onStreamPublished = { participantId, streamKey in } } ``` After pasting the above content, Kindly check and remove any new line added ## Disconnecting from a Room To disconect from a room do: ``` room.disconnect { // after the room disconnect that it's status is .disconnected } ``` After pasting the above content, Kindly check and remove any new line added When you disconnect from a `Room` all `Remote Participant`'s will be notified that you've left the `Room` because the `Room.onParticipantLeft` event will fire on their `Room` instance. --- ## Fax ### Getting Started > Source: https://developers.telnyx.com/docs/programmable-fax/get-started.md ## Introduction Welcome to Telnyx Programmable Fax, a powerful tool that allows you to send and receive faxes seamlessly through HTTP endpoints. This guide will introduce you to the basics of setting up and using the Programmable Fax API. ## Prerequisites Before you begin, you'll need to: * Set up a Telnyx account. [Sign up](https://telnyx.com/sign-up) here if you haven't already. * Obtain a phone number and create a Programmable Fax Application following our [Quickstart guide](/docs/programmable-fax/quickstart). * Generate a Telnyx V2 API Authentication key via the [Telnyx Mission Control Portal](https://portal.telnyx.com/#/app/api-keys). ## Core Concepts * Programmable Fax Application: Configure inbound traffic and authentication for your phone numbers. * Outbound Voice Profile: Manage outbound traffic, billing, and allowed destinations. * Webhooks: HTTP callbacks that notify your server about fax events. ## Hello World Example Send Your First Fax Using cURL: 1. Authenticate Your Request: Include your API key in the header for authentication. 2. Specify Fax Details: Provide the media URL, connection ID (use your application id), and the 'to' and 'from' fax numbers in E.164 format. 3. Send the Fax: ```bash curl -X POST https://api.telnyx.com/v2/faxes \ --data-raw '{ "connection_id": "234423", "media_url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", "to": "+13127367276", "from": "+13125790015" }' \ --header 'Authorization: Bearer YourAPIKey' ``` Replace placeholders with your actual data. 1. Verify the Response: Check for an HTTP 202 response indicating successful fax initiation ## Next Steps * Explore advanced features like receiving faxes and detailed webhook management. * Check out our detailed tutorials for comprehensive guidance. ## Additional Resources * Visit our API documentation for in-depth understanding. * Join our community forums for support and discussion ## Feedback We value your feedback. If you have questions or need help troubleshooting, our support team is ready to assist. Contact us [here](https://support.telnyx.com/). ## Glossary 1. Programmable Fax Application: A configuration within the Telnyx platform used to manage inbound and outbound fax traffic. It includes settings for authentication and handling of fax transmissions. 2. Outbound Voice Profile: A setting in Telnyx that allows users to initiate outbound traffic, including faxes. It includes configurations for billing, traffic management, and permitted destinations. 3. Fax_id: A unique identifier assigned to each fax transmission. It is used to track and manage specific fax communications within the Telnyx system. 4. Media_url: The URL pointing to the document you wish to fax, typically in PDF format. This URL is included in the fax command to specify the fax content. 5. Connection_id: An identifier for your specific application or connection within Telnyx, used to route the fax correctly and associate it with your account. 6. E.164 Format: An internationally-recognized format for phone numbers, used in specifying the 'to' and 'from' fields in fax commands. 7. Failure_reason: A field in the webhook payload that provides the reason for a fax's failure, helping in troubleshooting and understanding delivery issues. 8. Ngrok: A tunneling tool used to expose a local server to the internet, often used for testing webhook endpoints in development environments. 9. cURL: A command-line tool used for sending requests to URLs, commonly used for interacting with APIs like Telnyx's Programmable Fax API. --- ### Quickstart > Source: https://developers.telnyx.com/docs/programmable-fax/quickstart.md In this guide, you'll learn how to get started with Telnyx Programmable Fax within the Telnyx Portal. Just follow these simple steps: 1. Sign up for a Telnyx Account. 2. Create a Programmable Fax Application to configure how you connect your calls. 3. Buy or port a Phone Number. 4. Assign your number to the Programmable Fax Application. 5. Create an Outbound Voice Profile to configure your outbound call settings and assign it to your Programmable Fax Application. *** ## Step 1: Sign Up for a Telnyx Mission Control Portal account Head to telnyx.com/sign-up to sign up for your free Telnyx account. It’ll give you access to our Mission Control Portal where you can buy phone numbers, set up and manage Programmable Fax Applications, and more. ## Step 2: Create a programmable fax application in the Telnyx portal * Select "Programmable Fax" in the left-hand navigation menu. * Click "Add New App". * For testing purposes, you can set the webhook URL using an endpoint you create at https://hookbin.com . ## Step 3: Buy a phone number You can search for, buy, and provision new numbers, or port existing numbers - all within the Numbers section of the Telnyx Portal . Simply click on "Numbers", then either "Search & Buy Numbers" or "Port Numbers" and follow the prompts. > You can also do this programmatically via our **RESTful API**. Check out our documentation for number [searching](/api-reference/phone-number-search/list-available-phone-numbers) and [ordering](/api-reference/phone-number-orders/create-a-number-order). ## Step 4: Assign your phone number to the programmable fax application Once your number has been purchased or ported, assign it to the Programmable Fax Application that you created in step 2. You can do this via the My Numbers section of the Portal. ![Docs Images (1)](https://images.ctfassets.net/4b49ta6b3nwj/2BJYzATnJpHLpozAjst37j/208a1637c3b0cefb7a5e40d4ede66226/Docs_Images__1_.png) ## Step 5: Create an outbound voice profile (not required for Inbound) To initiate an outbound fax, you must create an Outbound Voice Profile and assign your Programmable Fax Application to it. Select "Outbound Voice Profiles" on the left-hand navigation menu, click "+Add New Profile" and set up your profile name. Add the Programmable Fax Application you just created above, the traffic type, the service plan, and your desired billing method. > You can also do this programmatically via our **RESTful API**. Check out our documentation for [Outbound Voice Profiles](/docs/voice/outbound-voice-profiles). That’s it. You're all set to start integrating Telnyx with your applications. ## Where to next? Head over to our tutorials to learn how to [send a fax](/docs/programmable-fax/send-a-fax-api) and [receive a fax](/docs/programmable-fax/receive-a-fax-api) via API. After you [setup your development environment](/development) you can also dive deeper into [sending commands](/docs/programmable-fax/sending-commands) and [receiving webhooks](/docs/programmable-fax/receiving-webhooks). --- ### Receiving Webhooks > Source: https://developers.telnyx.com/docs/programmable-fax/receiving-webhooks.md When you send a Programmable Fax command and receive a successful response (i.e. 200 OK), you can expect to receive a webhook. The webhook will be delivered to the primary URL specified on the Application associated with the call. If that URL does not resolve, or your application returns a non 200 OK response, the webhook will be delivered to the failover URL, if one has been specified. In order to minimize webhook delivery time, Telnyx: - does not enforce the order in which webhooks are delivered - retries webhook delivery if your application does not respond within a certain time threshold. As a result, you may encounter: - out-of-order webhooks - simultaneous (or near simultaneous) webhooks - duplicate webhooks Duplicate webhooks may cause your application to issue duplicate commands. You can instruct Telnyx to ignore duplicate commands by sending a command_id parameter as part of your commands. Commands with duplicate `command_ids` within 60 seconds will be ignored. ## Example: Receiving a webhook from an outbound fax When you place an outbound fax, you will receive a number of webhooks indicating the current status of the fax. The first webhook you will receive will have A queued status indicating that Telnyx successfully received the request to send the fax. ```json { "data": { "event_type": "fax.queued", "id": "3691d047-d22a-424d-80ed-fe871981aa6d", "occurred_at": "2020-04-22T19:32:12.538002Z", "record_type": "event", "payload": { "call_duration_secs": 50, "connection_id": "7267xxxxxxxxxxxxxx", "direction": "outbound", "fax_id": "b679398e-8b4c-46bd-8630-6797f1ab5228", "from": "+35319605860", "original_media_url": "https://www.telnyx.com/telnyx-fax/1.pdf", "partial_content": true, "status": "queued", "to": "+13129457420", "user_id": "a5b1dfa3-fd2e-4e02-8ea4-xxxxxxxxxxxx" } }, "meta": { "attempt": 1, "delivered_to": "http://example.com/webhooks" } } ``` FIELD VALUE record_type Description of the record. id unique id for the webhook event_type The type of event occurred_at ISO-8601 datetime of when event occured to Destination number or SIP URI of the call from Number or SIP URI placing the call fax_id Unique ID for the Programmable Fax client_state Configurable state to track commands status Can be one of queued, media.processed, sending.started, delivered, failed ## Example: Receiving a webhook on successful fax delivery ```bash { "event_type": "fax.delivered", "id": "3320554f-6b74-4138-a74b-a1e2ec7eaf8b", "occurred_at": "2022-01-07T10:01:43.677850Z", "payload": { "call_duration_secs": 79, "connection_id": "1232154810234", "direction": "outbound", "fax_id": "c62be5bc-9b13-4b6c-abda-34dd8b541287", "from": "+19459457421", "original_media_url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", "page_count": 1, "status": "delivered", "to": "+13129457420", "user_id": "bdaa1f9f-1018-4156-867d-6c4ac9f556eb" } } ``` ## Example: Receiving a webhook on failed fax delivery ```bash { "event_type": "fax.failed", "id": "d906ecda-db21-428e-9ca0-74dae7e7c144", "occurred_at": "2022-01-05T22:23:46.888808Z", "payload": { "connection_id": "1232154810234", "direction": "outbound", "failure_reason": "user_busy", "fax_id": "f7b303ed-674c-4962-951b-848380510893", "from": "+19459457421", "original_media_url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", "status": "failed", "to": "+13129457420", "user_id": "417b0cc2-39e0-4ab9-b116-e56543649aa9" } } ``` ## Possible failure reasons Inspect the `failure_reason` field in webhook's payload to debug failed deliveries of your faxes. The possible failure reasons are: - `account_disabled` - `connection_channel_limit_exceeded` - `destination_invalid` - `destination_not_in_countries_whitelist` - `destination_not_in_service_plan` - `destination_unreachable` - `fax_initial_communication_timeout` - `fax_signaling_error` - `invalid_ecm_response_from_receiver` - `no_outbound_profile` - `outbound_profile_channel_limit_exceeded` - `outbound_profile_daily_spend_limit_exceeded` - `receiver_call_dropped` - `receiver_communication_error` - `receiver_decline` - `receiver_incompatible_destination` - `receiver_invalid_number_format` - `receiver_no_answer` - `receiver_no_response` - `receiver_recovery_on_timer_expire` - `receiver_unallocated_number` - `service_unavailable` - `unverified_destination_not_allowed` - `unverified_origination_number` - `user_busy` - `user_channel_limit_exceeded` --- ### Sending Commands > Source: https://developers.telnyx.com/docs/programmable-fax/sending-commands.md A Programmable Fax API command is sent with a fax_id. The fax_id allows a user to communicate to Telnyx the fax the user wants to take an action on. The Telnys Programmable Fax API supports PDF files. To initiate sending the fax, we need to send the request to the Telnyx Programmable Fax API endpoint https://api.telnyx.com/v2/faxes. ## Authentication With the request we need to send additional parameters containing authentication information so Telnyx knows which account to send the fax from and information about the destination and file being sent. HEADER DESCRIPTION media_url Authorization: Bearer YOUR_API_KEY connection_id Authorization: Bearer YOUR_API_KEY to Authorization: Bearer YOUR_API_KEY from Authorization: Bearer YOUR_API_KEY Authorization: Bearer The prefix to your API V2 key created in Step 2. ## Available commands and their expected Webhooks Telnyx sends webhooks to update on the status of Programmable Fax. Webhooks will also be sent in response to requests to list and delete faxes. COMMAND ASSOCIATED WEBHOOKS Send a fax fax.queued fax.media.processed fax.sending.started fax.delivered fax.failed ## Response when sending command When you send a Programmable Fax API Command, you will immediately receive an http response. Responses include, but are not limited to: HTTP STATUS CODE MESSAGE DESCRIPTION 202 OK The request succeeded. 403 Forbidden The request was valid, however the user is not authorized to perform this action. 404 Not Found The requested resource could not be found. 422 Invalid Parameters The request has invalid parameters. ## Example: Sending commands To send a fax, send a POST request to the https://api.telnyx.com/v2/faxes endpoint as shown in the example below. ## WITH A API V2 KEY ```bash curl -X POST https://api.telnyx.com/v2/faxes \ --data-urlencode "media_url=https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" \ --data-urlencode "connection_id=1232154810234" \ --data-urlencode "to=+13129457420" \ --data-urlencode "from=+19459457421" \ --header "Authorization: Bearer APIAuthKey_fromPortal" ``` After pasting the above content, Kindly check and remove any new line added --- ### Send a fax via API > Source: https://developers.telnyx.com/docs/programmable-fax/send-a-fax-api.md The Telnyx Programmable Fax API lets you send, receive, and manage faxes through a set of easy-to-use HTTP endpoints. In this guide, you'll learn how to send a fax using the Programmable Fax API in four simple steps. ## Step 1: Setting up with Telnyx ### Setup a Telnyx account, phone number, and programmable fax application First, follow our [Quickstart](/docs/programmable-fax/quickstart) guide to create a Telnyx account, phone number, and Fax Application. ## Step 2: API authentication > If you already have a Telnyx V2 API Authentication key, skip to Step 3. 1. In the Telnyx Mission Control Portal , in the left menu bar navigate to " API Keys ". 2. Ensure, API V2 is selected in the horizontal menu bar. 3. Click "Create API key" 4. Copy the API key and save it somewhere safe. ## Step 3: Send a PDF with the programmable fax API using curl The Telnyx Programmable Fax API supports PDF files. To initiate sending a fax, we need to send the request to the Telnyx Programmable Fax API endpoint `https://api.telnyx.com/v2/faxes`. We need to send additional parameters containing authentication information with the request, so Telnyx knows which account to send the fax from and information about the destination and file being sent. Header Description media_url The URL of the PDF used for the fax's media. connection_id The app ID or connection ID to send the fax with. to The fax enabled phone number (in E.164 format), or SIP URI, the fax will be sent to. from The phone number, in E.164 format, the fax will be sent from. Authorizaton: Bearer The prefix to your API V2 key created in Step 2. Now that we know what we need to include in our request, we can use a number of different methods to structure and send it. In this example, we are going to use curl straight from the command line. You can also use a client such as Postman to structure your request. ```bash curl -X POST https://api.telnyx.com/v2/faxes \ --data-urlencode "media_url=https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" \ --data-urlencode "connection_id=1232154810234" \ --data-urlencode "to=+13129457420" \ --data-urlencode "from=+19459457421" \ --header "Authorization: Bearer APIAuthKey_fromPortal" ``` After pasting the above content, Kindly check and remove any new line added Simply copy the above code into your favorite text editor, paste in your parameters from the table in place of the existing data, and execute the command. ## Step 4: Verify HTTP response and receive webhooks If you have successfully structured your command and the fax has begun sending, the Programmable Fax API will respond with HTTP 202. If you do not receive a HTTP 202 response, double-check your request and try again. Explanations of other response codes can be found in our [API reference](/docs/programmable-fax/sending-commands#response-when-sending-command). Once the request to send the fax has been successfullly received by Telnyx, you should begin receiving a series of webhooks to the URL that you specified in your Fax Application. The webhooks you should receive are: - `fax.queued` - `fax.media.processed` - `fax.sending.started` - `fax.delivered` - `fax.failed` That's it! Once you've received a `fax.delivered` webhook, your fax has been delivered to its destinaion. ## Webhook examples #### On successful delivery ```json { "event_type": "fax.delivered", "id": "3320554f-6b74-4138-a74b-a1e2ec7eaf8b", "occurred_at": "2022-01-07T10:01:43.677850Z", "payload": { "call_duration_secs": 79, "connection_id": "1232154810234", "direction": "outbound", "fax_id": "c62be5bc-9b13-4b6c-abda-34dd8b541287", "from": "+19459457421", "original_media_url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", "page_count": 1, "status": "delivered", "to": "+13129457420", "user_id": "bdaa1f9f-1018-4156-867d-6c4ac9f556eb" } } ``` After pasting the above content, Kindly check and remove any new line added #### On failed delivery ```json { "event_type": "fax.failed", "id": "d906ecda-db21-428e-9ca0-74dae7e7c144", "occurred_at": "2022-01-05T22:23:46.888808Z", "payload": { "connection_id": "1232154810234", "direction": "outbound", "failure_reason": "user_busy", "fax_id": "f7b303ed-674c-4962-951b-848380510893", "from": "+19459457421", "original_media_url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", "status": "failed", "to": "+13129457420", "user_id": "417b0cc2-39e0-4ab9-b116-e56543649aa9" } } ``` After pasting the above content, Kindly check and remove any new line added #### Possible failure reasons Inspect the `failure_reason` field in webhook's payload to debug failed deliveries of your faxes. The possible failure reasons are: - `account_disabled` - `connection_channel_limit_exceeded` - `destination_invalid` - `destination_not_in_countries_whitelist` - `destination_not_in_service_plan` - `destination_unreachable` - `fax_initial_communication_timeout` - `fax_signaling_error` - `invalid_ecm_response_from_receiver` - `no_outbound_profile` - `outbound_profile_channel_limit_exceeded` - `outbound_profile_daily_spend_limit_exceeded` - `receiver_call_dropped` - `receiver_communication_error` - `receiver_decline` - `receiver_incompatible_destination` - `receiver_invalid_number_format` - `receiver_no_answer` - `receiver_no_response` - `receiver_recovery_on_timer_expire` - `receiver_unallocated_number` - `service_unavailable` - `user_busy` - `user_channel_limit_exceeded` ## Where to next? Continue to our next tutorial to learn how to [receive inbound faxes](/docs/programmable-fax/receive-a-fax-api) via API. --- ### Receive a fax via API > Source: https://developers.telnyx.com/docs/programmable-fax/receive-a-fax-api.md The Telnyx Programmable Fax API lets you send, receive and manage faxes through a set of easy-to-use HTTP endpoints. In this guide, you'll learn how to handle receiving a fax using the Programmable Fax API. ## Step 1: Setup with Telnyx ### Setup a Telnyx account, phone number, and programmable fax application First, follow our [Quickstart](/docs/programmable-fax/quickstart) guide to create a Telnyx account, phone number, and Fax Application. The phone number that you select and associate with the Programmable Fax Application will receive incoming fax calls. You can also port an existing number to Telnyx and use it for Programmable Fax. ### Step 2: Receiving Webhooks In order for the webhooks in this tutorial to work, Telnyx must be able to send your web application an HTTP request over the Internet. That means your application needs to have a URL or IP address that Telnyx can reach. Telnyx sends webhooks to the URL or IP address to notify your application of incoming faxes. For the purpose of this tutorial, we're using ngrok , a popular tunneling tool used to expose a locally running application to the internet. ngrok gives you a public URL for a local port on your development machine, which you can use to configure your Telnyx webhooks as described above. Download and install ngrok, then use it at the command line to create a tunnel to whatever port your web application is running on. For example, this command will create a public URL for a web application listening on port 3000. ```bash ngrok http 3000 ``` After executing that command, you will see that ngrok has given your application a public URL that you can use in your webhook connection configuration in the Telnyx Mission Control Portal . ![Send and receive fax using Telnyx Programmable Fax API](/img/twiml-conferencing-3.png) Grab your ngrok public URL and head back to the Programmable Fax Application you configured earlier. Now in the field under "Send a webhook to the URL" enter your new ngrok URL from either of the "Forwarding" URLs ngrok provided us with depending on if you want to use HTTP or HTTPS. You're now all set up to receive webhooks for events related to inbound faxes. If you use the same Programmable Fax Application for sending faxes, you will receive events to the same ngrok URL you just created. ### Example Webhooks If you have set everything up correctly, any time an inbound fax is received you can expect to receive the following webhooks: Webhook Name Description fax.receiving.started The fax has begun transmitting to Telnyx successfully. fax.media.processing.started Telnyx has received the fax and is generating the digital PDF file. fax.received The PDF has been generated and the file is ready to be downloaded. fax.failed Transmission of the fax failed. Check the <code>failure_reason</code> for more details. ### Fax has begun transmitting to Telnyx ```json { "data": { "event_type": "fax.receiving.started", "id": "bc004786-f166-4dd3-8c5d-737990b501bc", "occurred_at": "2020-08-27T16:33:29.684247Z", "payload": { "connection_id": "1447842681660114324", "direction": "inbound", "fax_id": "9fbc3f0d-5495-42af-9a4e-c57a235d9182", "from": "+16132484872", "status": "receiving", "to": "+17733372863", "user_id": "19a75cea-02c6-4b9a-84fa-c9bc8341feb8", "caller_id": "+16132484872" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://1a3097.ngrok.io/" } } ``` ### Fax transmission is complete and Telnyx is converting to PDF ```json { "data": { "event_type": "fax.media.processing.started", "id": "35e33b02-6365-47d0-93b7-3bfec97c467e", "occurred_at": "2020-08-27T16:33:33.175396Z", "payload": { "connection_id": "1447842681660114324", "direction": "inbound", "fax_id": "f72eebbe-f9b6-4f0f-b652-03e742e110d5", "from": "+16132484850", "status": "media.processing", "page_count": 2, "to": "+17733372863", "user_id": "19a75cea-02c6-4b9a-84fa-c9bc8341feb8", "caller_id": "+16132484872" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://1a3097.ngrok.io/" } } ``` ### PDF has been generated and is ready for download The media_url field contains a signed AWS link to a PDF of the received fax. This URL is valid for 10 minutes before the file is no longer accessible so be sure to download the file if you wish to keep it! ```json { "data": { "event_type": "fax.received", "id": "4844b70c-3c6c-4c3a-ba2e-e4c785f02d24", "occurred_at": "2020-08-27T16:33:36.843054Z", "payload": { "call_duration_secs": 50, "connection_id": "1447842681660114324", "direction": "inbound", "fax_id": "f72eebbe-f9b6-4f0f-b652-03e742e110d5", "from": "+16132484850", "media_url": "https://s3.amazonaws.com/faxes-prod/19a75cea-02c6-4b9a-84fa-c9bc8341feb8/f72eebbe-f9b6-4f0f-b652-03e742e110d5.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...%2F20200827%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200827T163336Z&X-Amz-Expires=7200&X-Amz-SignedHeaders=host&X-Amz-Signature=...", "page_count": 2, "partial_content": false, "status": "received", "to": "+17733372863", "user_id": "19a75cea-02c6-4b9a-84fa-c9bc8341feb8", "caller_id": "+16132484872" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://1a3097.ngrok.io/" } } ``` ### Uh oh, something has gone wrong and the fax failed Inbound faxes can fail for a variety of reasons. Some of the most common reasons are that the sending party hung up before the fax was finished transmitting or didn't send anything at all. ```json { "data": { "event_type": "fax.failed", "id": "1a7405a6-696c-4369-a0b1-168e4bb7e22c", "occurred_at": "2020-08-27T16:17:51.844250Z", "payload": { "connection_id": "1447842681660114324", "direction": "inbound", "failure_reason": "sender_call_dropped", "fax_id": "181533f3-b0b8-4bcd-ab01-b33cd8698508", "from": "+16617480240", "status": "failed", "to": "+17733372863", "user_id": "19a75cea-02c6-4b9a-84fa-c9bc8341feb8", "caller_id": "+16132484872" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://1a3097.ngrok.io/" } } ``` ### Where to next? Go back to our previous tutorial to learn how to [send outbound faxes](/docs/programmable-fax/send-a-fax-api) via API. --- ### Email to fax > Source: https://developers.telnyx.com/docs/programmable-fax/email-to-fax.md __⏱ 60 minutes build time.__ __🧰 Clone the sample application from our GitHub repo __ In this tutorial, you'll learn how to use the Telnyx Fax API to receieve faxes to your email in Python. Our Programmable Fax API, combined with our Numbers API, provides everything you need to build a robust call tracking application: The Numbers API enables you to search the Telnyx phone number inventory in real time; filtering by Area Code, City/State, and more to find the perfect local number for your use-case. Call Control enables you to quickly setup dynamic forwarding numbers, toggle dual-channel recording, join/leave dynamic conferences, and pull post-call analytics. By following this tutorial, you'll be able to: > 1. Configure a Telnyx portal account. > 2. Set up a Telnyx Fax profile and an outbound voice profile. > 3. How to send a fax to your email using Telnyx. > 4. How to send a fax using your email and to Telnyx Fax profile. ## What you'll need to get started with email to fax 1. A Telnyx account- take a minute to sign up to our self-service portal. 2. A Telnyx phone number that's enabled with a Telnyx Fax Appliction and a Telnyx Outbound Voice profile . 3. Ability to receive webhooks (with something like [ngrok](/development/development-tools/ngrok-setup/index#ngrok "ngrok")) 4. [Python and PIP](/development/sdk/python "Python and PIP") installed 5. An AWS Account setup with proper profiles and groups with IAM for S3. Check out the Quickstart for more information. 6. Finally you'll need a Mailgun account . If you're planning on using this guide to build an email to fax application you'll need an account with the ability to setup inbound routes . ### Create a Telnyx Mission Control Portal account To get started, you'll need to create an account . Verify your email address and you can log into the Mission Control Portal to get started. ### Usage The following environmental variables need to be set: Variable Description TELNYX_API_KEY Your Telnyx API Key TELNYX_PUBLIC_KEY Telnyx Public Key TELNYX_S3_BUCKET The name of the bucket to upload the media attachments TELNYX_FAX_CONNECTION_ID The connection ID for your Fax Applications MAILGUN_API_KEY Your Mailgun API Key MAILGUN_DOMAIN Your Mailgun Domain. PORT Defaults to 8000 The port the app will be served ### .env file This app uses the excellent python-dotenv package to manage environment variables. Make a copy of .env.sample and save as .env and update the variables to match your creds. TELNYX_PUBLIC_KEY="+kWXUag92mcUMFQopVlff7ctD/m2S/IoXv+AlI1/5a0=" TELNYX_API_KEY="KEYI" TELNYX_S3_BUCKET=telnyx-mms-demo TELNYX_FAX_CONNECTION_ID=36092346987 MAILGUN_API_KEY="123-432-123" MAILGUN_DOMAIN="sandbox367c5ec1512d458e95f5e5c60f5fe97a.mailgun.org" PORT=8000 ### Callback URLs for Telnyx application Callback Type URL Fax Callbacks `{ngrok-url}/faxes` Email Callbacks `{ngrok-url}/email/inbound` ### Install Run the following commands to get started: $ git clone https://github.com/team-telnyx/demo-python-telnyx.git ### Ngrok This application is served on the port defined in the runtime environment (or in the .env file). Be sure to launch ngrok for that port: ./ngrok http 8000 ngrok by @inconshreveable (Ctrl+C to quit) Session Status online Account Little Bobby Tables (Plan: Free) Version 2.3.35 Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding http://your-url.ngrok.io -> http://localhost:8000 Forwarding https://your-url.ngrok.io -> http://localhost:8000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00 At this point you can point your application to generated ngrok URL + path: (Example: `http://{your-url}.ngrok.io/faxes`). ## Run ### High level code overview #### Receiving fax, sending email 1. Receive the webhook from Telnyx indicating fax is incoming 2. We only only care about the fax.received webhook for inbound faxes 3. Extract the to/from and other information about the fax 4. Download the attachment and save locally 5. Lookup the association between phone_number and email 6. Create and send an email via Mailgun with downloaded media as an attachment #### Sending fax 1. Receive webhook from mailgun for incoming email 2. Extract the prefix of the email (19198675309@MAILGUN_DOMAIN.com, we want 19198675309) and prepend the + 3. Lookup the association between email and phone_number to determine the from phone number. 4. Save the first attachment locally 5. Upload the attachment to S3 6. Create and send a fax to the phone number extracted above ### Update "DB" The app as provided uses a dumb-hard-coded database to minimize dependencies for the sake of examples. There is an in memory database defined at the top that associates an email to a Telnyx number. ``` DB = [ { "email": "@telnyx.com", "phone_number": "+" } ] ``` - phone_number is a __Telnyx__ phone number - email is the email to associate with that phone number #### Receiving faxes ``` {+19198675309} ==(faxes)==> {telnyx_phone_number} ==(emails)==> {email_as_defined} ``` #### Sending faxes ``` {email_as_defined} ==(emails)==> {destination_phone_number@MAILGUN_DOMAIN} ==(faxes)==> {destination_phone_number} ``` ### Launch the app and update callbacks Start the server python app.py When you are able to run the server locally, the final step involves making your application accessible from the Internet. So far, we've set up a local web server. This is typically not accessible from the public internet, making testing inbound requests to web applications difficult. The best workaround is a tunneling service. They come with client software that runs on your computer and opens an outgoing permanent connection to a publicly available server in a data center. Then, they assign a public URL (typically on a random or custom subdomain) on that server to your account. The public server acts as a proxy that accepts incoming connections to your URL, forwards (tunnels) them through the already established connection and sends them to the local web server as if they originated from the same machine. The most popular tunneling tool is ngrok. Check out the [ngrok setup](/development/development-tools/ngrok-setup/index#ngrok "ngrok setup") walkthrough to set it up on your computer and start receiving webhooks from inbound faxes to your newly created application. Once you've set up ngrok or another tunneling service you can add the public proxy URL to your Inbound Settings in the Mission Control Portal. To do this, click the edit symbol [✎] next to your Fax Profile. In the "Inbound Settings" > "Webhook URL" field, paste the forwarding address from ngrok into the Webhook URL field. Add faxes to the end of the URL to direct the request to the webhook endpoint in your server. For now you'll leave “Failover URL” blank, but if you'd like to have Telnyx resend the webhook in the case where sending to the Webhook URL fails, you can specify an alternate address in this field. Once everything is setup, you should now be able to: - Fax your phone number and receive an email - Email `{19198675309}@domain.com` an attachment to send a fax to `{19198675309}` ## Fax follow-Ons Now that you've successfully sent a fax to your email and phone number, you can explore more fax features and discover ideas to build new applications. Our developer Slack community is full of Python developers like you - be sure to join to see what your fellow developers are building! --- ## API Reference (Other APIs) ### AutoRechargePreferences - [List auto recharge preferences](https://developers.telnyx.com/api-reference/autorechargepreferences/list-auto-recharge-preferences.md): Returns the payment auto recharge preferences. - [Update auto recharge preferences](https://developers.telnyx.com/api-reference/autorechargepreferences/update-auto-recharge-preferences.md): Update payment auto recharge preferences. ### Notifications - [List notification channels](https://developers.telnyx.com/api-reference/notifications/list-notification-channels.md): List notification channels. - [Create a notification channel](https://developers.telnyx.com/api-reference/notifications/create-a-notification-channel.md): Create a notification channel. - [Get a notification channel](https://developers.telnyx.com/api-reference/notifications/get-a-notification-channel.md): Get a notification channel. - [Update a notification channel](https://developers.telnyx.com/api-reference/notifications/update-a-notification-channel.md): Update a notification channel. - [Delete a notification channel](https://developers.telnyx.com/api-reference/notifications/delete-a-notification-channel.md): Delete a notification channel. - [List all Notifications Events Conditions](https://developers.telnyx.com/api-reference/notifications/list-all-notifications-events-conditions.md): Returns a list of your notifications events conditions. - [List all Notifications Events](https://developers.telnyx.com/api-reference/notifications/list-all-notifications-events.md): Returns a list of your notifications events. - [List all Notifications Profiles](https://developers.telnyx.com/api-reference/notifications/list-all-notifications-profiles.md): Returns a list of your notifications profiles. - [Create a notification profile](https://developers.telnyx.com/api-reference/notifications/create-a-notification-profile.md): Create a notification profile. - [Get a notification profile](https://developers.telnyx.com/api-reference/notifications/get-a-notification-profile.md): Get a notification profile. - [Update a notification profile](https://developers.telnyx.com/api-reference/notifications/update-a-notification-profile.md): Update a notification profile. - [Delete a notification profile](https://developers.telnyx.com/api-reference/notifications/delete-a-notification-profile.md): Delete a notification profile. - [List notification settings](https://developers.telnyx.com/api-reference/notifications/list-notification-settings.md): List notification settings. - [Add a Notification Setting](https://developers.telnyx.com/api-reference/notifications/add-a-notification-setting.md): Add a notification setting. - [Get a notification setting](https://developers.telnyx.com/api-reference/notifications/get-a-notification-setting.md): Get a notification setting. - [Delete a notification setting](https://developers.telnyx.com/api-reference/notifications/delete-a-notification-setting.md): Delete a notification setting. ### Addresses - [List all addresses](https://developers.telnyx.com/api-reference/addresses/list-all-addresses.md): Returns a list of your addresses. - [Creates an address](https://developers.telnyx.com/api-reference/addresses/creates-an-address.md): Creates an address. - [Retrieve an address](https://developers.telnyx.com/api-reference/addresses/retrieve-an-address.md): Retrieves the details of an existing address. - [Deletes an address](https://developers.telnyx.com/api-reference/addresses/deletes-an-address.md): Deletes an existing address. - [Accepts this address suggestion as a new emergency address for Operator Connect and finishes the uploads of the numbers associated with it to Microsoft.](https://developers.telnyx.com/api-reference/addresses/accepts-this-address-suggestion-as-a-new-emergency-address-for-operator-connect-and-finishes-the-uploads-of-the-numbers-associated-with-it-to-microsoft.md) - [Validate an address](https://developers.telnyx.com/api-reference/addresses/validate-an-address.md): Validates an address for emergency services. ### Billing - [Get user balance details](https://developers.telnyx.com/api-reference/billing/get-user-balance-details.md) ### Authentication Providers - [List all SSO authentication providers](https://developers.telnyx.com/api-reference/authentication-providers/list-all-sso-authentication-providers.md): Returns a list of your SSO authentication providers. - [Creates an authentication provider](https://developers.telnyx.com/api-reference/authentication-providers/creates-an-authentication-provider.md): Creates an authentication provider. - [Retrieve an authentication provider](https://developers.telnyx.com/api-reference/authentication-providers/retrieve-an-authentication-provider.md): Retrieves the details of an existing authentication provider. - [Update an authentication provider](https://developers.telnyx.com/api-reference/authentication-providers/update-an-authentication-provider.md): Updates settings of an existing authentication provider. - [Deletes an authentication provider](https://developers.telnyx.com/api-reference/authentication-providers/deletes-an-authentication-provider.md): Deletes an existing authentication provider. ### IP Addresses - [List all Access IP Addresses](https://developers.telnyx.com/api-reference/ip-addresses/list-all-access-ip-addresses.md) - [Create new Access IP Address](https://developers.telnyx.com/api-reference/ip-addresses/create-new-access-ip-address.md) - [Retrieve an access IP address](https://developers.telnyx.com/api-reference/ip-addresses/retrieve-an-access-ip-address.md) - [Delete access IP address](https://developers.telnyx.com/api-reference/ip-addresses/delete-access-ip-address.md) ### IP Ranges - [List all Access IP Ranges](https://developers.telnyx.com/api-reference/ip-ranges/list-all-access-ip-ranges.md) - [Create new Access IP Range](https://developers.telnyx.com/api-reference/ip-ranges/create-new-access-ip-range.md) - [Delete access IP ranges](https://developers.telnyx.com/api-reference/ip-ranges/delete-access-ip-ranges.md) ### Managed Accounts - [Lists accounts managed by the current user.](https://developers.telnyx.com/api-reference/managed-accounts/lists-accounts-managed-by-the-current-user.md): Lists the accounts managed by the current user. Users need to be explictly approved by Telnyx in order to become manager accounts. - [Create a new managed account.](https://developers.telnyx.com/api-reference/managed-accounts/create-a-new-managed-account.md): Create a new managed account owned by the authenticated user. You need to be explictly approved by Telnyx in order to become a manager account. - [Retrieve a managed account](https://developers.telnyx.com/api-reference/managed-accounts/retrieve-a-managed-account.md): Retrieves the details of a single managed account. - [Update a managed account](https://developers.telnyx.com/api-reference/managed-accounts/update-a-managed-account.md): Update a single managed account. - [Disables a managed account](https://developers.telnyx.com/api-reference/managed-accounts/disables-a-managed-account.md): Disables a managed account, forbidding it to use Telnyx services, including sending or receiving phone calls and SMS messages. Ongoing phone calls will not be… - [Enables a managed account](https://developers.telnyx.com/api-reference/managed-accounts/enables-a-managed-account.md): Enables a managed account and its sub-users to use Telnyx services. - [Update the amount of allocatable global outbound channels allocated to a specific managed account.](https://developers.telnyx.com/api-reference/managed-accounts/update-the-amount-of-allocatable-global-outbound-channels-allocated-to-a-specific-managed-account.md): Update the amount of allocatable global outbound channels allocated to a specific managed account. - [Display information about allocatable global outbound channels for the current user.](https://developers.telnyx.com/api-reference/managed-accounts/display-information-about-allocatable-global-outbound-channels-for-the-current-user.md): Display information about allocatable global outbound channels for the current user. Only usable by account managers. ### Organization Users - [List organization users](https://developers.telnyx.com/api-reference/organization-users/list-organization-users.md): Returns a list of the users in your organization. - [Get organization user](https://developers.telnyx.com/api-reference/organization-users/get-organization-user.md): Returns a user in your organization. - [Delete organization user](https://developers.telnyx.com/api-reference/organization-users/delete-organization-user.md): Deletes a user in your organization. - [Get organization users groups report](https://developers.telnyx.com/api-reference/organization-users/get-organization-users-groups-report.md): Returns a report of all users in your organization with their group memberships. This endpoint returns all users without pagination and always includes group i… ### Billing Groups - [List all billing groups](https://developers.telnyx.com/api-reference/billing-groups/list-all-billing-groups.md) - [Create a billing group](https://developers.telnyx.com/api-reference/billing-groups/create-a-billing-group.md) - [Get a billing group](https://developers.telnyx.com/api-reference/billing-groups/get-a-billing-group.md) - [Update a billing group](https://developers.telnyx.com/api-reference/billing-groups/update-a-billing-group.md) - [Delete a billing group](https://developers.telnyx.com/api-reference/billing-groups/delete-a-billing-group.md) ### Detail Records - [Search detail records](https://developers.telnyx.com/api-reference/detail-records/search-detail-records.md): Search for any detail record across the Telnyx Platform ### MDR Detailed Reports - [Get all MDR detailed report requests](https://developers.telnyx.com/api-reference/mdr-detailed-reports/get-all-mdr-detailed-report-requests.md): Retrieves all MDR detailed report requests for the authenticated user - [Create a new MDR detailed report request](https://developers.telnyx.com/api-reference/mdr-detailed-reports/create-a-new-mdr-detailed-report-request.md): Creates a new MDR detailed report request with the specified filters - [Get a specific MDR detailed report request](https://developers.telnyx.com/api-reference/mdr-detailed-reports/get-a-specific-mdr-detailed-report-request.md): Retrieves a specific MDR detailed report request by ID - [Delete a MDR detailed report request](https://developers.telnyx.com/api-reference/mdr-detailed-reports/delete-a-mdr-detailed-report-request.md): Deletes a specific MDR detailed report request by ID ### CDR Reports - [Get all CDR report requests](https://developers.telnyx.com/api-reference/cdr-reports/get-all-cdr-report-requests.md): Retrieves all CDR report requests for the authenticated user - [Create a new CDR report request](https://developers.telnyx.com/api-reference/cdr-reports/create-a-new-cdr-report-request.md): Creates a new CDR report request with the specified filters - [Get a specific CDR report request](https://developers.telnyx.com/api-reference/cdr-reports/get-a-specific-cdr-report-request.md): Retrieves a specific CDR report request by ID - [Delete a CDR report request](https://developers.telnyx.com/api-reference/cdr-reports/delete-a-cdr-report-request.md): Deletes a specific CDR report request by ID - [Get available CDR report fields](https://developers.telnyx.com/api-reference/cdr-reports/get-available-cdr-report-fields.md): Retrieves all available fields that can be used in CDR reports ### MDR Usage Reports - [List MDR usage reports](https://developers.telnyx.com/api-reference/mdr-usage-reports/list-mdr-usage-reports.md): Fetch all previous requests for MDR usage reports. - [Create a new legacy usage V2 MDR report request](https://developers.telnyx.com/api-reference/mdr-usage-reports/create-a-new-legacy-usage-v2-mdr-report-request.md): Creates a new legacy usage V2 MDR report request with the specified filters - [Get an MDR usage report](https://developers.telnyx.com/api-reference/mdr-usage-reports/get-an-mdr-usage-report.md): Fetch single MDR usage report by id. - [Delete a V2 legacy usage MDR report request](https://developers.telnyx.com/api-reference/mdr-usage-reports/delete-a-v2-legacy-usage-mdr-report-request.md): Deletes a specific V2 legacy usage MDR report request by ID - [Fetch all Messaging usage reports](https://developers.telnyx.com/api-reference/mdr-usage-reports/fetch-all-messaging-usage-reports.md): Fetch all messaging usage reports. Usage reports are aggregated messaging data for specified time period and breakdown - [Create MDR Usage Report](https://developers.telnyx.com/api-reference/mdr-usage-reports/create-mdr-usage-report.md): Submit request for new new messaging usage report. This endpoint will pull and aggregate messaging data in specified time period. - [Retrieve messaging report](https://developers.telnyx.com/api-reference/mdr-usage-reports/retrieve-messaging-report.md): Fetch a single messaging usage report by id - [Delete MDR Usage Report](https://developers.telnyx.com/api-reference/mdr-usage-reports/delete-mdr-usage-report.md): Delete messaging usage report by id - [Generate and fetch MDR Usage Report](https://developers.telnyx.com/api-reference/mdr-usage-reports/generate-and-fetch-mdr-usage-report.md): Generate and fetch messaging usage report synchronously. This endpoint will both generate and fetch the messaging report over a specified time period. No polli… ### Telco Data Usage Reports - [List telco data usage reports](https://developers.telnyx.com/api-reference/telco-data-usage-reports/list-telco-data-usage-reports.md): Retrieve a paginated list of telco data usage reports - [Submit telco data usage report](https://developers.telnyx.com/api-reference/telco-data-usage-reports/submit-telco-data-usage-report.md): Submit a new telco data usage report - [Get telco data usage report by ID](https://developers.telnyx.com/api-reference/telco-data-usage-reports/get-telco-data-usage-report-by-id.md): Retrieve a specific telco data usage report by its ID - [Delete telco data usage report](https://developers.telnyx.com/api-reference/telco-data-usage-reports/delete-telco-data-usage-report.md): Delete a specific telco data usage report by its ID ### Speech to text Usage Reports - [Get speech to text usage report](https://developers.telnyx.com/api-reference/speech-to-text-usage-reports/get-speech-to-text-usage-report.md): Generate and fetch speech to text usage report synchronously. This endpoint will both generate and fetch the speech to text report over a specified time period. ### CDR Usage Reports - [List CDR usage reports](https://developers.telnyx.com/api-reference/cdr-usage-reports/list-cdr-usage-reports.md): Fetch all previous requests for cdr usage reports. - [Create a new legacy usage V2 CDR report request](https://developers.telnyx.com/api-reference/cdr-usage-reports/create-a-new-legacy-usage-v2-cdr-report-request.md): Creates a new legacy usage V2 CDR report request with the specified filters - [Get a CDR usage report](https://developers.telnyx.com/api-reference/cdr-usage-reports/get-a-cdr-usage-report.md): Fetch single cdr usage report by id. - [Delete a V2 legacy usage CDR report request](https://developers.telnyx.com/api-reference/cdr-usage-reports/delete-a-v2-legacy-usage-cdr-report-request.md): Deletes a specific V2 legacy usage CDR report request by ID - [Generates and fetches CDR Usage Reports](https://developers.telnyx.com/api-reference/cdr-usage-reports/generates-and-fetches-cdr-usage-reports.md): Generate and fetch voice usage report synchronously. This endpoint will both generate and fetch the voice report over a specified time period. No polling is ne… ### Usage Reports (BETA) - [Get Telnyx product usage data (BETA)](https://developers.telnyx.com/api-reference/usage-reports-beta/get-telnyx-product-usage-data-beta.md): Get Telnyx usage data by product, broken out by the specified dimensions - [Get Usage Reports query options (BETA)](https://developers.telnyx.com/api-reference/usage-reports-beta/get-usage-reports-query-options-beta.md): Get the Usage Reports options for querying usage, including the products available and their respective metrics and dimensions ### Speech to Text Batch Reports - [Get all Speech to Text batch report requests](https://developers.telnyx.com/api-reference/speech-to-text-batch-reports/get-all-speech-to-text-batch-report-requests.md): Retrieves all Speech to Text batch report requests for the authenticated user - [Create a new Speech to Text batch report request](https://developers.telnyx.com/api-reference/speech-to-text-batch-reports/create-a-new-speech-to-text-batch-report-request.md): Creates a new Speech to Text batch report request with the specified filters - [Get a specific Speech to Text batch report request](https://developers.telnyx.com/api-reference/speech-to-text-batch-reports/get-a-specific-speech-to-text-batch-report-request.md): Retrieves a specific Speech to Text batch report request by ID - [Delete a Speech to Text batch report request](https://developers.telnyx.com/api-reference/speech-to-text-batch-reports/delete-a-speech-to-text-batch-report-request.md): Deletes a specific Speech to Text batch report request by ID ### MDR Detail Reports - [Fetch all Mdr records](https://developers.telnyx.com/api-reference/mdr-detail-reports/fetch-all-mdr-records.md): Fetch all Mdr records ### WDR Detail Reports - [Fetches all Wdr records](https://developers.telnyx.com/api-reference/wdr-detail-reports/fetches-all-wdr-records.md): Fetch all Wdr records ### Session Analysis - [Get metadata overview](https://developers.telnyx.com/api-reference/session-analysis/get-metadata-overview.md): Returns all available record types and supported query parameters for session analysis. - [Get record type metadata](https://developers.telnyx.com/api-reference/session-analysis/get-record-type-metadata.md): Returns detailed metadata for a specific record type, including relationships and examples. - [Get session analysis](https://developers.telnyx.com/api-reference/session-analysis/get-session-analysis.md): Retrieves a full session analysis tree for a given event, including costs, child events, and product linkages. ### Webhooks - [List webhook deliveries](https://developers.telnyx.com/api-reference/webhooks/list-webhook-deliveries.md): Lists webhook_deliveries for the authenticated user - [Find webhook_delivery details by ID](https://developers.telnyx.com/api-reference/webhooks/find-webhook_delivery-details-by-id.md): Provides webhook_delivery debug data, such as timestamps, delivery status and attempts. ### Audit Logs - [List Audit Logs](https://developers.telnyx.com/api-reference/audit-logs/list-audit-logs.md): Retrieve a list of audit log entries. Audit logs are a best-effort, eventually consistent record of significant account-related changes. ### User Tags - [List User Tags](https://developers.telnyx.com/api-reference/user-tags/list-user-tags.md): List all user tags. ### UserAddresses - [List all user addresses](https://developers.telnyx.com/api-reference/useraddresses/list-all-user-addresses.md): Returns a list of your user addresses. - [Creates a user address](https://developers.telnyx.com/api-reference/useraddresses/creates-a-user-address.md): Creates a user address. - [Retrieve a user address](https://developers.telnyx.com/api-reference/useraddresses/retrieve-a-user-address.md): Retrieves the details of an existing user address. ### Integration Secrets - [List integration secrets](https://developers.telnyx.com/api-reference/integration-secrets/list-integration-secrets.md): Retrieve a list of all integration secrets configured by the user. - [Create a secret](https://developers.telnyx.com/api-reference/integration-secrets/create-a-secret.md): Create a new secret with an associated identifier that can be used to securely integrate with other services. - [Delete an integration secret](https://developers.telnyx.com/api-reference/integration-secrets/delete-an-integration-secret.md): Delete an integration secret given its ID. ### Room Compositions - [View a list of room compositions.](https://developers.telnyx.com/api-reference/room-compositions/view-a-list-of-room-compositions.md) - [Create a room composition.](https://developers.telnyx.com/api-reference/room-compositions/create-a-room-composition.md): Asynchronously create a room composition. - [View a room composition.](https://developers.telnyx.com/api-reference/room-compositions/view-a-room-composition.md) - [Delete a room composition.](https://developers.telnyx.com/api-reference/room-compositions/delete-a-room-composition.md): Synchronously delete a room composition. ### Room Participants - [View a list of room participants.](https://developers.telnyx.com/api-reference/room-participants/view-a-list-of-room-participants.md) - [View a room participant.](https://developers.telnyx.com/api-reference/room-participants/view-a-room-participant.md) ### Room Recordings - [View a list of room recordings.](https://developers.telnyx.com/api-reference/room-recordings/view-a-list-of-room-recordings.md) - [Delete several room recordings in a bulk.](https://developers.telnyx.com/api-reference/room-recordings/delete-several-room-recordings-in-a-bulk.md) - [View a room recording.](https://developers.telnyx.com/api-reference/room-recordings/view-a-room-recording.md) - [Delete a room recording.](https://developers.telnyx.com/api-reference/room-recordings/delete-a-room-recording.md): Synchronously delete a Room Recording. ### Room Sessions - [View a list of room sessions.](https://developers.telnyx.com/api-reference/room-sessions/view-a-list-of-room-sessions.md) - [View a room session.](https://developers.telnyx.com/api-reference/room-sessions/view-a-room-session.md) - [End a room session.](https://developers.telnyx.com/api-reference/room-sessions/end-a-room-session.md): Note: this will also kick all participants currently present in the room - [Kick participants from a room session.](https://developers.telnyx.com/api-reference/room-sessions/kick-participants-from-a-room-session.md) - [Mute participants in room session.](https://developers.telnyx.com/api-reference/room-sessions/mute-participants-in-room-session.md) - [Unmute participants in room session.](https://developers.telnyx.com/api-reference/room-sessions/unmute-participants-in-room-session.md) - [View a list of room participants.](https://developers.telnyx.com/api-reference/room-sessions/view-a-list-of-room-participants.md) ### Rooms - [View a list of rooms.](https://developers.telnyx.com/api-reference/rooms/view-a-list-of-rooms.md) - [Create a room.](https://developers.telnyx.com/api-reference/rooms/create-a-room.md): Synchronously create a Room. - [View a room.](https://developers.telnyx.com/api-reference/rooms/view-a-room.md) - [Update a room.](https://developers.telnyx.com/api-reference/rooms/update-a-room.md): Synchronously update a Room. - [Delete a room.](https://developers.telnyx.com/api-reference/rooms/delete-a-room.md): Synchronously delete a Room. Participants from that room will be kicked out, they won't be able to join that room anymore, and you won't be charged anymore for… - [View a list of room sessions.](https://developers.telnyx.com/api-reference/rooms/view-a-list-of-room-sessions.md) ### Rooms Client Tokens - [Create Client Token to join a room.](https://developers.telnyx.com/api-reference/rooms-client-tokens/create-client-token-to-join-a-room.md): Synchronously create an Client Token to join a Room. Client Token is necessary to join a Telnyx Room. Client Token will expire after `token_ttl_secs`, a Refres… - [Refresh Client Token to join a room.](https://developers.telnyx.com/api-reference/rooms-client-tokens/refresh-client-token-to-join-a-room.md): Synchronously refresh an Client Token to join a Room. Client Token is necessary to join a Telnyx Room. Client Token will expire after `token_ttl_secs`. ### SETI Observability - [Retrieve Black Box Test Results](https://developers.telnyx.com/api-reference/seti-observability/retrieve-black-box-test-results.md): Returns the results of the various black box tests ### Programmable Fax Applications - [List all Fax Applications](https://developers.telnyx.com/api-reference/programmable-fax-applications/list-all-fax-applications.md): This endpoint returns a list of your Fax Applications inside the 'data' attribute of the response. You can adjust which applications are listed by using filter… - [Creates a Fax Application](https://developers.telnyx.com/api-reference/programmable-fax-applications/creates-a-fax-application.md): Creates a new Fax Application based on the parameters sent in the request. The application name and webhook URL are required. Once created, you can assign phon… - [Retrieve a Fax Application](https://developers.telnyx.com/api-reference/programmable-fax-applications/retrieve-a-fax-application.md): Return the details of an existing Fax Application inside the 'data' attribute of the response. - [Update a Fax Application](https://developers.telnyx.com/api-reference/programmable-fax-applications/update-a-fax-application.md): Updates settings of an existing Fax Application based on the parameters of the request. - [Deletes a Fax Application](https://developers.telnyx.com/api-reference/programmable-fax-applications/deletes-a-fax-application.md): Permanently deletes a Fax Application. Deletion may be prevented if the application is in use by phone numbers. ### Programmable Fax Commands - [View a list of faxes](https://developers.telnyx.com/api-reference/programmable-fax-commands/view-a-list-of-faxes.md) - [Send a fax](https://developers.telnyx.com/api-reference/programmable-fax-commands/send-a-fax.md): Send a fax. Files have size limits and page count limit validations. If a file is bigger than 50MB or has more than 350 pages it will fail with `file_size_limi… - [View a fax](https://developers.telnyx.com/api-reference/programmable-fax-commands/view-a-fax.md) - [Delete a fax](https://developers.telnyx.com/api-reference/programmable-fax-commands/delete-a-fax.md) - [Cancel a fax](https://developers.telnyx.com/api-reference/programmable-fax-commands/cancel-a-fax.md): Cancel the outbound fax that is in one of the following states: `queued`, `media.processed`, `originated` or `sending` - [Refresh a fax](https://developers.telnyx.com/api-reference/programmable-fax-commands/refresh-a-fax.md): Refreshes the inbound fax's media_url when it has expired ### Callbacks - [Fax Delivered](https://developers.telnyx.com/api-reference/callbacks/fax-delivered.md) - [Fax Failed](https://developers.telnyx.com/api-reference/callbacks/fax-failed.md) - [Fax Media Processed](https://developers.telnyx.com/api-reference/callbacks/fax-media-processed.md) - [Fax Queued](https://developers.telnyx.com/api-reference/callbacks/fax-queued.md) - [Fax Sending Started](https://developers.telnyx.com/api-reference/callbacks/fax-sending-started.md) ### OAuth Discovery - [Authorization server metadata](https://developers.telnyx.com/api-reference/oauth-discovery/authorization-server-metadata.md): OAuth 2.0 Authorization Server Metadata (RFC 8414) - [Protected resource metadata](https://developers.telnyx.com/api-reference/oauth-discovery/protected-resource-metadata.md): OAuth 2.0 Protected Resource Metadata for resource discovery ### OAuth Protocol - [OAuth authorization endpoint](https://developers.telnyx.com/api-reference/oauth-protocol/oauth-authorization-endpoint.md): OAuth 2.0 authorization endpoint for the authorization code flow - [Get OAuth consent token](https://developers.telnyx.com/api-reference/oauth-protocol/get-oauth-consent-token.md): Retrieve details about an OAuth consent token - [Token introspection](https://developers.telnyx.com/api-reference/oauth-protocol/token-introspection.md): Introspect an OAuth access token to check its validity and metadata - [JSON Web Key Set](https://developers.telnyx.com/api-reference/oauth-protocol/json-web-key-set.md): Retrieve the JSON Web Key Set for token verification - [Dynamic client registration](https://developers.telnyx.com/api-reference/oauth-protocol/dynamic-client-registration.md): Register a new OAuth client dynamically (RFC 7591) - [OAuth token endpoint](https://developers.telnyx.com/api-reference/oauth-protocol/oauth-token-endpoint.md): Exchange authorization code, client credentials, or refresh token for access token ### OAuth Clients - [List OAuth clients](https://developers.telnyx.com/api-reference/oauth-clients/list-oauth-clients.md): Retrieve a paginated list of OAuth clients for the authenticated user - [Create OAuth client](https://developers.telnyx.com/api-reference/oauth-clients/create-oauth-client.md): Create a new OAuth client - [Get OAuth client](https://developers.telnyx.com/api-reference/oauth-clients/get-oauth-client.md): Retrieve a single OAuth client by ID - [Update OAuth client](https://developers.telnyx.com/api-reference/oauth-clients/update-oauth-client.md): Update an existing OAuth client - [Delete OAuth client](https://developers.telnyx.com/api-reference/oauth-clients/delete-oauth-client.md): Delete an OAuth client ### OAuth Grants - [List OAuth grants](https://developers.telnyx.com/api-reference/oauth-grants/list-oauth-grants.md): Retrieve a paginated list of OAuth grants for the authenticated user - [Get OAuth grant](https://developers.telnyx.com/api-reference/oauth-grants/get-oauth-grant.md): Retrieve a single OAuth grant by ID - [Revoke OAuth grant](https://developers.telnyx.com/api-reference/oauth-grants/revoke-oauth-grant.md): Revoke an OAuth grant