diff --git a/fern/docs.yml b/fern/docs.yml index ab5e2e39..3b6e71a0 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -302,8 +302,10 @@ navigation: - page: Self-hosted streaming path: pages/05-guides/self-hosted-streaming.mdx hidden: true - - page: Webhooks + - page: Webhooks for pre-recorded audio path: pages/05-guides/webhooks.mdx + - page: Webhooks for streaming speech-to-text + path: pages/05-guides/webhooks-streaming.mdx - page: Evaluating STT models path: pages/08-concepts/evals.mdx - page: Account Management diff --git a/fern/pages/05-guides/webhooks-streaming.mdx b/fern/pages/05-guides/webhooks-streaming.mdx new file mode 100644 index 00000000..2c028cbc --- /dev/null +++ b/fern/pages/05-guides/webhooks-streaming.mdx @@ -0,0 +1,541 @@ +--- +title: "Webhooks for streaming speech-to-text" +hide-nav-links: true +description: "Get notified when a streaming transcription session ends." +--- + +Webhooks allow you to receive the complete transcript via HTTP callback when a streaming session ends. This is in addition to the normal WebSocket responses you receive during the session. + + + This guide covers webhooks for [streaming audio + transcription](/docs/speech-to-text/universal-streaming). For webhooks with + pre-recorded audio, see [Webhooks for pre-recorded + audio](/docs/deployment/webhooks). + + +## Configure webhooks for a streaming session + +To use webhooks with streaming speech-to-text, add the following parameters to your WebSocket connection URL: + +| Parameter | Required | Description | +| --------------------------- | -------- | ------------------------------------------------------------------------- | +| `webhook_url` | Yes | The URL to send the transcript to when the session ends. | +| `webhook_auth_header_name` | No | The name of the authentication header to include in the webhook request. | +| `webhook_auth_header_value` | No | The value of the authentication header to include in the webhook request. | + + + Create a test webhook endpoint with [webhook.site](https://webhook.site) to + test your webhook integration. + + +### Example WebSocket URL with webhook parameters + +Add the webhook parameters as query parameters to the WebSocket URL: + +``` +wss://streaming.assemblyai.com/v3/ws?sample_rate=16000&webhook_url=https://example.com/webhook +``` + +To include authentication: + +``` +wss://streaming.assemblyai.com/v3/ws?sample_rate=16000&webhook_url=https://example.com/webhook&webhook_auth_header_name=X-Webhook-Secret&webhook_auth_header_value=secret-value +``` + + + + +```python +import assemblyai as aai +from assemblyai.streaming.v3 import ( + BeginEvent, + StreamingClient, + StreamingClientOptions, + StreamingError, + StreamingEvents, + StreamingParameters, + TerminationEvent, + TurnEvent, +) +from typing import Type + +api_key = "" + +def on_begin(self: Type[StreamingClient], event: BeginEvent): + print(f"Session started: {event.id}") + +def on_turn(self: Type[StreamingClient], event: TurnEvent): + print(f"{event.transcript} ({event.end_of_turn})") + +def on_terminated(self: Type[StreamingClient], event: TerminationEvent): + print(f"Session terminated: {event.audio_duration_seconds} seconds of audio processed") + +def on_error(self: Type[StreamingClient], error: StreamingError): + print(f"Error occurred: {error}") + +def main(): + client = StreamingClient( + StreamingClientOptions( + api_key=api_key, + api_host="streaming.assemblyai.com", + ) + ) + + client.on(StreamingEvents.Begin, on_begin) + client.on(StreamingEvents.Turn, on_turn) + client.on(StreamingEvents.Termination, on_terminated) + client.on(StreamingEvents.Error, on_error) + + client.connect( + StreamingParameters( + sample_rate=16000, + format_turns=True, + # Webhook parameters + webhook_url="https://example.com/webhook", + webhook_auth_header_name="X-Webhook-Secret", # Optional + webhook_auth_header_value="secret-value", # Optional + ) + ) + + try: + client.stream(aai.extras.MicrophoneStream(sample_rate=16000)) + finally: + client.disconnect(terminate=True) + +if __name__ == "__main__": + main() +``` + + + + +```python +import pyaudio +import websocket +import json +import threading +import time +from urllib.parse import urlencode +from datetime import datetime + +# --- Configuration --- +YOUR_API_KEY = "" + +CONNECTION_PARAMS = { + "sample_rate": 16000, + "format_turns": True, + # Webhook parameters + "webhook_url": "https://example.com/webhook", + "webhook_auth_header_name": "X-Webhook-Secret", # Optional + "webhook_auth_header_value": "secret-value", # Optional +} +API_ENDPOINT_BASE_URL = "wss://streaming.assemblyai.com/v3/ws" +API_ENDPOINT = f"{API_ENDPOINT_BASE_URL}?{urlencode(CONNECTION_PARAMS)}" + +# Audio Configuration +FRAMES_PER_BUFFER = 800 # 50ms of audio (0.05s * 16000Hz) +SAMPLE_RATE = CONNECTION_PARAMS["sample_rate"] +CHANNELS = 1 +FORMAT = pyaudio.paInt16 + +# Global variables +audio = None +stream = None +ws_app = None +audio_thread = None +stop_event = threading.Event() + + +def on_open(ws): + """Called when the WebSocket connection is established.""" + print("WebSocket connection opened.") + print(f"Connected to: {API_ENDPOINT}") + + def stream_audio(): + global stream + print("Starting audio streaming...") + while not stop_event.is_set(): + try: + audio_data = stream.read(FRAMES_PER_BUFFER, exception_on_overflow=False) + ws.send(audio_data, websocket.ABNF.OPCODE_BINARY) + except Exception as e: + print(f"Error streaming audio: {e}") + break + print("Audio streaming stopped.") + + global audio_thread + audio_thread = threading.Thread(target=stream_audio) + audio_thread.daemon = True + audio_thread.start() + + +def on_message(ws, message): + """Called when a message is received from the WebSocket.""" + try: + data = json.loads(message) + msg_type = data.get("type") + + if msg_type == "Begin": + session_id = data.get("id") + expires_at = data.get("expires_at") + print(f"\nSession began: ID={session_id}, ExpiresAt={datetime.fromtimestamp(expires_at)}") + elif msg_type == "Turn": + transcript = data.get("transcript", "") + formatted = data.get("turn_is_formatted", False) + if formatted: + print("\r" + " " * 80 + "\r", end="") + print(transcript) + else: + print(f"\r{transcript}", end="") + elif msg_type == "Termination": + audio_duration = data.get("audio_duration_seconds", 0) + session_duration = data.get("session_duration_seconds", 0) + print(f"\nSession Terminated: Audio Duration={audio_duration}s, Session Duration={session_duration}s") + except json.JSONDecodeError as e: + print(f"Error decoding message: {e}") + except Exception as e: + print(f"Error handling message: {e}") + + +def on_error(ws, error): + """Called when a WebSocket error occurs.""" + print(f"\nWebSocket Error: {error}") + stop_event.set() + + +def on_close(ws, close_status_code, close_msg): + """Called when the WebSocket connection is closed.""" + print(f"\nWebSocket Disconnected: Status={close_status_code}, Msg={close_msg}") + global stream, audio + stop_event.set() + + if stream: + if stream.is_active(): + stream.stop_stream() + stream.close() + stream = None + if audio: + audio.terminate() + audio = None + if audio_thread and audio_thread.is_alive(): + audio_thread.join(timeout=1.0) + + +def run(): + global audio, stream, ws_app + + audio = pyaudio.PyAudio() + + try: + stream = audio.open( + input=True, + frames_per_buffer=FRAMES_PER_BUFFER, + channels=CHANNELS, + format=FORMAT, + rate=SAMPLE_RATE, + ) + print("Microphone stream opened successfully.") + print("Speak into your microphone. Press Ctrl+C to stop.") + except Exception as e: + print(f"Error opening microphone stream: {e}") + if audio: + audio.terminate() + return + + ws_app = websocket.WebSocketApp( + API_ENDPOINT, + header={"Authorization": YOUR_API_KEY}, + on_open=on_open, + on_message=on_message, + on_error=on_error, + on_close=on_close, + ) + + ws_thread = threading.Thread(target=ws_app.run_forever) + ws_thread.daemon = True + ws_thread.start() + + try: + while ws_thread.is_alive(): + time.sleep(0.1) + except KeyboardInterrupt: + print("\nCtrl+C received. Stopping...") + stop_event.set() + + if ws_app and ws_app.sock and ws_app.sock.connected: + try: + terminate_message = {"type": "Terminate"} + ws_app.send(json.dumps(terminate_message)) + time.sleep(2) + except Exception as e: + print(f"Error sending termination message: {e}") + + if ws_app: + ws_app.close() + ws_thread.join(timeout=2.0) + + finally: + if stream and stream.is_active(): + stream.stop_stream() + if stream: + stream.close() + if audio: + audio.terminate() + print("Cleanup complete.") + + +if __name__ == "__main__": + run() +``` + + + + +```javascript +import { Readable } from "stream"; +import { AssemblyAI } from "assemblyai"; +import recorder from "node-record-lpcm16"; + +const run = async () => { + const client = new AssemblyAI({ + apiKey: "", + }); + + const transcriber = client.streaming.transcriber({ + sampleRate: 16_000, + formatTurns: true, + // Webhook parameters + webhookUrl: "https://example.com/webhook", + webhookAuthHeaderName: "X-Webhook-Secret", // Optional + webhookAuthHeaderValue: "secret-value", // Optional + }); + + transcriber.on("open", ({ id }) => { + console.log(`Session opened with ID: ${id}`); + }); + + transcriber.on("error", (error) => { + console.error("Error:", error); + }); + + transcriber.on("close", (code, reason) => + console.log("Session closed:", code, reason) + ); + + transcriber.on("turn", (turn) => { + if (!turn.transcript) { + return; + } + console.log("Turn:", turn.transcript); + }); + + try { + console.log("Connecting to streaming transcript service"); + await transcriber.connect(); + + console.log("Starting recording"); + const recording = recorder.record({ + channels: 1, + sampleRate: 16_000, + audioType: "wav", + }); + + Readable.toWeb(recording.stream()).pipeTo(transcriber.stream()); + + process.on("SIGINT", async function () { + console.log(); + console.log("Stopping recording"); + recording.stop(); + + console.log("Closing streaming transcript connection"); + await transcriber.close(); + + process.exit(); + }); + } catch (error) { + console.error(error); + } +}; + +run(); +``` + + + + +```javascript +const WebSocket = require("ws"); + +const API_KEY = ""; + +const connectionParams = new URLSearchParams({ + sample_rate: 16000, + format_turns: true, + // Webhook parameters + webhook_url: "https://example.com/webhook", + webhook_auth_header_name: "X-Webhook-Secret", // Optional + webhook_auth_header_value: "secret-value", // Optional +}); + +const API_ENDPOINT = `wss://streaming.assemblyai.com/v3/ws?${connectionParams.toString()}`; + +const ws = new WebSocket(API_ENDPOINT, { + headers: { + Authorization: API_KEY, + }, +}); + +ws.on("open", () => { + console.log("WebSocket connection opened."); + console.log(`Connected to: ${API_ENDPOINT}`); + + // Start streaming audio data here + // For example, using a microphone input library +}); + +ws.on("message", (data) => { + try { + const message = JSON.parse(data); + const msgType = message.type; + + if (msgType === "Begin") { + const sessionId = message.id; + const expiresAt = new Date(message.expires_at * 1000); + console.log(`\nSession began: ID=${sessionId}, ExpiresAt=${expiresAt}`); + } else if (msgType === "Turn") { + const transcript = message.transcript || ""; + const formatted = message.turn_is_formatted || false; + if (formatted) { + process.stdout.write("\r" + " ".repeat(80) + "\r"); + console.log(transcript); + } else { + process.stdout.write(`\r${transcript}`); + } + } else if (msgType === "Termination") { + const audioDuration = message.audio_duration_seconds || 0; + const sessionDuration = message.session_duration_seconds || 0; + console.log( + `\nSession Terminated: Audio Duration=${audioDuration}s, Session Duration=${sessionDuration}s` + ); + } + } catch (e) { + console.error("Error handling message:", e); + } +}); + +ws.on("error", (error) => { + console.error("WebSocket Error:", error); +}); + +ws.on("close", (code, reason) => { + console.log(`WebSocket Disconnected: Status=${code}, Msg=${reason}`); +}); + +// To gracefully close the session, send a Terminate message +function terminateSession() { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "Terminate" })); + } +} +``` + + + + +## Handle webhook deliveries + +When the streaming session ends, AssemblyAI sends a `POST` HTTP request to the URL you specified. The webhook contains the complete transcript from the session. + +Your webhook endpoint must return a 2xx HTTP status code within 10 seconds to indicate successful receipt. If a 2xx status is not received within 10 seconds, AssemblyAI will retry the webhook call up to a total of 10 attempts. If at any point your endpoint returns a 4xx status code, the webhook call is considered failed and will not be retried. + + + +AssemblyAI sends all webhook deliveries from fixed IP addresses: + +| Region | IP Address | +| ------ | -------------- | +| US | `44.238.19.20` | +| EU | `54.220.25.36` | + + + +### Delivery payload + +The webhook delivery payload contains the complete transcript from the streaming session as a JSON object. The payload includes the session ID and an array of messages containing all the transcript turns. + +```json +{ + "session_id": "273e79fd-99e9-4e1d-91da-90f56a132d01", + "messages": [ + { + "turn_order": 0, + "turn_is_formatted": true, + "end_of_turn": true, + "transcript": "Smoke from hundreds of wildfires in Canada is triggering air quality alerts throughout the US Skylines from Maine to Maryland to Minnesota are gray and smoggy, and in some places the air.", + "end_of_turn_confidence": 0.5005, + "words": [ + { + "start": 4880, + "end": 5040, + "text": "Smoke", + "confidence": 0.76054, + "word_is_final": true + }, + { + "start": 5280, + "end": 5360, + "text": "from", + "confidence": 0.761065, + "word_is_final": true + } + ], + "utterance": "", + "type": "Turn" + } + ] +} +``` + +| Key | Type | Description | +| ----------------------------------- | ------- | -------------------------------------------------------------- | +| `session_id` | string | The unique identifier for the streaming session. | +| `messages` | array | An array of transcript turn objects from the session. | +| `messages[].turn_order` | integer | The order of the turn in the session (0-indexed). | +| `messages[].turn_is_formatted` | boolean | Whether the transcript has been formatted. | +| `messages[].end_of_turn` | boolean | Whether this message represents the end of a turn. | +| `messages[].transcript` | string | The transcribed text for this turn. | +| `messages[].end_of_turn_confidence` | number | Confidence score for the end of turn detection. | +| `messages[].words` | array | Word-level details including timestamps and confidence scores. | +| `messages[].type` | string | The message type, typically "Turn". | + +## Authenticate webhook deliveries + +To secure your webhook endpoint, you can include custom authentication headers in the webhook request. When configuring your streaming session, provide the `webhook_auth_header_name` and `webhook_auth_header_value` parameters. + +AssemblyAI will include this header in the webhook request, allowing you to verify that the request came from AssemblyAI. + +``` +webhook_auth_header_name=X-Webhook-Secret&webhook_auth_header_value=secret-value +``` + +In your webhook receiver, verify the header value matches what you configured: + +```python +auth_header = request.headers.get("X-Webhook-Secret") +if auth_header != "secret-value": + return "Unauthorized", 401 +``` + +## Best practices + +When implementing webhooks for streaming speech-to-text, consider the following best practices: + +1. **Always verify authentication**: If you configure an authentication header, always verify it in your webhook receiver to ensure requests are from AssemblyAI. + +2. **Respond quickly**: Return a response from your webhook endpoint as quickly as possible. If you need to perform time-consuming processing, do it asynchronously after returning the response. + +3. **Handle failures gracefully**: Your webhook endpoint should handle errors gracefully and return appropriate HTTP status codes. + +4. **Use HTTPS**: Always use HTTPS for your webhook URL to ensure the transcript data is encrypted in transit. + +5. **Log webhook deliveries**: Keep logs of webhook deliveries for debugging and auditing purposes. diff --git a/fern/pages/05-guides/webhooks.mdx b/fern/pages/05-guides/webhooks.mdx index 75df9cfa..065779a5 100644 --- a/fern/pages/05-guides/webhooks.mdx +++ b/fern/pages/05-guides/webhooks.mdx @@ -1,10 +1,17 @@ --- -title: "Webhooks" +title: "Webhooks for pre-recorded audio" hide-nav-links: true -description: "Get notified when a transcription is ready." +description: "Get notified when a pre-recorded audio transcription is ready." --- -Webhooks are custom HTTP callbacks that you can define to get notified when your transcripts are ready. +Webhooks are custom HTTP callbacks that you can define to get notified when your pre-recorded audio transcripts are ready. + + + This guide covers webhooks for [pre-recorded audio + transcription](/docs/speech-to-text/pre-recorded-audio). For webhooks with + streaming audio, see [Webhooks for streaming + speech-to-text](/docs/deployment/webhooks-streaming). + To use webhooks, you need to set up your own webhook receiver to handle webhook deliveries. @@ -126,195 +133,6 @@ const url = `${baseUrl}/v2/transcript`; const response = await axios.post(url, data, { headers: headers }); ``` - - - -To create a webhook, set the `webhook_url` parameter when you create a new transcription. The URL must be accessible from AssemblyAI's servers. - -```csharp -using System; -using System.IO; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Net.Http.Json; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; - -public class Transcript -{ -public string Id { get; set; } -public string Status { get; set; } -public string Text { get; set; } -public string Error { get; set; } -} - -async Task UploadFileAsync(string filePath, HttpClient httpClient) -{ -using (var fileStream = File.OpenRead(filePath)) -using (var fileContent = new StreamContent(fileStream)) -{ -fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); - - using (var response = await httpClient.PostAsync("https://api.assemblyai.com/v2/upload", fileContent)) - { - response.EnsureSuccessStatusCode(); - var jsonDoc = await response.Content.ReadFromJsonAsync(); - return jsonDoc.RootElement.GetProperty("upload_url").GetString(); - } - } - -} - -async Task CreateTranscriptAsync(string audioUrl, HttpClient httpClient) -{ -var data = new { audio_url = audioUrl, webhook_url = "https://example.com/webhook" }; -var content = new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json"); - - using (var response = await httpClient.PostAsync("https://api.assemblyai.com/v2/transcript", content)) - { - response.EnsureSuccessStatusCode(); - return await response.Content.ReadFromJsonAsync(); - } - -} - -// Main execution -using (var httpClient = new HttpClient()) -{ -httpClient.DefaultRequestHeaders.Authorization = -new AuthenticationHeaderValue(""); - - var uploadUrl = await UploadFileAsync("my-audio.mp3", httpClient); - var transcript = await CreateTranscriptAsync(uploadUrl, httpClient); - -} - -``` - - - - -To create a webhook, set the `webhook_url` parameter when you create a new transcription. The URL must be accessible from AssemblyAI's servers. - -```ruby -require 'net/http' -require 'json' - -base_url = 'https://api.assemblyai.com' - -headers = { - 'authorization' => '', - 'content-type' => 'application/json' -} - -path = "./my-audio.mp3" -uri = URI("#{base_url}/v2/upload") -request = Net::HTTP::Post.new(uri, headers) -request.body = File.read(path) - -http = Net::HTTP.new(uri.host, uri.port) -http.use_ssl = true -upload_response = http.request(request) -upload_url = JSON.parse(upload_response.body)["upload_url"] - -data = { - "audio_url" => upload_url, - "webhook_url" => "https://example.com/webhook" -} - -uri = URI.parse("#{base_url}/v2/transcript") -http = Net::HTTP.new(uri.host, uri.port) -http.use_ssl = true - -request = Net::HTTP::Post.new(uri.request_uri, headers) -request.body = data.to_json - -response = http.request(request) -response_body = JSON.parse(response.body) - -unless response.is_a?(Net::HTTPSuccess) - raise "API request failed with status #{response.code}: #{response.body}" -end - -transcript_id = response_body['id'] -puts "Transcript ID: #{transcript_id}" - -polling_endpoint = URI.parse("#{base_url}/v2/transcript/#{transcript_id}") - -while true - polling_http = Net::HTTP.new(polling_endpoint.host, polling_endpoint.port) - polling_http.use_ssl = true - polling_request = Net::HTTP::Get.new(polling_endpoint.request_uri, headers) - polling_response = polling_http.request(polling_request) - - transcription_result = JSON.parse(polling_response.body) - - if transcription_result['status'] == 'completed' - puts "Transcription text: #{transcription_result['text']}" - break - elsif transcription_result['status'] == 'error' - raise "Transcription failed: #{transcription_result['error']}" - else - puts 'Waiting for transcription to complete...' - sleep(3) - end -end -``` - - - - -```php -", -"content-type: application/json" -); - -$path = "./my-audio.mp3"; - -$ch = curl_init(); - -curl_setopt($ch, CURLOPT_URL, $base_url . "/v2/upload"); -curl_setopt($ch, CURLOPT_POST, 1); -curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents($path)); -curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - -$response = curl_exec($ch); -$response_data = json_decode($response, true); -$upload_url = $response_data["upload_url"]; - -curl_close($ch); - -$data = array( -"audio_url" => $upload_url, -"webhook_url" => "https://example.com/webhook" -); - -$url = $base_url . "/v2/transcript"; -$curl = curl_init($url); - -curl_setopt($curl, CURLOPT_POST, true); -curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data)); -curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); -curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - -$response = curl_exec($curl); - -$response = json_decode($response, true); - -curl_close($curl); - -``` - @@ -322,6 +140,8 @@ curl_close($curl); When the transcript is ready, AssemblyAI will send a `POST` HTTP request to the URL that you specified. +Your webhook endpoint must return a 2xx HTTP status code within 10 seconds to indicate successful receipt. If a 2xx status is not received within 10 seconds, AssemblyAI will retry the webhook call up to a total of 10 attempts. If at any point your endpoint returns a 4xx status code, the webhook call is considered failed and will not be retried. + AssemblyAI sends all webhook deliveries from fixed IP addresses: @@ -349,6 +169,10 @@ The webhook delivery payload contains a JSON object with the following propertie | `transcript_id` | string | The ID of the transcript. | | `status` | string | The status of the transcript. Either `completed` or `error`. | +### Webhook status code + +When you retrieve the transcript using the transcript ID, the JSON response will include a `webhook_status_code` field. This field contains the HTTP status code that AssemblyAI received when calling your webhook endpoint, allowing you to verify that the webhook was delivered successfully. + ### Retrieve a transcript with the transcript ID @@ -442,119 +266,6 @@ if (transcriptionResult.status === "completed") { ``` - - -```csharp -using System; -using System.IO; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Net.Http.Json; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; - -public class Transcript -{ -public string Id { get; set; } -public string Status { get; set; } -public string Text { get; set; } -public string Error { get; set; } -} - -async Task RetrieveTranscript(string transcriptId, HttpClient httpClient) -{ -var pollingEndpoint = $"https://api.assemblyai.com/v2/transcript/{transcriptId}"; - var pollingResponse = await httpClient.GetAsync(pollingEndpoint); - var transcript = await pollingResponse.Content.ReadFromJsonAsync(); - switch (transcript.Status) - { - case "completed": - return transcript; - case "error": - throw new Exception($"Transcription failed: {transcript.Error}"); -default: -throw new Exception("This code should not be reachable."); -} -} - -// Main execution -using (var httpClient = new HttpClient()) -{ -httpClient.DefaultRequestHeaders.Authorization = -new AuthenticationHeaderValue(""); -var transcript = await RetrieveTranscript("", httpClient); -} - -``` - - - - -```ruby -require 'net/http' -require 'json' - -base_url = 'https://api.assemblyai.com' - -headers = { - 'authorization' => '', -} - -transcript_id = "" -polling_endpoint = URI.parse("#{base_url}/v2/transcript/#{transcript_id}") - -polling_http = Net::HTTP.new(polling_endpoint.host, polling_endpoint.port) -polling_http.use_ssl = true -polling_request = Net::HTTP::Get.new(polling_endpoint.request_uri, headers) -polling_response = polling_http.request(polling_request) - -transcription_result = JSON.parse(polling_response.body) - -if transcription_result['status'] == 'completed' - puts "Transcription text: #{transcription_result['text']}" -elsif transcription_result['status'] == 'error' - raise "Transcription failed: #{transcription_result['error']}" -else - puts 'Waiting for transcription to complete...' - sleep(3) -end -``` - - - - -```php -", -"content-type: application/json" -); - -$transcript_id = ""; -$polling_endpoint = "https://api.assemblyai.com/v2/transcript/" . $transcript_id; - -$polling_response = curl_init($polling_endpoint); -curl_setopt($polling_response, CURLOPT_HTTPHEADER, $headers); -curl_setopt($polling_response, CURLOPT_RETURNTRANSFER, true); - -$transcription_result = json_decode(curl_exec($polling_response), true); - -if ($transcription_result['status'] === "completed") { - echo $transcription_result['text']; -} else if ($transcription_result['status'] === "error") { -throw new Exception("Transcription failed: " . $transcription_result['error']); -} - -``` - - ## Authenticate webhook deliveries