<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: api</title>
    <description>The latest articles tagged 'api' on DEV Community.</description>
    <link>https://dev.to/t/api</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tag/api"/>
    <language>en</language>
    <item>
      <title>Your Pipeline Is 27.3h Behind: Catching World Sentiment Leads with Pulsebit</title>
      <dc:creator>Pulsebit News Sentiment API</dc:creator>
      <pubDate>Wed, 22 Jul 2026 17:02:25 +0000</pubDate>
      <link>https://dev.to/pulsebitapi/your-pipeline-is-273h-behind-catching-world-sentiment-leads-with-pulsebit-51op</link>
      <guid>https://dev.to/pulsebitapi/your-pipeline-is-273h-behind-catching-world-sentiment-leads-with-pulsebit-51op</guid>
      <description>&lt;h2&gt;
  
  
  Your Pipeline Is 27.3h Behind: Catching World Sentiment Leads with Pulsebit
&lt;/h2&gt;

&lt;p&gt;We recently uncovered a striking anomaly: a 24h momentum spike at +0.210. This spike reveals something significant in the landscape of sentiment surrounding mental health, as evidenced by the leading English press coverage leading by 27.3 hours. Specifically, the article titled "World Brain Day: Mental health is as important as physical health, says Ballari" is pivotal in this frame, clustered around themes like brain, health, and world. If you’re not adjusting your models to account for these shifts, you’re missing critical insights.&lt;/p&gt;

&lt;p&gt;The problem here is straightforward: your pipeline may be lagging significantly. With a leading language of English and the dominant entity being 'brain', your model missed this sentiment spike by 27.3 hours. This gap highlights a structural issue in handling multilingual origins and entity dominance. If you’re not capturing these signals promptly, you risk making decisions based on outdated data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq9h8x7ahg3jm7jpsf7is.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq9h8x7ahg3jm7jpsf7is.png" alt="English coverage led by 27.3 hours. Af at T+27.3h. Confidenc" width="800" height="423"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;English coverage led by 27.3 hours. Af at T+27.3h. Confidence scores: English 0.85, Spanish 0.85, Id 0.85 Source: Pulsebit /sentiment_by_lang.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let’s dive into the code that can help catch these anomalies effectively. We’ll first set up a geographic origin filter to query only English language sources. Here’s how to do that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Python&lt;/span&gt; &lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;news_semantic&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;world&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;pub&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c3309ec893c24fb9ae292f229e1688a6&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;figures&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;g3_code_output_split_1784739743929&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;png&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Python&lt;/span&gt; &lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;news_semantic&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;world&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;returned&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="nf"&gt;structure &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clusters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt; &lt;span class="n"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Pulsebit&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;news_semantic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;


&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pulsebit.com/articles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;world&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lang&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;0.062&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;momentum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;0.210&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have the relevant articles, the next step is to run the cluster reason string back through our sentiment analysis endpoint. This allows us to score the narrative framing itself, providing deeper insights. Here’s how to implement this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sentiment_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pulsebit.com/sentiment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clustered by shared themes: brain, health, world, day:, mental.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;sentiment_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentiment_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sentiment_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sentiment_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this setup, you’re not just capturing data; you’re gaining a comprehensive view of the narrative that’s shaping public sentiment around critical issues.&lt;/p&gt;

&lt;p&gt;Now, let's discuss three specific builds you can implement with this pattern. First, use the geographic filter to identify spikes in sentiment around discussions of 'world' with a threshold of +0.210. This ensures you’re catching relevant discussions early. Second, leverage the meta-sentiment loop to analyze narratives around 'brain' and 'health' with a focus on emotional framing; consider a threshold of sentiment score +0.062 to prioritize content that resonates deeply. Finally, track forming themes like 'world', 'cup', and 'after' as these can indicate emerging conversations that often precede larger trends in mental health discussions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmw2xzl34kw5re8pqlt5w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmw2xzl34kw5re8pqlt5w.png" alt="Geographic detection output for world. India leads with 46 a" width="800" height="423"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Geographic detection output for world. India leads with 46 articles and sentiment +0.33. Source: Pulsebit /news_recent geographic fields.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Curious to get started? Head over to pulsebit.lojenterprise.com/docs. You can copy-paste the code snippets above and run them in under 10 minutes. Let’s ensure our pipelines are responsive and in tune with the world’s sentiment—don’t let valuable insights slip through the cracks.&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>datascience</category>
      <category>nlp</category>
    </item>
    <item>
      <title>Building an Agent-Native Data Layer over MCP</title>
      <dc:creator>Arman Obosyan</dc:creator>
      <pubDate>Wed, 22 Jul 2026 16:58:09 +0000</pubDate>
      <link>https://dev.to/sugra-systems/building-an-agent-native-data-layer-over-mcp-1i3a</link>
      <guid>https://dev.to/sugra-systems/building-an-agent-native-data-layer-over-mcp-1i3a</guid>
      <description>&lt;p&gt;Agents are good at reasoning and bad at remembering how to talk to every data vendor. The usual pattern is a growing pile of one-off integrations: one client for markets, another for macro series, another for sanctions lists, another for weather. Each has its own auth, schema, rate limit, and failure mode. The agent either hardcodes a few endpoints or invents numbers when a call fails.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F36lkozqpsnsssk38iuyw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F36lkozqpsnsssk38iuyw.gif" alt=" " width="760" height="593"&gt;&lt;/a&gt;&lt;br&gt;
This post is about a different shape: one MCP server in front of one API that already normalizes a large catalog of world data. The server is &lt;a href="https://github.com/Sugra-Systems/sugra-api-mcp?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp_launch_202607" rel="noopener noreferrer"&gt;Sugra MCP&lt;/a&gt; (&lt;code&gt;pip install sugra-api-mcp&lt;/code&gt;). Behind it sits the Sugra API - 1,500+ endpoints across 36 domains, backed by 160+ primary sources (sovereign statistics agencies, central banks, intergovernmental bodies, academic sources, and a smaller commercial slice under Sugra-branded wrappers).&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem agents actually hit
&lt;/h2&gt;

&lt;p&gt;Tool-using agents do not need "more models." They need reliable access to facts with provenance. In practice teams rebuild the same glue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auth headers and key rotation per vendor&lt;/li&gt;
&lt;li&gt;Divergent JSON shapes and pagination&lt;/li&gt;
&lt;li&gt;Silent empty responses that the model fills with guesses&lt;/li&gt;
&lt;li&gt;No single place to discover what is even available&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MCP fixes the client-side contract (tools, prompts, resources over one protocol). It does not by itself fix the data plane. You still need a stable catalog, a consistent call shape, and errors the agent can act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MCP solves - and what it does not
&lt;/h2&gt;

&lt;p&gt;MCP gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A standard way for Claude, ChatGPT, Gemini, xAI, Cursor, VS Code, and other clients to list and call tools&lt;/li&gt;
&lt;li&gt;Hosted HTTP and local stdio transports&lt;/li&gt;
&lt;li&gt;Room for prompts and resources next to tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MCP does not give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A curated multi-domain data catalog&lt;/li&gt;
&lt;li&gt;Source attribution and freshness on every number&lt;/li&gt;
&lt;li&gt;One rate limit and one API key across domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those belong in the data layer behind the server. Sugra MCP is that layer's agent face, not a replacement for the protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundled catalog instead of live schema scraping
&lt;/h2&gt;

&lt;p&gt;Discovery should not cost quota and should not depend on a network hop to the API for every &lt;code&gt;search&lt;/code&gt; or &lt;code&gt;describe&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The package ships a bundled endpoint catalog. Search and describe run locally against that catalog. Only real data fetches hit &lt;a href="https://sugra.ai?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp_launch_202607" rel="noopener noreferrer"&gt;sugra.ai&lt;/a&gt;. That keeps agent planning loops cheap and makes the server useful offline for "what exists?" questions.&lt;/p&gt;

&lt;p&gt;When the API grows, the catalog is regenerated and released with the package (and mirrored on the hosted endpoint after deploy). Agents get a versioned view of the surface instead of scraping OpenAPI on every session.&lt;/p&gt;

&lt;h2&gt;
  
  
  The search / describe / call pattern
&lt;/h2&gt;

&lt;p&gt;The tool surface is intentionally small:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Search&lt;/strong&gt; - natural language or keywords over the catalog&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Describe&lt;/strong&gt; - parameters, domains, and source notes for an endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call / fetch&lt;/strong&gt; - execute with structured arguments&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On the hosted endpoint the surface is wider (11 tools vs 8 local), including composed helpers for common agent paths. The idea is the same: the agent finds the right operation, inspects it, then calls it - instead of you hardcoding paths into the system prompt.&lt;/p&gt;

&lt;p&gt;Six workflow prompts (market snapshot, macro briefing, sanctions screening, sector compare, earth/conditions-style flows, source overview) package multi-step patterns for clients that surface MCP prompts. Three read-only catalog resources expose discovery material without burning data quota.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured errors instead of exceptions the model swallows
&lt;/h2&gt;

&lt;p&gt;When an endpoint is missing, a parameter is wrong, or a source is temporarily empty, the tool returns a &lt;strong&gt;structured error&lt;/strong&gt; the agent can branch on: retry, pick another series, or tell the user the data is unavailable.&lt;/p&gt;

&lt;p&gt;That is more valuable than "covering every source on day one." An honest failure with a machine-readable reason beats a fluent wrong number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attribution and freshness as first-class fields
&lt;/h2&gt;

&lt;p&gt;Every successful data result carries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source attribution&lt;/strong&gt; - where the number came from (sovereign and intergovernmental names are shown as-is; commercial upstreams appear under Sugra product wrappers on public surfaces)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness&lt;/strong&gt; - how current the observation is&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agents can cite sources in the user-facing answer. Humans can audit a chain of tool calls. This is the difference between a demo scraper and something you can put near a research or compliance workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosted vs local: one contract
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local / self-hosted&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pip install sugra-api-mcp&lt;/code&gt; over stdio or your HTTP&lt;/td&gt;
&lt;td&gt;8 tools + prompts/resources&lt;/td&gt;
&lt;td&gt;IDEs, private runners, offlin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://app.sugra.ai/mcp?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp_launch_202607" rel="noopener noreferrer"&gt;app.sugra.ai/mcp&lt;/a&gt; (Streamable HT&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same API key model (&lt;code&gt;SUGRA_API_KEY&lt;/code&gt; / account key). Free tier is 50 requests/day so you can evaluate without a card. Paid tiers scale volume only - the full endpoint surface is on every plan.&lt;/p&gt;

&lt;p&gt;The server is also listed in the official MCP Registry as &lt;code&gt;ai.sugra/api-mcp&lt;/code&gt; and in the &lt;a href="https://chatgpt.com/plugins/plugin_asdk_app_6a33ce728e488191a82df247ab605e91" rel="noopener noreferrer"&gt;OpenAI Plugins Directory&lt;/a&gt; for ChatGPT and Codex. On Smithery: &lt;a href="https://smithery.ai/servers/sugra-systems/sugra-api" rel="noopener noreferrer"&gt;sugra-systems/sugra-api&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof (live)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,500+ endpoints / 36 domains / 160+ sources&lt;/td&gt;
&lt;td&gt;&lt;a href="https://sugra.ai/stats" rel="noopener noreferrer"&gt;sugra.ai/stats&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP package&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://pypi.org/project/sugra-api-mcp/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt; · &lt;a href="https://github.com/Sugra-Systems/sugra-api-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official MCP Registry&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ai.sugra/api-mcp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted MCP&lt;/td&gt;
&lt;td&gt;&lt;a href="https://app.sugra.ai/mcp" rel="noopener noreferrer"&gt;app.sugra.ai/mcp&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Plugins&lt;/td&gt;
&lt;td&gt;&lt;a href="https://chatgpt.com/plugins/plugin_asdk_app_6a33ce728e488191a82df247ab605e91" rel="noopener noreferrer"&gt;listing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smithery&lt;/td&gt;
&lt;td&gt;&lt;a href="https://smithery.ai/servers/sugra-systems/sugra-api" rel="noopener noreferrer"&gt;@sugra-systems/sugra-api&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;50 req/day - &lt;a href="https://app.sugra.ai/register?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp_launch_202607" rel="noopener noreferrer"&gt;register&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MIT license. Every tool result carries source attribution and freshness - not a black box.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep the catalog honest as domains grow&lt;/li&gt;
&lt;li&gt;Dual-host OAuth hardening for &lt;code&gt;mcp.sugra.ai&lt;/code&gt; alongside &lt;code&gt;app.sugra.ai/mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tighter agent evals for search ranking and entity resolution&lt;/li&gt;
&lt;li&gt;More runnable recipes in the public cookbook&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build research, compliance, or multi-domain agent workflows and you are tired of re-wiring vendors:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
bash
pip install sugra-api-mcp

• Repo: github.com/Sugra-Systems/sugra-api-mcp (https://github.com/Sugra-Systems/sugra-api-mcp?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp_launch_202607)
• API and free key: sugra.ai (https://sugra.ai?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp_launch_202607)
• Hosted MCP: app.sugra.ai/mcp (https://app.sugra.ai/mcp?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp_launch_202607)

Feedback on the tool surface - especially from people wiring external data into coding agents - is welcome.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>api</category>
      <category>agents</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Why Your CRM Keeps Failing You (And What I Learned Running Sales Solo for Three Years)</title>
      <dc:creator>Nano brain</dc:creator>
      <pubDate>Wed, 22 Jul 2026 15:30:17 +0000</pubDate>
      <link>https://dev.to/nano_brain_afcb626ad0d1c2/why-your-crm-keeps-failing-you-and-what-i-learned-running-sales-solo-for-three-years-2jhg</link>
      <guid>https://dev.to/nano_brain_afcb626ad0d1c2/why-your-crm-keeps-failing-you-and-what-i-learned-running-sales-solo-for-three-years-2jhg</guid>
      <description>&lt;p&gt;I spent three years running growth for an early-stage company where I was, on any given week, the marketer, the salesperson, and the person answering support tickets at 11pm. I tried five different CRMs in that time. Every single one of them was built on the same wrong assumption: that I had a team.&lt;br&gt;
I did not. And if you are reading this, there is a good chance you do not either.&lt;br&gt;
This is not a theoretical argument about tools. It is what I learned the hard way, and what I now understand backed by how these systems are actually built.&lt;br&gt;
The Experience: What It Actually Feels Like to Run Growth Alone&lt;br&gt;
Here is a Tuesday I remember clearly. A prospect replied to a cold email I had sent three weeks earlier. I could not remember which campaign it was from. I opened my CRM: no note. I opened my email: found the thread, but no context on why I had reached out. I checked my content calendar, because I vaguely recalled they had liked a LinkedIn post. Fifteen minutes later I had pieced together enough to reply like I knew what I was talking about.&lt;br&gt;
That fifteen minutes happened multiple times a week, for every lead who took more than one touch to convert. Which, if you have ever sold anything, is most of them.&lt;br&gt;
The tools were not broken individually. The CRM did what CRMs do. The email client did what email clients do. But none of them knew about each other, so the burden of connecting the dots fell entirely on me, every single time. I was the integration layer. That is an exhausting place to live.&lt;br&gt;
The Expertise: Why This Keeps Happening&lt;br&gt;
Once I started paying attention to how CRM software is actually designed, the pattern became obvious. Most systems in this category are modeled on a "funnel and handoff" structure: marketing generates a lead, hands it to sales, sales closes it and hands it to success, success owns the renewal. Each stage gets its own tool because, historically, each stage had its own team and its own budget line.&lt;br&gt;
That model works fine when three different people are actually holding those three roles. It breaks down completely when one person is holding all three, because the handoffs that used to happen between people now have to happen inside a single person's head, across tools that were never designed to talk to each other.&lt;br&gt;
This is not a minor UX complaint. It is a structural mismatch between how the software is organized and how small teams actually operate. You can layer on integrations and Zapier workflows to patch the gaps, and I tried that too, but patched gaps are still gaps. Something always falls through, usually the thing you needed most: context on why this person matters and what happened last time.&lt;br&gt;
The Authority: What the Better Systems Get Right&lt;br&gt;
The fix, when I finally found something that worked, was not a smarter individual tool. It was a change in the underlying unit of the system. Instead of the record being a "lead" that changes shape and location as it moves through stages, the record is the person, and everything, the first LinkedIn comment, the cold email reply, the closed deal, the renewal conversation, lives on that same profile permanently.&lt;br&gt;
This is the model behind Distribution Engine, built by &lt;a href="https://nanobrains.ai/" rel="noopener noreferrer"&gt;Nanobrains&lt;/a&gt;, an engagement CRM designed specifically for the founder or small team doing marketing, sales, and retention without a department for each. A universal profile holds the whole relationship in one place: identity, channel history, notes from every stage, and a live read on how warm or cold the relationship currently is. Discovery, content publishing, messaging across email, LinkedIn, and WhatsApp, and re-engagement all write to and read from that same record.&lt;br&gt;
The busywork that used to eat my time, finding the right people to reach out to, turning one idea into platform-specific posts, tracking who has gone quiet, runs through agents that handle the repetitive parts and surface what needs a human decision. I still make every real call. I am just no longer the one manually stitching six tools together to figure out what is even happening.&lt;br&gt;
The Trust: What I'd Tell You to Actually Check&lt;br&gt;
I am not going to pretend one tool fixes every problem in solo sales and marketing, because it does not. Here is what I'd actually verify before you commit to anything, this system included:&lt;br&gt;
Does the tool keep one record per person across every stage, or does it still split "leads" from "contacts" from "customers" the moment they convert? That split is where context dies.&lt;br&gt;
Can you see, at a glance, when you last touched a relationship and how long it has been? Warm leads go cold quietly, and the tools that surface this before you notice on your own are doing something genuinely useful.&lt;br&gt;
Are the automated parts, discovery, drafting, warming reminders, actually saving you decision time, or are they just moving the busywork to a different screen? Automation that still requires you to review, edit, and approve everything is not automation. It is a to-do list with extra steps.&lt;br&gt;
I ask these questions because I lost real deals to cold relationships I forgot existed, and I do not want anyone else to learn that lesson the expensive way. A CRM built around one person, one record, from first contact to renewal, is not a nice-to-have feature. For a team of one to three people, it is the difference between running your pipeline and your pipeline running you.&lt;br&gt;
That is the problem Nanobrains set out to solve with Distribution Engine, and it is why I hold it to a higher bar than I would hold a typical SaaS tool. If you are doing this work alone or with one or two other people, it is worth seeing whether it holds up against your own Tuesday afternoons.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>python</category>
      <category>api</category>
    </item>
    <item>
      <title>Enhance Your JPG Images with Ellipse Annotations Using Java REST API</title>
      <dc:creator>Shahzad Ashraf</dc:creator>
      <pubDate>Wed, 22 Jul 2026 15:27:51 +0000</pubDate>
      <link>https://dev.to/shahzad_ashraf_5ea18e2b2d/enhance-your-jpg-images-with-ellipse-annotations-using-java-rest-api-8al</link>
      <guid>https://dev.to/shahzad_ashraf_5ea18e2b2d/enhance-your-jpg-images-with-ellipse-annotations-using-java-rest-api-8al</guid>
      <description>&lt;p&gt;Adding ellipse annotations to JPG images can be surprisingly straightforward with the right tools. The GroupDocs.Annotation Cloud SDK for Java empowers you to annotate images efficiently via a REST API, making complex image manipulation accessible and fast. This approach eliminates the need for heavy local processing, allowing developers to focus on core functionalities instead of tedious image-editing tasks.&lt;/p&gt;

&lt;p&gt;The SDK provides a simple and intuitive way to implement annotations, supporting various file formats. By leveraging REST API principles, developers can easily integrate image annotation features into their applications without the overhead of complex setups. The cloud-based nature of the SDK means you can work from anywhere, ensuring flexibility in your development process.&lt;/p&gt;

&lt;p&gt;If you're ready to enhance your image handling capabilities, check out the working code examples in the article to get started. Dive into the documentation and elevate your projects with powerful annotation features.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kb.groupdocs.cloud/annotation/java/add-ellipse-annotations-to-jpg-images-using-java-rest-api/" rel="noopener noreferrer"&gt;https://kb.groupdocs.cloud/annotation/java/add-ellipse-annotations-to-jpg-images-using-java-rest-api/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>api</category>
      <category>automation</category>
      <category>java</category>
    </item>
    <item>
      <title>Unleash Your APIs: Why Swagger UI is a Game-Changer for Developers!</title>
      <dc:creator>GitHubOpenSource</dc:creator>
      <pubDate>Wed, 22 Jul 2026 15:01:05 +0000</pubDate>
      <link>https://dev.to/githubopensource/unleash-your-apis-why-swagger-ui-is-a-game-changer-for-developers-3p2k</link>
      <guid>https://dev.to/githubopensource/unleash-your-apis-why-swagger-ui-is-a-game-changer-for-developers-3p2k</guid>
      <description>&lt;h2&gt;
  
  
  Quick Summary: 📝
&lt;/h2&gt;

&lt;p&gt;Swagger UI is a JavaScript-based tool that generates interactive API documentation from OpenAPI specifications. It allows developers and consumers to visualize and interact with API resources without needing to implement the API logic, making API exploration and integration significantly easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways: 💡
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;✅ Swagger UI generates interactive and visually appealing API documentation from your OpenAPI Specification.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ It allows developers to visualize, understand, and even test API endpoints directly in the browser.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ Documentation is automatically kept up-to-date with your API definition, preventing discrepancies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ It significantly improves collaboration between backend and frontend teams by providing a clear API contract.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ Available as various npm packages (&lt;code&gt;swagger-ui&lt;/code&gt;, &lt;code&gt;swagger-ui-dist&lt;/code&gt;, &lt;code&gt;swagger-ui-react&lt;/code&gt;) for flexible integration into different project types.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Project Statistics: 📊
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;Stars:&lt;/strong&gt; 28917&lt;/li&gt;
&lt;li&gt;🍴 &lt;strong&gt;Forks:&lt;/strong&gt; 9263&lt;/li&gt;
&lt;li&gt;❗ &lt;strong&gt;Open Issues:&lt;/strong&gt; 1284&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack: 💻
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ JavaScript&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hey fellow developers! Have you ever found yourself wrestling with cryptic API documentation, trying to figure out what endpoints do what, or what kind of data you're supposed to send? We've all been there, and it can be a real productivity killer. But what if I told you there's a fantastic open-source project that makes exploring and interacting with APIs an absolute breeze? Prepare to be amazed by Swagger UI!&lt;/p&gt;

&lt;p&gt;Swagger UI is nothing short of brilliant. It's a powerful tool that takes your OpenAPI (formerly Swagger) Specification and transforms it into stunning, interactive, and human-readable API documentation right in your browser. Imagine a beautifully designed web page where every single one of your API's resources is laid out clearly. You can see all the available endpoints, understand their parameters, view example requests and responses, and even make live API calls directly from the UI! It's like having a sandbox for your API, without writing a single line of client-side code to test it.&lt;/p&gt;

&lt;p&gt;The beauty of Swagger UI lies in its simplicity and automation. Once you have your OpenAPI Specification defined (which describes your API's structure), Swagger UI automatically generates this interactive documentation. This means your documentation is always up-to-date with your API's definition, eliminating the dreaded problem of outdated or incorrect docs. It acts as a single source of truth, making life easier for everyone involved.&lt;/p&gt;

&lt;p&gt;For backend developers, it's a fantastic way to validate your API design and ensure it's easily consumable. For frontend developers, it's an invaluable resource for quickly understanding how to integrate with the backend, reducing guesswork and speeding up development cycles. Collaboration between teams becomes seamless because everyone is looking at the same, clear, and executable documentation. No more endless back-and-forth trying to clarify API contracts!&lt;/p&gt;

&lt;p&gt;Swagger UI is also incredibly versatile in how you can integrate it into your projects. You can use the &lt;code&gt;swagger-ui&lt;/code&gt; npm module for modern single-page applications, &lt;code&gt;swagger-ui-dist&lt;/code&gt; if you need a dependency-free bundle for server-side projects, or even &lt;code&gt;swagger-ui-react&lt;/code&gt; if you're building a React application. This flexibility means there's almost certainly a way to get this incredible tool working for you. Seriously, if you're building or consuming APIs, you absolutely need to check out Swagger UI. It's a massive time-saver and a huge boost to developer experience!&lt;/p&gt;

&lt;h2&gt;
  
  
  Learn More: 🔗
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/swagger-api/swagger-ui" rel="noopener noreferrer"&gt;View the Project on GitHub&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🌟 Stay Connected with GitHub Open Source!
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;📱 &lt;strong&gt;Join us on Telegram&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Get daily updates on the best open-source projects&lt;br&gt;&lt;br&gt;
&lt;a href="https://t.me/GitHub_Open_Source" rel="noopener noreferrer"&gt;GitHub Open Source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👥 &lt;strong&gt;Follow us on Facebook&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Connect with our community and never miss a discovery&lt;br&gt;&lt;br&gt;
&lt;a href="https://www.facebook.com/people/GitHub-Open-Source/61571925474856/" rel="noopener noreferrer"&gt;GitHub Open Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>api</category>
      <category>openapi</category>
      <category>swagger</category>
      <category>developer</category>
    </item>
    <item>
      <title>Amazon Scraping Tools Compared — Which One Should You Use in 2026?</title>
      <dc:creator>Nick Davies</dc:creator>
      <pubDate>Wed, 22 Jul 2026 14:59:17 +0000</pubDate>
      <link>https://dev.to/nick_davies_323125afbb05c/amazon-scraping-tools-compared-which-one-should-you-use-in-2026-4gkh</link>
      <guid>https://dev.to/nick_davies_323125afbb05c/amazon-scraping-tools-compared-which-one-should-you-use-in-2026-4gkh</guid>
      <description>&lt;p&gt;If you need to scrape Amazon data, there are multiple tools available. But which one should you pick? I compared the top 5 options.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Developer&lt;/th&gt;
&lt;th&gt;Rating&lt;/th&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;Pricing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Product Scraper&lt;/td&gt;
&lt;td&gt;Junglee&lt;/td&gt;
&lt;td&gt;4.4&lt;/td&gt;
&lt;td&gt;20K&lt;/td&gt;
&lt;td&gt;Pay per event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Scraper&lt;/td&gt;
&lt;td&gt;Junglee&lt;/td&gt;
&lt;td&gt;5.0&lt;/td&gt;
&lt;td&gt;14K&lt;/td&gt;
&lt;td&gt;Pay per event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Reviews Scraper&lt;/td&gt;
&lt;td&gt;Junglee&lt;/td&gt;
&lt;td&gt;3.2&lt;/td&gt;
&lt;td&gt;11K&lt;/td&gt;
&lt;td&gt;Pay per event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Bestsellers Scraper&lt;/td&gt;
&lt;td&gt;Junglee&lt;/td&gt;
&lt;td&gt;5.0&lt;/td&gt;
&lt;td&gt;4K&lt;/td&gt;
&lt;td&gt;Pay per event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon ASINs Scraper&lt;/td&gt;
&lt;td&gt;Junglee&lt;/td&gt;
&lt;td&gt;5.0&lt;/td&gt;
&lt;td&gt;2K&lt;/td&gt;
&lt;td&gt;Pay per event&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Detailed Breakdown
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Amazon Product Scraper
&lt;/h3&gt;

&lt;p&gt;Use this Amazon scraper to collect data based on URL and country from the Amazon website. Extract product information without using the Amazon API, including reviews, prices, descriptions, and Amazon &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Users:&lt;/strong&gt; 20K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rating:&lt;/strong&gt; 4.4/5&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; Pay per event&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://apify.com/junglee/Amazon-crawler?fpr=97nrp4" rel="noopener noreferrer"&gt;Try it on Apify&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Amazon Scraper
&lt;/h3&gt;

&lt;p&gt;Gets you product data from Amazon. Unofficial API. Scrapes and downloads product information without using the Amazon API, including reviews, prices, descriptions, and ASIN.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Users:&lt;/strong&gt; 14K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rating:&lt;/strong&gt; 5.0/5&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; Pay per event&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://apify.com/junglee/free-amazon-product-scraper?fpr=97nrp4" rel="noopener noreferrer"&gt;Try it on Apify&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Amazon Reviews Scraper
&lt;/h3&gt;

&lt;p&gt;Amazon scraper to extract reviews from Amazon products. Scrape and download detailed reviews without using the Amazon API, including rating score, review description, reactions and images. Download yo&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Users:&lt;/strong&gt; 11K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rating:&lt;/strong&gt; 3.2/5&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; Pay per event&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://apify.com/junglee/amazon-reviews-scraper?fpr=97nrp4" rel="noopener noreferrer"&gt;Try it on Apify&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Amazon Bestsellers Scraper
&lt;/h3&gt;

&lt;p&gt;Scrape the Amazon Best Sellers categories and extract details on top 100 most popular items on Amazon. Download product name, price, URL, and thumbnail image. Best used on .com, .co.uk, .de, .fr, .es,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Users:&lt;/strong&gt; 4K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rating:&lt;/strong&gt; 5.0/5&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; Pay per event&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://apify.com/junglee/amazon-bestsellers?fpr=97nrp4" rel="noopener noreferrer"&gt;Try it on Apify&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Amazon ASINs Scraper
&lt;/h3&gt;

&lt;p&gt;Gets you product data from Amazon. Unofficial API. Scrapes and downloads product information without using the Amazon API, including reviews, prices, descriptions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Users:&lt;/strong&gt; 2K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rating:&lt;/strong&gt; 5.0/5&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; Pay per event&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://apify.com/junglee/amazon-asins-scraper?fpr=97nrp4" rel="noopener noreferrer"&gt;Try it on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;All tools are available on &lt;a href="https://www.apify.com/?fpr=97nrp4" rel="noopener noreferrer"&gt;Apify&lt;/a&gt; with $5/month in free credits for new accounts.&lt;/p&gt;

&lt;p&gt;Full directory: &lt;a href="https://github.com/p32nicky/apify-actors-directory" rel="noopener noreferrer"&gt;26,000+ tools on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>automation</category>
      <category>tools</category>
    </item>
    <item>
      <title>Transcription Webhooks &amp; Callbacks: The Complete Guide</title>
      <dc:creator>Mart Schweiger</dc:creator>
      <pubDate>Wed, 22 Jul 2026 14:56:03 +0000</pubDate>
      <link>https://dev.to/martschweiger/transcription-webhooks-callbacks-the-complete-guide-2nk9</link>
      <guid>https://dev.to/martschweiger/transcription-webhooks-callbacks-the-complete-guide-2nk9</guid>
      <description>&lt;p&gt;If you're polling GET /v2/transcript/{id} every few seconds waiting for a transcript to finish, you're burning requests and adding latency for no reason. AssemblyAI can just call you back. Set one field, expose one endpoint, and you get a POST the moment transcription completes.&lt;/p&gt;

&lt;p&gt;This guide covers the whole path—not just "set webhook_url," but the part most docs skip: what the payload actually contains, how to verify the request is really from us, and how retries behave when your server hiccups. If you're building anything production-grade on top of &lt;a href="https://www.assemblyai.com/products/speech-to-text" rel="noopener noreferrer"&gt;speech-to-text&lt;/a&gt;, read to the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Polling vs. webhooks: when to use which&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Both approaches answer the same question—"is my transcript done yet?"—but they trade off differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polling&lt;/strong&gt; means repeatedly calling GET /v2/transcript/{id} (say, every 3 seconds) until status comes back completed or error. It's dead simple, needs no public endpoint, and works from a script on your laptop. The cost is wasted requests and a delay equal to your polling interval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhooks&lt;/strong&gt; flip the direction. You register a URL, AssemblyAI POSTs to it the instant the job finishes, and you do zero waiting. The tradeoff: you need a publicly reachable endpoint that returns a 2xx within 10 seconds, plus a little plumbing to secure and verify it.&lt;/p&gt;

&lt;p&gt;Rule of thumb: prototypes and CLI tools poll; backend services that process audio at any real volume use webhooks. If you're running a queue of transcription jobs behind a web service, webhooks are the obvious choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Enabling a webhook&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;You enable a webhook by setting webhook_url in the body of your POST /v2/transcript request against the base URL &lt;a href="https://api.assemblyai.com" rel="noopener noreferrer"&gt;https://api.assemblyai.com&lt;/a&gt;. Auth is a plain authorization header with your API key—no Bearer prefix.&lt;/p&gt;

&lt;p&gt;Here it is with curl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;curl&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//api.assemblyai.com/v2/transcript \&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization: &amp;lt;YOUR_API_KEY&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type: application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{
    "audio_url": "https://example.com/audio.mp3",
    "webhook_url": "https://your-app.com/webhooks/transcript"
  }&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Python, build a TranscriptionConfig, call set_webhook(url), and submit the job with submit()—not transcribe(). Using submit() matters: it returns immediately instead of blocking until the transcript is ready, which is the entire point of a webhook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;assemblyai&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;aai&lt;/span&gt;

&lt;span class="n"&gt;aai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;YOUR_API_KEY&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TranscriptionConfig&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;set_webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://your-app.com/webhooks/transcript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;transcriber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transcriber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transcriber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/audio.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Submitted &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. We&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ll get a callback when it&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s done.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AssemblyAI&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assemblyai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AssemblyAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;YOUR_API_KEY&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transcripts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/audio.mp3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;webhook_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://your-app.com/webhooks/transcript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Submitted &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole setup. Full field reference lives in the &lt;a href="https://www.assemblyai.com/docs/api-reference/transcripts/submit" rel="noopener noreferrer"&gt;submit endpoint docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What the payload actually contains&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the gotcha that trips up almost everyone. When transcription finishes, AssemblyAI POSTs to your URL with a &lt;strong&gt;minimal&lt;/strong&gt; payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transcript_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-transcript-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;status is either completed or error. That's it.&lt;/p&gt;

&lt;p&gt;The payload does &lt;strong&gt;not&lt;/strong&gt; contain the transcript text. It does &lt;strong&gt;not&lt;/strong&gt; contain the error details either. To get the words—or to read the error field when something failed—you have to call GET /v2/transcript/{transcript_id} yourself. The webhook is a notification, not a delivery of the result.&lt;/p&gt;

&lt;p&gt;So your receiver's job is really two steps: catch the notification, then go fetch the actual transcript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;requests&lt;/span&gt;

&lt;span class="nx"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_transcript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transcript_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.assemblyai.com/v2/transcript/{transcript_id}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you internalize one thing from this article, make it this: the webhook tells you &lt;em&gt;when&lt;/em&gt;, the GET tells you &lt;em&gt;what&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Building a receiver endpoint&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Let's put it together into a complete, working receiver. Here's a Python/Flask service that validates the custom auth header, checks the status, fetches the transcript on success, and reads the error field on failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abort&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;WEBHOOK_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_transcript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcript_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.assemblyai.com/v2/transcript/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transcript_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/webhooks/transcript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_webhook&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Verify the request via your custom auth header.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-My-Webhook-Secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;silent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;transcript_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transcript_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;transcript_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Malformed request—4xx tells AssemblyAI to stop retrying.
&lt;/span&gt;        &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Branch on status. Fetch the real data with a GET.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_transcript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcript_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Transcript &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transcript_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ready: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# ... enqueue downstream work, write to your DB, etc.
&lt;/span&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_transcript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcript_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Transcript &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transcript_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# ... alert, retry the job, flag the customer record, etc.
&lt;/span&gt;
    &lt;span class="c1"&gt;# 3. Return 2xx fast so AssemblyAI marks delivery successful.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the shape of it. Verify first, respond fast, and keep heavy work out of the request path. If fetching and processing the transcript is slow, push it onto a background queue and return 200 immediately—you've only got a 10-second budget, which we'll get to.&lt;/p&gt;

&lt;p&gt;The same structure works in Node/Express:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WEBHOOK_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/transcript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-My-Webhook-Secret&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;transcript_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;transcript_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Respond first, then do the work off the request path.&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.assemblyai.com/v2/transcript/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;transcript_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Ready: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;...`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Securing your webhook&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;There's no HMAC or signature scheme here—AssemblyAI doesn't sign the payload. So don't go looking for a signature header to validate. Instead, you get three complementary controls, and you should use more than one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom auth header.&lt;/strong&gt; Attach a secret header when you submit the job, and check it on every incoming request. Set webhook_auth_header_name and webhook_auth_header_value in the submit body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;curl&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//api.assemblyai.com/v2/transcript \&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization: &amp;lt;YOUR_API_KEY&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type: application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{
    "audio_url": "https://example.com/audio.mp3",
    "webhook_url": "https://your-app.com/webhooks/transcript",
    "webhook_auth_header_name": "X-My-Webhook-Secret",
    "webhook_auth_header_value": "secret-value"
  }&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Python, pass the extra arguments to set_webhook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TranscriptionConfig&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;set_webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://your-app.com/webhooks/transcript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-My-Webhook-Secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secret-value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AssemblyAI will send that header back on the webhook request, and your receiver rejects anything without the right value—that's the abort(401) in the example above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source-IP allow-list.&lt;/strong&gt; Webhook requests originate from fixed IPs: 44.238.19.20 in the US and 54.220.25.36 in the EU. Restrict your endpoint to those addresses at the firewall or load balancer for a second layer of defense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;webhook_status_code.&lt;/strong&gt; After delivery, the transcript object carries a webhook_status_code field—the HTTP status code AssemblyAI received from your endpoint. Pull it with a GET to confirm your receiver actually returned what you think it did. It's the fastest way to debug a webhook that "isn't firing" (spoiler: it usually is, and your endpoint returned a 500).&lt;/p&gt;

&lt;p&gt;Use the auth header and the IP allow-list together. Neither is a signature, but combined they're a solid perimeter.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Reliability and retries&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Your endpoint has to return a 2xx status within &lt;strong&gt;10 seconds&lt;/strong&gt;. That's the contract.&lt;/p&gt;

&lt;p&gt;If you don't—timeout, 5xx, connection refused—AssemblyAI retries. You get up to &lt;strong&gt;10 total attempts&lt;/strong&gt;, spaced &lt;strong&gt;10 seconds apart&lt;/strong&gt;. That gives your service a bit under two minutes to recover from a transient blip without losing the notification.&lt;/p&gt;

&lt;p&gt;One sharp edge: a 4xx response marks the delivery as failed and &lt;strong&gt;stops retries immediately&lt;/strong&gt;. The reasoning is that 4xx means "your request is wrong and will always be wrong"—retrying won't help. So return 4xx only for genuinely bad requests (a missing transcript_id, a failed auth check). If your database is momentarily down, return a 5xx so the retry machinery kicks in and you get another shot.&lt;/p&gt;

&lt;p&gt;This is exactly why the receiver examples respond 200 before doing the heavy lifting. Fetching the transcript, running downstream analysis, writing to storage—none of that should happen inside the 10-second window. Acknowledge fast, process async.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Adding metadata via query params&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The webhook payload only carries transcript_id and status, so how do you know which customer or order a given callback belongs to? Put it in the URL. AssemblyAI POSTs to your webhook_url exactly as you supplied it, query string and all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TranscriptionConfig&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;set_webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://your-app.com/webhooks/transcript?customer_id=1234&amp;amp;order_id=5678&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the callback lands, read those params off the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customer_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the clean way to correlate deliveries and do things like per-customer usage tracking without a lookup table keyed on transcript_id. Just don't put secrets in the query string—keep those in the auth header.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Testing locally&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Your receiver needs a public URL, which is awkward on localhost. Use a tunneling tool—ngrok, Cloudflare Tunnel, or similar—to expose your local port to the internet, then submit a job pointing webhook_url at the tunnel's HTTPS address.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ngrok&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Use&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;forwarding&lt;/span&gt; &lt;span class="nc"&gt;URL &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//abc123.ngrok.io/webhooks/transcript)&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;your&lt;/span&gt; &lt;span class="nx"&gt;webhook_url&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt; &lt;span class="nx"&gt;submitting&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Submit a short audio file, watch the request hit your terminal, and confirm you return 200. If nothing arrives, GET the transcript and check webhook_status_code—it'll tell you whether AssemblyAI reached your endpoint and what it got back.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A note on streaming webhooks&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Everything above is for pre-recorded (async) transcription. If you're working with real-time audio, streaming has its own webhook mechanism—see the &lt;a href="https://www.assemblyai.com/docs/streaming/webhooks" rel="noopener noreferrer"&gt;streaming webhooks docs&lt;/a&gt;. Don't mix the two; the payloads and lifecycle differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Next steps&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Webhooks turn transcription from something you wait on into something that notifies you. Set webhook_url, verify the request, respond fast, and fetch the transcript with a GET—that's a production-grade integration.&lt;/p&gt;

&lt;p&gt;From here, dig into the &lt;a href="https://www.assemblyai.com/docs/pre-recorded-audio/webhooks" rel="noopener noreferrer"&gt;pre-recorded audio webhooks guide&lt;/a&gt; for the canonical reference, or read up on &lt;a href="https://www.assemblyai.com/blog/speech-to-text" rel="noopener noreferrer"&gt;what speech-to-text is&lt;/a&gt; and &lt;a href="https://www.assemblyai.com/blog/how-accurate-speech-to-text" rel="noopener noreferrer"&gt;how accurate it is in 2026&lt;/a&gt; if you're still evaluating models.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.assemblyai.com/docs/api-reference/overview" rel="noopener noreferrer"&gt;&lt;strong&gt;View the full API reference&lt;/strong&gt;&lt;/a&gt; for every field on the transcript object.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Frequently asked questions&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Does AssemblyAI support webhooks?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Yes. Set webhook_url in the body of your POST /v2/transcript request (or use set_webhook() in the Python SDK / webhook_url in the JS submit() call), and AssemblyAI will POST to that URL when transcription finishes. It's the recommended alternative to polling for backend services.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What's in the webhook payload?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Just two fields: transcript_id and status (completed or error). The payload does not include the transcript text or error details. To get the actual result—or the error field on failure—call GET /v2/transcript/{transcript_id}.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How do I secure and verify webhook requests?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;There's no HMAC signature. Use a custom auth header (webhook_auth_header_name and webhook_auth_header_value) that your receiver checks on every request, restrict your endpoint to AssemblyAI's source IPs (44.238.19.20 in the US, 54.220.25.36 in the EU), and inspect the webhook_status_code field on the transcript to confirm what your endpoint returned.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What happens if my endpoint is down when a webhook fires?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Your endpoint must return a 2xx within 10 seconds. If it doesn't, AssemblyAI retries up to 10 total attempts, 10 seconds apart. A 4xx response marks the delivery failed and stops retries immediately, so reserve 4xx for genuinely bad requests and return 5xx for transient errors you want retried.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Should I use webhooks or polling?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Polling (GET /v2/transcript/{id} every few seconds) is simplest and needs no public endpoint—good for scripts and prototypes. Webhooks push a notification the instant a job finishes with no wasted requests, but require a publicly reachable endpoint that returns 2xx in 10 seconds. Use webhooks for production backend services.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Can I attach my own metadata to a webhook?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Yes. Add query parameters to your webhook_url, like ?customer_id=1234&amp;amp;order_id=5678. AssemblyAI POSTs to the URL exactly as you supplied it, so you read those params off the incoming request to correlate deliveries. Keep secrets in the auth header, not the query string.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webhooks</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Transcribe Audio From a Mobile App (iOS, Android)</title>
      <dc:creator>Mart Schweiger</dc:creator>
      <pubDate>Wed, 22 Jul 2026 14:55:56 +0000</pubDate>
      <link>https://dev.to/martschweiger/how-to-transcribe-audio-from-a-mobile-app-ios-android-16pn</link>
      <guid>https://dev.to/martschweiger/how-to-transcribe-audio-from-a-mobile-app-ios-android-16pn</guid>
      <description>&lt;p&gt;If you searched for an AssemblyAI mobile SDK, here's the short version: there isn't one. The official SDKs are &lt;a href="https://www.assemblyai.com/docs" rel="noopener noreferrer"&gt;Python&lt;/a&gt; (pip install assemblyai) and &lt;a href="https://www.assemblyai.com/docs" rel="noopener noreferrer"&gt;JavaScript/TypeScript&lt;/a&gt; (npm install assemblyai). No Swift, no Kotlin, no React Native package.&lt;/p&gt;

&lt;p&gt;That's not the problem you think it is. The bigger constraint is security: &lt;strong&gt;don't ship your API key to client-side code.&lt;/strong&gt; A mobile binary can be decompiled, and anyone who pulls your key out of it can spend your account balance. So even if a native SDK existed, calling AssemblyAI directly from a phone would be the wrong move.&lt;/p&gt;

&lt;p&gt;The right pattern is a thin backend you control. Your mobile app records audio and sends it to your server. Your server holds the API key, talks to AssemblyAI over the REST API, and returns plain text. That's the whole lesson, and it's a good one—you get key security, request logging, rate limiting, and the freedom to swap models later without shipping a new app build.&lt;/p&gt;

&lt;p&gt;Here's what you'll build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A backend (Node or Python) that receives an audio file, uploads it to AssemblyAI, creates a transcript, polls for the result, and returns the text.&lt;/li&gt;
&lt;li&gt;Three thin clients—Swift, Kotlin, and React Native—that record audio and call &lt;em&gt;your&lt;/em&gt; backend, never AssemblyAI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Prerequisites&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An AssemblyAI account and API key. &lt;a href="https://www.assemblyai.com/dashboard/signup" rel="noopener noreferrer"&gt;Get your free API key&lt;/a&gt; from the &lt;a href="https://www.assemblyai.com/dashboard/home" rel="noopener noreferrer"&gt;dashboard&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Node 18+ or Python 3.8+ for the backend.&lt;/li&gt;
&lt;li&gt;Xcode for iOS, Android Studio for Android, or a React Native toolchain—whichever platforms you're targeting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're new to the API, the &lt;a href="https://www.assemblyai.com/docs/pre-recorded-audio/getting-started/transcribe-an-audio-file" rel="noopener noreferrer"&gt;transcribe an audio file quickstart&lt;/a&gt; covers the async flow end to end. This tutorial adapts it for mobile.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Architecture overview&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One rule shapes everything: the API key lives on the server, and only the server talks to AssemblyAI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="err"&gt;┌──────────────┐&lt;/span&gt;        &lt;span class="err"&gt;┌──────────────────────┐&lt;/span&gt;        &lt;span class="err"&gt;┌─────────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="nx"&gt;Mobile&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;  &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="nx"&gt;Your&lt;/span&gt; &lt;span class="nx"&gt;backend&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="nx"&gt;AssemblyAI&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Swift&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;holds&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="nx"&gt;REST&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="nx"&gt;Kotlin&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;                      &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;                 &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="nx"&gt;RN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;                      &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;                 &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│──&lt;/span&gt;&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="err"&gt;─▶│&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;transcribe&lt;/span&gt;     &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;                 &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="nx"&gt;file&lt;/span&gt;  &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;v2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;upload&lt;/span&gt; &lt;span class="err"&gt;─┼──&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="err"&gt;─▶&lt;/span&gt; &lt;span class="nx"&gt;upload_url&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;v2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="err"&gt;──&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="err"&gt;─▶&lt;/span&gt; &lt;span class="nx"&gt;transcript&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;  &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;v2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;──&lt;/span&gt;&lt;span class="nx"&gt;poll&lt;/span&gt;&lt;span class="err"&gt;──▶&lt;/span&gt; &lt;span class="nx"&gt;completed&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│◀─&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="err"&gt;──│&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;     &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;                 &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;└──────────────┘&lt;/span&gt;        &lt;span class="err"&gt;└──────────────────────┘&lt;/span&gt;        &lt;span class="err"&gt;└─────────────────┘&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what's &lt;em&gt;not&lt;/em&gt; in the diagram: the mobile client never sees the API key and never has AssemblyAI's URL. It only knows about your endpoint. If you rotate the key or change models, the app doesn't change.&lt;/p&gt;

&lt;p&gt;Why a backend proxy specifically? For pre-recorded audio there's no temporary-token mechanism—temporary tokens exist only for &lt;a href="https://www.assemblyai.com/docs/streaming/authenticate-with-a-temporary-token" rel="noopener noreferrer"&gt;streaming&lt;/a&gt;. So for file transcription, the key has to live somewhere trusted, and that's your server.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The AssemblyAI async flow&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Everything the backend does is three REST calls against &lt;a href="https://api.assemblyai.com" rel="noopener noreferrer"&gt;https://api.assemblyai.com&lt;/a&gt;. Auth is your API key in the authorization header—no Bearer prefix.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upload (optional):&lt;/strong&gt; POST /v2/upload with the raw audio bytes as the body. Returns { "upload_url": "..." }. Skip this if your audio already lives at a public URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create transcript:&lt;/strong&gt; POST /v2/transcript with JSON { "audio_url": "..." }. Returns a transcript object with an id and a status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get result:&lt;/strong&gt; GET /v2/transcript/{id}. Poll every few seconds until status is completed or error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The status lifecycle is queued → processing → completed (or error).&lt;/p&gt;

&lt;p&gt;One gotcha that bites people on the upload step: the body must be &lt;strong&gt;raw bytes&lt;/strong&gt;, not JSON. If you send a JSON envelope, the upload appears to succeed but transcription fails later with "Transcoding failed." Send the file bytes directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Backend implementation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Pick your language. Both versions expose a single POST /transcribe endpoint that accepts a multipart file upload from the mobile client and returns { "text": "..." }. Both read the API key from an environment variable—never hardcode it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Node (Express)&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="nx"&gt;multer&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your_key_here&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;multer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;multer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;multer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;multer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memoryStorage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.assemblyai.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Poll until the transcript is done or errors out.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pollTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v2/transcript/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// poll every 3s&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/transcribe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Upload the raw audio bytes. Body is the buffer, NOT JSON.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uploadRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v2/upload`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;upload_url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;uploadRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Create the transcript.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v2/transcript`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;audio_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;upload_url&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;createRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Poll for the result and return just the text.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pollTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Listening on :8080&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Python (FastAPI)&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uvicorn[standard]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;multipart&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_key_here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UploadFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.assemblyai.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;poll_transcript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcript_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/v2/transcript/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transcript_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# poll every 3s
&lt;/span&gt;
&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/transcribe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UploadFile&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;audio_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# 1. Upload the raw bytes. data=audio_bytes sends them as-is, not JSON.
&lt;/span&gt;        &lt;span class="n"&gt;upload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/v2/upload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;audio_bytes&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;upload_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upload_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="c1"&gt;# 2. Create the transcript.
&lt;/span&gt;        &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/v2/transcript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;upload_url&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;transcript_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="c1"&gt;# 3. Poll and return the text.
&lt;/span&gt;        &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;poll_transcript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcript_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with uvicorn main:app --port 8080.&lt;/p&gt;

&lt;p&gt;A few production notes that apply to both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polling vs. webhooks.&lt;/strong&gt; Polling every 3 seconds is simple and fine for short clips. For longer audio, hold the HTTP connection open only if your infra tolerates it—otherwise return the transcript id immediately and let the client poll a GET /transcribe/{id} route, or register a webhook so AssemblyAI notifies you when the job finishes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limits.&lt;/strong&gt; The upload endpoint accepts files up to 2.2 GB. The transcript request body caps at 5 GB, and audio duration must be between 160ms and 10 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add features here, not on the client.&lt;/strong&gt; Want speaker labels or summarization? Add the options to the /v2/transcript JSON body server-side. The app doesn't change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For every field you can pass, see the &lt;a href="https://www.assemblyai.com/docs/api-reference/transcripts/submit" rel="noopener noreferrer"&gt;submit transcript reference&lt;/a&gt;, the &lt;a href="https://www.assemblyai.com/docs/api-reference/files/upload" rel="noopener noreferrer"&gt;upload reference&lt;/a&gt;, and the &lt;a href="https://www.assemblyai.com/docs/api-reference/transcripts/get" rel="noopener noreferrer"&gt;get transcript reference&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Client implementations&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;All three clients do the same three things: record audio to a file, POST that file to your backend as multipart form data, and show the returned text. None of them know AssemblyAI exists.&lt;/p&gt;

&lt;p&gt;Set BACKEND_URL to wherever your server runs. Use &lt;a href="http://localhost:8080" rel="noopener noreferrer"&gt;http://localhost:8080&lt;/a&gt; for a simulator, or your machine's LAN IP for a physical device.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;iOS / Swift&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Record with AVAudioRecorder, then upload with URLSession. Add NSMicrophoneUsageDescription to your Info.plist first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AVFoundation&lt;/span&gt;

&lt;span class="nx"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Transcriber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NSObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AVAudioRecorderDelegate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AVAudioRecorder&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;backendURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:8080/transcribe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;lazy&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;fileURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;FileManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;temporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendingPathComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recording.m4a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="nx"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;startRecording&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AVAudioSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sharedInstance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCategory&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="nx"&gt;playAndRecord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nx"&gt;AVFormatIDKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kAudioFormatMPEG4AAC&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nx"&gt;AVSampleRateKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;AVNumberOfChannelsKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;recorder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="nc"&gt;AVAudioRecorder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fileURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;stopAndTranscribe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;audioData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contentsOf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fileURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;boundary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Boundary-&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;(UUID().uuidString)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;URLRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;backendURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpMethod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;multipart/form-data; boundary=&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;(boundary)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;forHTTPHeaderField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Build the multipart body under the "audio" field name.&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;(boundary)&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;using&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Disposition: form-data; name=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;audio&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; filename=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;recording.m4a&lt;/span&gt;&lt;span class="se"&gt;\"\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;using&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type: audio/m4a&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;using&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s2"&gt;--&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;(boundary)--&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;using&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;let &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;URLSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="nc"&gt;JSONDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TranscriptResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;struct&lt;/span&gt; &lt;span class="nx"&gt;TranscriptResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Decodable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API key anywhere. The client's entire world is backendURL.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Android / Kotlin&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Record with MediaRecorder, upload with OkHttp. Declare RECORD_AUDIO in your manifest and request it at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;android&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;media&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MediaRecorder&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;okhttp3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MediaType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Companion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toMediaType&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;okhttp3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MultipartBody&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;okhttp3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OkHttpClient&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;okhttp3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;okhttp3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RequestBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Companion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;asRequestBody&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JSONObject&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;File&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Transcriber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="nx"&gt;cacheDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OkHttpClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="nx"&gt;backendUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://10.0.2.2:8080/transcribe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;// 10.0.2.2 = host from emulator&lt;/span&gt;
    &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="nx"&gt;outputFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recording.m4a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

    &lt;span class="nx"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;startRecording&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;recorder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;setAudioSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AudioSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MIC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;setOutputFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OutputFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MPEG_4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;setAudioEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AudioEncoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AAC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;setAudioChannels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;setOutputFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;absolutePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;stopAndTranscribe&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;recorder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

        &lt;span class="c1"&gt;// Multipart upload under the "audio" field name.&lt;/span&gt;
        &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MultipartBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MultipartBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FORM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addFormDataPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recording.m4a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nx"&gt;outputFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asRequestBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio/m4a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMediaType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backendUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call stopAndTranscribe() off the main thread (a coroutine on Dispatchers.IO works well).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;React Native&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Record with a library like react-native-audio-recorder-player, then upload with the built-in fetch and FormData:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AudioRecorderPlayer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native-audio-recorder-player&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recorder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioRecorderPlayer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BACKEND_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:8080/transcribe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startRecording&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startRecorder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// returns the file path&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stopAndTranscribe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stopRecorder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recording.m4a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio/m4a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BACKEND_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// don't set Content-Type; fetch sets the multipart boundary&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because JavaScript is a first-class citizen here, you might be tempted to reach for the assemblyai npm package inside your React Native app. Don't. The Node SDK is built for server environments, and using it on the client means embedding your API key in the bundle—the exact thing we're avoiding. The fetch-to-your-backend pattern above is the correct one for React Native.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Testing and validation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Test the backend on its own before wiring up a phone. Record or grab any short audio file and hit the endpoint with curl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;curl&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;X&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:8080/transcribe \&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;F&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio=@sample.m4a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should get back {"text":"..."} within a few seconds for a short clip. If you don't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Transcoding failed"&lt;/strong&gt; almost always means the upload body wasn't raw bytes. Confirm you're sending the buffer/bytes directly, not a JSON wrapper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;401 Unauthorized&lt;/strong&gt; means the authorization header is missing or wrong. Check that ASSEMBLYAI_API_KEY is set in the server's environment—and remember, no Bearer prefix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The request hangs&lt;/strong&gt; on long audio because polling holds the connection open. Switch to the return-id-and-poll pattern or a webhook.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once curl works, point the app at the backend. On a physical device, localhost won't resolve to your dev machine—use its LAN IP (for example &lt;a href="http://192.168.1.20:8080/transcribe" rel="noopener noreferrer"&gt;http://192.168.1.20:8080/transcribe&lt;/a&gt;), and on Android the emulator reaches the host at 10.0.2.2.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Realtime from mobile (next step)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If you need live captions instead of file-at-a-time transcription, AssemblyAI has a streaming API over WebSocket at wss://streaming.assemblyai.com/v3/ws. There's still no mobile-native streaming SDK, but streaming has something file transcription doesn't: short-lived tokens.&lt;/p&gt;

&lt;p&gt;Your backend calls GET /v3/token?expires_in_seconds=60 (tokens last 1–600s and are single-use), hands the token to the app, and the app opens the WebSocket with the token as a token query param. The key still never leaves your server; the token is safe to give the client because it expires fast and works once. Audio goes up as mono 16-bit PCM.&lt;/p&gt;

&lt;p&gt;That flow is documented in &lt;a href="https://www.assemblyai.com/docs/streaming/authenticate-with-a-temporary-token" rel="noopener noreferrer"&gt;authenticate with a temporary token&lt;/a&gt;. For a working streaming client to model your backend on, see &lt;a href="https://www.assemblyai.com/blog/real-time-transcription-python" rel="noopener noreferrer"&gt;real-time transcription in Python&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Next steps&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;You now have a mobile transcription pipeline that keeps your API key where it belongs. From here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add speech understanding.&lt;/strong&gt; Speaker diarization, summarization, and more are options on the /v2/transcript body—turn them on server-side and your app gets them for free. Start with &lt;a href="https://www.assemblyai.com/blog/speech-to-text" rel="noopener noreferrer"&gt;what is speech-to-text&lt;/a&gt; and the &lt;a href="https://www.assemblyai.com/products/speech-to-text" rel="noopener noreferrer"&gt;Speech-to-Text product page&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harden the backend.&lt;/strong&gt; Add authentication on your /transcribe route, rate limiting, and request logging—the proxy is the natural place for all of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explore every parameter.&lt;/strong&gt; The &lt;a href="https://www.assemblyai.com/docs/api-reference/overview" rel="noopener noreferrer"&gt;full API reference&lt;/a&gt; documents the complete transcript object.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Frequently asked questions&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Is there an official AssemblyAI iOS or Android SDK?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No. The only official SDKs are Python (pip install assemblyai) and JavaScript/TypeScript (npm install assemblyai). For mobile, call the REST API at &lt;a href="https://api.assemblyai.com" rel="noopener noreferrer"&gt;https://api.assemblyai.com&lt;/a&gt; from a backend you control.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Can I call the AssemblyAI API directly from my mobile app?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Technically the REST API works from anywhere, but you shouldn't. Calling it directly means embedding your API key in the app binary, where it can be extracted. Route every request through your own backend instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How do I keep my API key safe in a mobile app?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Never ship it to the client. Store it as an environment variable on your server, have the app send audio to your backend, and let the backend attach the authorization header when it calls AssemblyAI. That's the architecture in this tutorial.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Does AssemblyAI support React Native?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;There's no React Native SDK. Use fetch with FormData to send audio to your backend, exactly like the web. Avoid the assemblyai npm package on the client—it's a server SDK and would expose your key.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Can I do realtime transcription on mobile?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Yes, over the streaming WebSocket at wss://streaming.assemblyai.com/v3/ws. There's no mobile streaming SDK, so your backend mints a one-time token via GET /v3/token?expires_in_seconds=60 and the app connects with it. Audio is mono 16-bit PCM. See the &lt;a href="https://www.assemblyai.com/docs/streaming/authenticate-with-a-temporary-token" rel="noopener noreferrer"&gt;temporary token docs&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What are the file size and duration limits?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The upload endpoint accepts files up to 2.2 GB. The transcript request body caps at 5 GB, and audio must run between 160ms and 10 hours.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mobile</category>
      <category>speechtotext</category>
      <category>api</category>
    </item>
    <item>
      <title>AI Medical Scribe: Build vs Buy Nuance DAX &amp; Abridge</title>
      <dc:creator>Mart Schweiger</dc:creator>
      <pubDate>Wed, 22 Jul 2026 14:55:23 +0000</pubDate>
      <link>https://dev.to/martschweiger/ai-medical-scribe-build-vs-buy-nuance-dax-abridge-53hk</link>
      <guid>https://dev.to/martschweiger/ai-medical-scribe-build-vs-buy-nuance-dax-abridge-53hk</guid>
      <description>&lt;p&gt;Every healthcare AI team lands on the same question eventually. You want ambient clinical documentation in your product, and there are two obvious paths. Buy a finished scribe like Nuance DAX Copilot or Abridge and wire it in. Or build your own.&lt;/p&gt;

&lt;p&gt;Here's the reframe most comparison posts miss. DAX and Abridge are finished products. They're good ones. If you're a health system that wants a scribe in clinicians' hands next quarter and you're never going to differentiate on documentation itself, buying is often the right call. Full stop.&lt;/p&gt;

&lt;p&gt;But if you're a product team building a clinical application, a specialty EHR, or an ambient scribe you plan to sell, the "which scribe do I buy" question is the wrong one. You're not shopping for a scribe. You're deciding whether documentation is a feature you own or a feature you rent. And if you build, the question becomes: what do you build on?&lt;/p&gt;

&lt;p&gt;That's the fork. Let's walk it honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The build-vs-buy fork for ambient clinical documentation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When you buy DAX or Abridge, you get a lot the same day: a working scribe, a note format clinicians already recognize, EHR integrations, a compliance posture, and a vendor who owns the roadmap. That's real value, and pretending otherwise is a mistake.&lt;/p&gt;

&lt;p&gt;What you give up is control. The note format is theirs. The specialties they support are theirs. The pace of improvement is theirs. And the economics are per-seat, per-provider, forever—which means your gross margin on documentation is capped by someone else's price list.&lt;/p&gt;

&lt;p&gt;When you build, you flip every one of those. You own the note structure, the specialty coverage, the UX, the model choices underneath, and the margin. You also own the work: accurate medical transcription, speaker separation, note generation, and compliance. That's not a weekend project. But none of it is mysterious anymore, and most of the hard part isn't where teams expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What "buy" actually costs you&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The sticker price is the easy part. The real costs show up later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Margin.&lt;/strong&gt; Per-seat pricing scales with your clinician count, not your efficiency. Get 10,000 providers on the platform and your documentation cost grows linearly right alongside. You can't engineer that number down because you don't own the stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Control over the note.&lt;/strong&gt; Cardiology notes and behavioral health notes shouldn't look the same. If your differentiation is a note tuned to a specialty or a workflow, a general-purpose scribe fights you on it. You file feature requests and wait.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock-in.&lt;/strong&gt; Once your workflows, integrations, and clinician muscle memory are built around a vendor's output format, switching is a migration project, not a config change. That's leverage—theirs, not yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Differentiation.&lt;/strong&gt; If you and three competitors all ship the same underlying scribe, documentation stops being a reason anyone chooses you. It becomes table stakes you're paying a premium to rent.&lt;/p&gt;

&lt;p&gt;None of this makes buying wrong. It makes buying a strategic decision about where your product's value actually lives. If documentation isn't your edge, rent it and move on. If it is—or could be—renting your core is a strange way to build a company.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What "build" actually requires&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;An ambient scribe is a pipeline, and it's shorter than it looks. Four layers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Medical speech-to-text.&lt;/strong&gt; The audio-to-text layer. Multi-speaker clinical audio, drug names, procedures, dosages, accents, background noise. This is the input to everything downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speaker diarization.&lt;/strong&gt; Who said what. You need clinician and patient separated cleanly, including the rapid back-and-forth and interruptions of a real visit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Note generation.&lt;/strong&gt; An LLM turns the diarized transcript into a structured note—SOAP, H&amp;amp;P, whatever your specialty needs. This is where teams love to spend their time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance.&lt;/strong&gt; A BAA, PHI handling, access controls, audit trails.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the part that surprises people. The LLM layer, the part everyone's excited about, is the most commoditized. Strong models are a few API calls away, and prompt engineering a good SOAP note is tractable. The layer that quietly decides whether your scribe is trustworthy is the first one—the &lt;a href="https://www.assemblyai.com/products/speech-to-text" rel="noopener noreferrer"&gt;speech-to-text&lt;/a&gt; foundation. Garbage transcript in, confident-sounding garbage note out. An LLM will happily summarize a hallucinated dosage into a clean, plausible sentence a clinician might sign.&lt;/p&gt;

&lt;p&gt;So the build decision is really a decision about your transcription layer. Get that right and the rest of the pipeline has something solid to stand on.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The speech-to-text layer is make-or-break&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is the input that determines everything, so it's worth being specific about what "good enough for clinical" means.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medical accuracy that isn't generic.&lt;/strong&gt; General speech-to-text mangles clinical vocabulary—that's the whole problem. AssemblyAI's &lt;a href="https://www.assemblyai.com/blog/universal-3-5-pro-async" rel="noopener noreferrer"&gt;Universal-3.5 Pro&lt;/a&gt; is the flagship async model at $0.21/hr, and you turn on medical accuracy by adding one parameter: "domain": "medical-v1". That's Medical Mode, +$0.15/hr, so the flagship plus Medical Mode runs $0.36/hr combined. It reduces the Missed Entity Rate on drugs, conditions, procedures, and clinical terms by roughly 20%. No model switch, no separate pipeline—one param on the model you're already calling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context that adapts to the patient.&lt;/strong&gt; Medical Mode gets you baseline clinical accuracy. Contextual prompting gets you the last mile. Prime the model with a patient's prior-visit note and it knows what to listen for. In an internal healthcare test, feeding a prior-visit note cut missed medical terms by 31%—even when the note was from an earlier visit. That's the kind of gain you can't buy off a shelf because it depends on data only your application has.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diarization built in.&lt;/strong&gt; Universal-3.5 Pro produces the transcript and the speaker turns together—the most accurate diarization we've shipped, optimized for cpWER and tuned for the short turns, rapid exchanges, and overlapped speech of an actual exam room. You're not bolting on a separate speaker model and hoping the timestamps line up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A streaming option when you need it.&lt;/strong&gt; Most ambient scribes are async—record the visit, generate the note. But if you're building live documentation, a real-time assistant, or clinician-facing prompts mid-visit, &lt;a href="https://www.assemblyai.com/blog/universal-3-5-pro-realtime" rel="noopener noreferrer"&gt;Universal-3.5 Pro Realtime&lt;/a&gt; offers Medical Mode too, with live diarization. Same foundation, real-time shape. That flexibility matters when your roadmap outgrows batch processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Languages.&lt;/strong&gt; Medical Mode supports English, Spanish, German, and French—so a Spanish-language visit gets clinical-grade transcription, not a downgrade.&lt;/p&gt;

&lt;p&gt;Compare that to buying: with DAX or Abridge, the transcription layer is a black box. You can't tune it, you can't prompt it with your data, and you can't fix it when it's wrong on a term that matters to your specialty. Building on a foundation you control means the accuracy ceiling is yours to raise. If you want the deeper argument for why this input dominates outcomes, we made it in &lt;a href="https://www.assemblyai.com/blog/true-cost-of-inaccurate-transcription" rel="noopener noreferrer"&gt;the true cost of inaccurate transcription&lt;/a&gt; and in why &lt;a href="https://www.assemblyai.com/blog/transcription-accuracy-vs-transcription-quality" rel="noopener noreferrer"&gt;transcription accuracy and transcription quality&lt;/a&gt; aren't the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Compliance, done right&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Compliance is where "build" teams get nervous, and fair enough—PHI is not the place to improvise. The good news is that the transcription layer of your stack can carry a clean posture.&lt;/p&gt;

&lt;p&gt;AssemblyAI enables covered entities and their business associates subject to HIPAA to use the AssemblyAI services to process protected health information (PHI). AssemblyAI is considered a business associate under HIPAA, and we offer a Business Associate Addendum (BAA) that is required under HIPAA to ensure that AssemblyAI appropriately safeguards PHI.&lt;/p&gt;

&lt;p&gt;Practically, that BAA is available to sign without booking a sales call—so a compliance review doesn't turn into a procurement saga before you've written a line of code. For teams with data-residency requirements, EU processing is available too. Compliance is table stakes, but it shouldn't be the thing that slows your build to a crawl.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Build vs buy: the decision at a glance&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the comparison that actually matters—not a spec smackdown, but the strategic trade-offs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision criterion&lt;/th&gt;
&lt;th&gt;Buy (DAX / Abridge)&lt;/th&gt;
&lt;th&gt;Build (on AssemblyAI)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Control over note format &amp;amp; UX&lt;/td&gt;
&lt;td&gt;Vendor-defined; feature requests&lt;/td&gt;
&lt;td&gt;Fully yours — tune per specialty and workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gross margin on documentation&lt;/td&gt;
&lt;td&gt;Capped by per-seat pricing&lt;/td&gt;
&lt;td&gt;Yours to engineer; usage-based inputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-to-market&lt;/td&gt;
&lt;td&gt;Fastest — finished product&lt;/td&gt;
&lt;td&gt;Longer, but the STT layer is an API call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Differentiation&lt;/td&gt;
&lt;td&gt;Shared with everyone on the same scribe&lt;/td&gt;
&lt;td&gt;Documentation becomes a product edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance ownership&lt;/td&gt;
&lt;td&gt;Vendor's posture&lt;/td&gt;
&lt;td&gt;Your posture; BAA available on the STT layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost model&lt;/td&gt;
&lt;td&gt;Per-seat, per-provider, recurring&lt;/td&gt;
&lt;td&gt;Pay-as-you-go on transcription ($0.36/hr with Medical Mode)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roadmap &amp;amp; pace of improvement&lt;/td&gt;
&lt;td&gt;Vendor's priorities&lt;/td&gt;
&lt;td&gt;Yours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Orgs that won't differentiate on docs&lt;/td&gt;
&lt;td&gt;Product teams selling or owning the experience&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Who each path is for&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Buy DAX or Abridge if:&lt;/strong&gt; you're a health system or clinic that needs a working scribe in clinicians' hands fast, documentation isn't part of your competitive story, and you'd rather pay per seat than staff an engineering team against it. That's a legitimate, common, correct choice. These are mature products for exactly this buyer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build if:&lt;/strong&gt; documentation is part of what you sell, you need note formats or specialty coverage a general scribe won't give you, per-seat economics break your margin at scale, or you're already a software company and owning the stack is how you compete. In that case, the smart move isn't building transcription from scratch—it's building on a transcription layer that's already solved medical accuracy, diarization, and compliance, so your team spends its time on the note logic and product experience that actually differentiate you.&lt;/p&gt;

&lt;p&gt;Healthcare teams building their own documentation experiences tend to land in the second camp for the same reason: they want the margin and the control, and they don't want to reinvent speech-to-text to get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A quick decision checklist&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Run your project through these before committing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is documentation a feature we sell, or a chore we want handled? If we sell it, lean build.&lt;/li&gt;
&lt;li&gt;Do we need note formats or specialties a general scribe won't support? If yes, lean build.&lt;/li&gt;
&lt;li&gt;What does per-seat pricing do to our margin at 1,000 and 10,000 providers? If it breaks, lean build.&lt;/li&gt;
&lt;li&gt;Do we have (or want) engineering ownership of this workflow? If not, lean buy.&lt;/li&gt;
&lt;li&gt;How fast do we need to ship? If it's this quarter and docs aren't our edge, lean buy.&lt;/li&gt;
&lt;li&gt;If we build, is our transcription layer clinically accurate, diarized, and BAA-backed on day one? If not, fix that first—it's the foundation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're weighing the broader landscape, our guides on &lt;a href="https://www.assemblyai.com/blog/how-to-use-speech-ai-for-healthcare-market-research" rel="noopener noreferrer"&gt;Voice AI for healthcare market research&lt;/a&gt; and &lt;a href="https://www.assemblyai.com/blog/conversation-intelligence" rel="noopener noreferrer"&gt;conversation intelligence&lt;/a&gt; show how the same speech foundation extends well past the exam room. For the numbers behind the models, the &lt;a href="https://www.assemblyai.com/benchmarks" rel="noopener noreferrer"&gt;benchmarks&lt;/a&gt; and our take on &lt;a href="https://www.assemblyai.com/blog/how-accurate-speech-to-text" rel="noopener noreferrer"&gt;how accurate speech-to-text is in 2026&lt;/a&gt; are the receipts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.assemblyai.com/products" rel="noopener noreferrer"&gt;&lt;strong&gt;Explore Voice AI solutions&lt;/strong&gt;&lt;/a&gt; to see the full platform under a scribe you own—or dig into the &lt;a href="https://www.assemblyai.com/solutions/medical" rel="noopener noreferrer"&gt;medical solution&lt;/a&gt; directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The insight most teams learn too late&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the thing nobody tells you at the start. Build vs buy isn't a one-time decision—it's a decision that gets more expensive to reverse the longer you wait. The team that buys "to move fast" and plans to build later usually can't—by then their workflows, integrations, and clinician habits are welded to a vendor's format. And the team that builds on a weak transcription layer finds the ceiling only after shipping, when clinicians stop trusting the notes.&lt;/p&gt;

&lt;p&gt;So the real move is to decouple the decision. Even if you buy a finished scribe today, own the layer that's hardest to swap later—your speech-to-text foundation—so building stays a live option instead of a fantasy. Accuracy compounds. Lock-in compounds. Pick the one you want on your side.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Frequently asked questions&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Should I build my own AI medical scribe or buy Nuance DAX or Abridge?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Buy if you need a finished scribe fast and documentation isn't your competitive edge—DAX and Abridge are mature products built for that. Build if documentation is part of what you sell, you need custom note formats or specialty coverage, per-seat pricing breaks your margin at scale, or you want to own the experience. If you build, don't build transcription from scratch—build on a medical-grade speech-to-text layer and spend your effort on note logic and UX.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Can I use AssemblyAI with PHI under HIPAA, and is a BAA available?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;AssemblyAI enables covered entities and their business associates subject to HIPAA to use the AssemblyAI services to process protected health information (PHI). AssemblyAI is considered a business associate under HIPAA, and we offer a Business Associate Addendum (BAA) that is required under HIPAA to ensure that AssemblyAI appropriately safeguards PHI. The BAA is available to sign without a sales call.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How accurate is AssemblyAI on medical terminology?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Universal-3.5 Pro with Medical Mode ("domain": "medical-v1") reduces the Missed Entity Rate on drugs, conditions, procedures, and clinical terms by roughly 20%. Contextual prompting goes further—feeding a patient's prior-visit note cut missed medical terms by 31% in an internal healthcare test, even when the note came from an earlier visit.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What does it cost to build a scribe on AssemblyAI?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Transcription is pay-as-you-go, billed per second with no minimums. Universal-3.5 Pro async is $0.21/hr, and Medical Mode adds $0.15/hr—so the flagship plus Medical Mode is $0.36/hr combined. That's a usage-based input cost you control, versus the recurring per-seat pricing of a finished scribe.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Which languages does Medical Mode support?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;English, Spanish, German, and French. Medical Mode is available on Universal-3.5 Pro (async) and on Universal-3.5 Pro Realtime for streaming use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How do I get started?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Add "domain": "medical-v1" to a Universal-3.5 Pro request and run it against your own clinical audio. Sign the BAA when you're ready to process PHI, and check the &lt;a href="https://www.assemblyai.com/solutions/medical" rel="noopener noreferrer"&gt;medical solution page&lt;/a&gt; for the full picture.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>healthcare</category>
      <category>api</category>
    </item>
    <item>
      <title>Best Voice Agent API for Startups: A First-Build Guide</title>
      <dc:creator>Mart Schweiger</dc:creator>
      <pubDate>Wed, 22 Jul 2026 14:54:57 +0000</pubDate>
      <link>https://dev.to/martschweiger/best-voice-agent-api-for-startups-a-first-build-guide-3eik</link>
      <guid>https://dev.to/martschweiger/best-voice-agent-api-for-startups-a-first-build-guide-3eik</guid>
      <description>&lt;p&gt;You've decided to build a voice product. Maybe it's a support agent, maybe an AI companion, maybe a voice layer on the app you already shipped. Either way, you're staring at a blank editor and a very fast-moving landscape, and the question in front of you isn't "which model is most accurate." It's "how do I get a working demo by Friday?"&lt;/p&gt;

&lt;p&gt;If that's you, this guide is for you. Most people building their first voice product aren't voice agent power users. Roughly 80% are net-new to this. You're demo-driven, you're code-forward, and you don't want to spend your first week learning someone's proprietary conversation-design UI. So let's skip the theory and talk about the decision you're actually making.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What a voice agent API bundles, and why the pipeline is the hard part&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A voice agent is a loop. Someone talks, your system transcribes it (speech-to-text), an LLM decides what to say, and a text-to-speech engine says it back. STT to LLM to TTS. That's the whole pipeline.&lt;/p&gt;

&lt;p&gt;Sounds simple. It isn't.&lt;/p&gt;

&lt;p&gt;The hard part isn't any single box in that diagram. It's the seams between them. When does the user stop talking? (Harder than it sounds - people pause mid-thought all the time.) What happens when they interrupt the agent? How do you keep latency low enough that the conversation doesn't feel like a walkie-talkie? And here's the one that quietly sinks most first builds: if the transcription is wrong, the LLM responds to the wrong thing. Garbage in, confident garbage out.&lt;/p&gt;

&lt;p&gt;That last point matters more than founders expect. Your LLM prompt can be perfect, your TTS voice can sound gorgeous, but if the STT hears "Karen" as "Aaron" or drops the last four digits of an account number, your agent is now confidently wrong. Accuracy at the input is the foundation everything else sits on. If you want the deeper version of this argument, we wrote about &lt;a href="https://www.assemblyai.com/blog/new-2026-insights-report-what-actually-makes-a-good-voice-agent" rel="noopener noreferrer"&gt;what actually makes a good voice agent&lt;/a&gt; - the short version is that the listening layer is where good agents are won or lost.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://www.assemblyai.com/blog/ai-voice-agents" rel="noopener noreferrer"&gt;voice agent API&lt;/a&gt; bundles that whole pipeline - plus turn detection, interruption handling, and voice activity detection - so you don't have to hand-build the seams. That's the category. The question is how you get one.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The three paths for a startup&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;There are basically three ways to ship your first voice product. Each has a real trade-off, and the "right" one depends less on your budget than on how much of the plumbing you want to own.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;What you own&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DIY multi-vendor&lt;/td&gt;
&lt;td&gt;Separate STT, LLM, and TTS providers wired together yourself&lt;/td&gt;
&lt;td&gt;Maximum control, but three vendors, three invoices, and you build turn detection, barge-in, and streaming orchestration&lt;/td&gt;
&lt;td&gt;Teams whose differentiator is the pipeline itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform (Retell, Vapi, etc.)&lt;/td&gt;
&lt;td&gt;A managed environment, often no-code, with built-in telephony&lt;/td&gt;
&lt;td&gt;Fast to launch, but opinionated conversation design, awkward custom integrations, and a real ceiling&lt;/td&gt;
&lt;td&gt;No-code builders and telephony-first products&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One API&lt;/td&gt;
&lt;td&gt;Your system prompt, tools, and conversation logic — over a single connection&lt;/td&gt;
&lt;td&gt;More control than a platform, far less overhead than DIY; you write code&lt;/td&gt;
&lt;td&gt;Code-forward startups shipping a first voice product&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Path 1: DIY multi-vendor.&lt;/strong&gt; You pick an STT provider, an LLM provider, and a TTS provider, and you wire them together yourself. Maximum control. But you're now managing three vendors, three invoices, three sets of rate limits, and three places to debug when a call goes sideways. You also own the hard middle - turn detection, barge-in, streaming orchestration. For a first product, that's a lot of undifferentiated engineering before you've validated that anyone wants the thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path 2: A platform (Retell, Vapi, and similar).&lt;/strong&gt; These give you a managed environment, often with a no-code builder and built-in telephony. Great for getting something live fast without much code. The catch is the ceiling. Platforms are opinionated about conversation design, custom integrations can get awkward, and - candidly - a lot of platform-built agents end up sounding the same because they're built on the same rails. Vapi in particular is orchestration middleware, which means your agent is only as good as the weakest third-party link in its chain. We wrote more about &lt;a href="https://www.assemblyai.com/blog/where-voice-agent-stacks-start-showing-their-limits" rel="noopener noreferrer"&gt;where voice agent stacks start showing their limits&lt;/a&gt; if you want the honest version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path 3: One API.&lt;/strong&gt; A single API that handles STT to LLM to TTS through one connection. You still write your own system prompt, define your own tools, and control the conversation - but you don't manage three vendors or build the streaming plumbing. It's the middle path: more control than a platform, far less overhead than DIY.&lt;/p&gt;

&lt;p&gt;For a first voice product, path 3 is usually the sweet spot. You get to spend your time on product logic - the thing that makes your agent yours - instead of on infrastructure nobody will ever thank you for.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What to actually evaluate&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before you pick, here's the short list of things that genuinely move the needle for a first build. Ignore the spec-sheet noise; these five are what you'll feel in production.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;th&gt;AssemblyAI Voice Agent API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Accuracy&lt;/td&gt;
&lt;td&gt;Wrong transcription means the LLM answers the wrong thing — names, numbers, and addresses break flows&lt;/td&gt;
&lt;td&gt;Built on Universal-3.5 Pro Realtime: 6.99% pooled WER and 15.31% entity error rate on Pipecat's open STT benchmark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;End-to-end round-trip is what makes a conversation feel human&lt;/td&gt;
&lt;td&gt;Around one second end-to-end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developer experience&lt;/td&gt;
&lt;td&gt;Time from "read the docs" to "talking to an agent" is the whole game for a first build&lt;/td&gt;
&lt;td&gt;Standard JSON over one WebSocket, no SDK required; readable in ~10 minutes and works natively with Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing&lt;/td&gt;
&lt;td&gt;Predictable pricing lets you model unit economics before the product exists&lt;/td&gt;
&lt;td&gt;Flat $4.50/hr billed by the minute — STT, LLM, and TTS included; roughly 4x cheaper than OpenAI's Realtime API (~$18/hr)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lock-in&lt;/td&gt;
&lt;td&gt;If you outgrow the tool, how painful is the exit?&lt;/td&gt;
&lt;td&gt;A raw API over a standard WebSocket — you keep full control of conversation design, tools, VAD, and turn timing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Speech accuracy.&lt;/strong&gt; This is the garbage-in problem from earlier. Look at word error rate on &lt;em&gt;real conversational audio&lt;/em&gt;, not clean read-aloud benchmarks, and pay attention to entity accuracy - names, numbers, addresses - because that's what breaks support and booking flows. If you want to go deeper on why the usual metric is shaky, &lt;a href="https://www.assemblyai.com/blog/word-error-rate-is-broken" rel="noopener noreferrer"&gt;word error rate is broken&lt;/a&gt; is a good read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; End-to-end round-trip time is what makes a conversation feel human or robotic. You want something around a second. Don't obsess over shaving milliseconds off one stage; obsess over the full loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer experience and time-to-ship.&lt;/strong&gt; How long from "read the docs" to "talking to a working agent"? For a first-timer this is the whole ballgame. SDK sprawl, dozens of event types, and framework lock-in all add days. A clean JSON API you can read in ten minutes does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing model.&lt;/strong&gt; Is it flat and predictable, or are you doing token math across three invoices plus per-minute STT plus TTS characters? Unpredictable pricing is its own kind of tax when you're trying to model unit economics for a product that doesn't exist yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock-in.&lt;/strong&gt; If you outgrow the tool, how painful is the exit? No-code platforms can be sticky in ways that hurt later. An API you call over a standard WebSocket is a lot easier to walk away from.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How AssemblyAI's Voice Agent API maps to that list&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Full disclosure: this is our product. I'm going to be specific so you can check the claims, not just take my word.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.assemblyai.com/products/voice-agent-api" rel="noopener noreferrer"&gt;Voice Agent API&lt;/a&gt; is one WebSocket API that runs the entire STT to LLM to TTS pipeline. You connect to a socket, stream audio in, and get audio back. You write the system prompt. That's the mental model.&lt;/p&gt;

&lt;p&gt;On the five criteria:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accuracy.&lt;/strong&gt; It's built on &lt;a href="https://www.assemblyai.com/blog/universal-3-5-pro-realtime" rel="noopener noreferrer"&gt;Universal-3.5 Pro Realtime&lt;/a&gt;, our flagship streaming model, which posts a 6.99% pooled word error rate on Pipecat's open STT benchmark of real agent conversations. For comparison on that same benchmark, entity error rate lands at 15.31% - names, places, phone numbers, the stuff that actually breaks agents. It also takes your agent's question as context, so a mumbled one-word reply or a spelled-out account ID resolves correctly instead of getting mangled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; Around one second end-to-end. Fast enough to feel like a conversation, not a transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer experience.&lt;/strong&gt; Standard JSON over a WebSocket. No SDK required, no framework to learn. You can read the &lt;a href="https://www.assemblyai.com/docs/api-reference/overview" rel="noopener noreferrer"&gt;full API reference&lt;/a&gt; in about ten minutes, and most developers have something working the same afternoon. It also works natively with Claude Code - copy the docs, paste them in, and build - which is exactly why we &lt;a href="https://www.assemblyai.com/blog/why-assemblyais-voice-agent-api-is-designed-for-coding-agents" rel="noopener noreferrer"&gt;designed it for coding agents&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's roughly what connecting looks like - one socket, JSON messages, no ceremony:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wss://agents.assemblyai.com/v1/ws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;session.update&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are a friendly booking assistant for a dental clinic.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// stream your audio frames in from here&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also update the system prompt, tools, and settings mid-conversation without reconnecting - handy when the flow branches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing.&lt;/strong&gt; Flat $4.50/hr, billed by the minute, with STT, LLM, and TTS all included. One number, one bill, one set of logs. No reconciling three invoices. For context, that's roughly 4x cheaper than OpenAI's Realtime API (around $18/hr) and comes with a cleaner developer experience - their API has 30-plus event types to reason about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock-in.&lt;/strong&gt; It's a raw API over a standard WebSocket. We call it "invisible infrastructure" for a reason - your customers should feel like you built the thing from scratch, and you keep full control over conversation design, tools, VAD, and turn timing. We're not the agent. We're what you build the agent on.&lt;/p&gt;

&lt;p&gt;It currently supports six languages - English, Spanish, French, German, Italian, and Portuguese - and ships drop-in plugins for LiveKit and Pipecat if you're already using those. When you're ready to go from "hello world" to something real, &lt;a href="https://www.assemblyai.com/blog/how-to-build-with-voice-agent-api" rel="noopener noreferrer"&gt;how to build with the Voice Agent API&lt;/a&gt; walks the full pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;When a platform is the better call&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I said this would be an honest guide, so here's the honest part. A one-API approach is not always right, and sometimes a platform genuinely is the better choice.&lt;/p&gt;

&lt;p&gt;Reach for a platform like Retell or Vapi if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You want no-code.&lt;/strong&gt; If you're a non-technical founder or you need business users to build and tweak agents without touching a WebSocket, a visual builder is worth a lot. Our API assumes you can write code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need built-in telephony today.&lt;/strong&gt; If your product is fundamentally about placing and receiving phone calls and you want that handled out of the box right now, platforms have mature telephony baked in. We're shipping telephony as a fast-follow, but "today" matters when you're shipping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call routing and hosted orchestration are the product.&lt;/strong&gt; If the routing, queueing, and call-center-style workflow &lt;em&gt;is&lt;/em&gt; your value, a platform gives you that scaffolding for free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no shame in starting on a platform to validate demand and moving to an API when you hit the ceiling. Just go in knowing where that ceiling is.&lt;/p&gt;

&lt;p&gt;And if the real question is DIY versus one API? Unless stitching STT, LLM, and TTS together is itself your differentiator, one API replaces three vendors, three invoices, and three debugging surfaces. For a first product, that's almost always the right trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Quickstart and a live demo to close&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the thing about voice products: reading about them tells you very little. Talking to one tells you everything. Accuracy, latency, turn-taking, the feel of an interruption landing cleanly - you catch all of it in about thirty seconds of conversation.&lt;/p&gt;

&lt;p&gt;So the move is simple. Grab a key, wire up the socket, point it at a system prompt, and talk to it. If you can talk to it and it feels right, you're most of the way to a demo.&lt;/p&gt;

&lt;p&gt;The best voice agent API for your first product is the one that gets you to that working demo fastest without boxing you in later. For most code-forward startups, that's a single API with strong accuracy at the input, predictable pricing, and an exit that isn't a trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Frequently asked questions&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What's the best voice agent API for a startup building its first voice product?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The best one is whatever gets you to a working demo fastest without locking you in. For code-forward startups, that usually means a single API that bundles STT, LLM, and TTS over one connection - so you write product logic instead of managing three vendors. AssemblyAI's Voice Agent API fits that profile: one WebSocket, standard JSON, flat pricing, and around one-second end-to-end latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What's the cheapest voice agent API?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Watch out for pricing that looks cheap per stage but adds up across three invoices (STT per minute, LLM per token, TTS per character). A flat rate is easier to model. AssemblyAI's Voice Agent API is $4.50/hr all-in - STT, LLM, and TTS included - which is roughly 4x cheaper than OpenAI's Realtime API at around $18/hr. Cheapest on paper isn't always cheapest in practice; predictability matters when you're modeling unit economics.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;One API or a platform like Retell or Vapi - which should I choose?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Choose a platform if you want no-code building, need telephony out of the box today, or if hosted call routing is your actual product. Choose a single API if you want full control over conversation design and integrations without hitting a platform ceiling, and you're comfortable writing code. Platforms trade complexity for constraints; an API keeps you in control but expects you to build.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How good does the developer experience really need to be?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;For a first voice product, it's the whole game. The gap between an API you can read in ten minutes and one with dozens of event types and required SDKs is measured in days of your life. AssemblyAI's Voice Agent API uses standard JSON over a WebSocket with no SDK required, works natively with Claude Code, and lets you update prompts and tools mid-conversation without reconnecting.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Which languages does the Voice Agent API support?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Six today: English, Spanish, French, German, Italian, and Portuguese. If you need broader coverage for pre-recorded workflows, &lt;a href="https://www.assemblyai.com/blog/universal-3-5-pro-async" rel="noopener noreferrer"&gt;Universal-3.5 Pro&lt;/a&gt; handles native code-switching across 18 languages on the async side.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How do I get started?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Open the &lt;a href="https://www.assemblyai.com/docs/api-reference/overview" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;, connect to the WebSocket, and stream audio at a system prompt. Most developers have a working agent the same afternoon.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>voiceassistant</category>
      <category>api</category>
      <category>startup</category>
    </item>
    <item>
      <title>Speech-to-Text API Fundamentals: Auth, Polling &amp; JSON</title>
      <dc:creator>Mart Schweiger</dc:creator>
      <pubDate>Wed, 22 Jul 2026 14:54:50 +0000</pubDate>
      <link>https://dev.to/martschweiger/speech-to-text-api-fundamentals-auth-polling-json-11h6</link>
      <guid>https://dev.to/martschweiger/speech-to-text-api-fundamentals-auth-polling-json-11h6</guid>
      <description>&lt;p&gt;Every integration with a speech-to-text API comes down to three questions. How do I authenticate? How do I get results out of an async job? And what does the response actually contain? Get those right and everything else — speaker labels, PII redaction, language detection — is just another request option.&lt;/p&gt;

&lt;p&gt;This is the canonical walkthrough. We'll go end-to-end: your first authenticated request, the status lifecycle of a transcription job, a correct polling loop, and a field-by-field tour of the JSON that comes back. Code in curl, Python, and JavaScript throughout.&lt;/p&gt;

&lt;p&gt;If you're new to the space, &lt;a href="https://www.assemblyai.com/blog/speech-to-text" rel="noopener noreferrer"&gt;What is speech-to-text?&lt;/a&gt; and &lt;a href="https://www.assemblyai.com/blog/what-is-asr" rel="noopener noreferrer"&gt;What is Automatic Speech Recognition (ASR)?&lt;/a&gt; give you the conceptual grounding. Here, we're shipping a first successful call.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Overview&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;AssemblyAI's REST API lives at &lt;a href="https://api.assemblyai.com" rel="noopener noreferrer"&gt;https://api.assemblyai.com&lt;/a&gt;. You submit audio, the API transcribes it asynchronously, and you read the result as JSON. There's no streaming to manage and no SDK required for the core flow — though we ship official SDKs that make it a two-liner.&lt;/p&gt;

&lt;p&gt;The whole loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authenticate&lt;/strong&gt; — send your API key in the authorization header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Submit&lt;/strong&gt; — POST /v2/transcript with an audio_url. You get back an id and a status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poll&lt;/strong&gt; — GET /v2/transcript/{id} every few seconds until status is completed or error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse&lt;/strong&gt; — read text, confidence, words[], and friends from the JSON.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's build it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Prerequisites&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An AssemblyAI API key. &lt;a href="https://www.assemblyai.com/dashboard/signup" rel="noopener noreferrer"&gt;Get your free API key&lt;/a&gt; from the &lt;a href="https://www.assemblyai.com/dashboard/home" rel="noopener noreferrer"&gt;dashboard&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A publicly reachable audio file URL (or one you upload — that's a separate endpoint; here we'll use a URL).&lt;/li&gt;
&lt;li&gt;Optionally, one of the official SDKs:&lt;/li&gt;
&lt;li&gt;Python: pip install assemblyai&lt;/li&gt;
&lt;li&gt;JavaScript/TypeScript: npm install assemblyai&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set your key as an environment variable so it never lands in source control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_api_key_here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every example below reads the key from ASSEMBLYAI_API_KEY.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Quick start&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The SDKs collapse submit-and-poll into a single call. Here's the entire flow in each.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;assemblyai&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;aai&lt;/span&gt;

&lt;span class="n"&gt;aai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transcriber&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://assembly.ai/wildfires.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;AssemblyAI&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assemblyai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;const&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AssemblyAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;const&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transcripts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://assembly.ai/wildfires.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the destination. Now let's take apart what's happening underneath, because understanding it is what lets you debug, add features, and use webhooks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;1. Authentication&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;AssemblyAI authenticates with a single header on every request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;authorization: &amp;lt;YOUR_API_KEY&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things trip people up here, so let's be explicit. There's &lt;strong&gt;no Bearer prefix&lt;/strong&gt; — the header value is your raw key, nothing else. And the header goes on &lt;em&gt;every&lt;/em&gt; request, both the POST that submits a job and the GETs that poll it.&lt;/p&gt;

&lt;p&gt;Here's a first authenticated request that lists your account's transcripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;curl&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//api.assemblyai.com/v2/transcript \&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization: $ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your key is valid, you get a 200 and a JSON body. If it's missing or wrong, you get a 401. That's your authentication smoke test before you send any audio.&lt;/p&gt;

&lt;p&gt;Never hardcode the key. Read it from the environment, a secrets manager, or your platform's config store.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Submitting a job and the status lifecycle&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Transcription is asynchronous. You hand the API an audio URL, it returns immediately with a job id, and the work happens in the background. You don't get the transcript in the POST response — you get a receipt.&lt;/p&gt;

&lt;p&gt;Submit with POST /v2/transcript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;curl&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//api.assemblyai.com/v2/transcript \&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization: $ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type: application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"audio_url": "https://assembly.ai/wildfires.mp3"}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is a transcript object. The two fields that matter right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;106993b6-...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;queued&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Persist that id.&lt;/strong&gt; It's how you retrieve the result, and it's how the transcript is addressed for the rest of its life.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The status lifecycle&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A job moves through a small, fixed set of status values. These are the exact strings the API returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;queued&lt;/strong&gt; — waiting to start. In practice you'll typically only see this when you're being rate-limited; otherwise jobs go straight to processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;processing&lt;/strong&gt; — the model is transcribing the audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;completed&lt;/strong&gt; — success. The text field and all the other output fields are now populated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;error&lt;/strong&gt; — the job failed. The object carries an error string explaining why.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;completed and error are the two &lt;strong&gt;terminal&lt;/strong&gt; states. Your job is to poll until you hit one of them. Everything about handling a transcription job is really just handling this state machine correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. Polling correctly&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Since the result isn't in the POST response, you fetch it with GET /v2/transcript/{id} and check the status. Loop, sleep, repeat until terminal.&lt;/p&gt;

&lt;p&gt;The convention: &lt;strong&gt;poll every 3 seconds&lt;/strong&gt;, break on completed (read text), break on error (read error). Three seconds is a sane interval — frequent enough to feel responsive, relaxed enough that you're not hammering the endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;curl&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;TRANSCRIPT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;106993b6-...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;curl&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;silent&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//api.assemblyai.com/v2/transcript/$TRANSCRIPT_ID \&lt;/span&gt;
    &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization: $ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;echo&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;jq&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="nx"&gt;then&lt;/span&gt;
    &lt;span class="nx"&gt;echo&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;jq&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;
  &lt;span class="nx"&gt;elif&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="nx"&gt;then&lt;/span&gt;
    &lt;span class="nx"&gt;echo&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;jq&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;
  &lt;span class="nx"&gt;fi&lt;/span&gt;

  &lt;span class="nx"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="nx"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Python&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.assemblyai.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Submit the job
&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/v2/transcript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://assembly.ai/wildfires.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;transcript_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Submitted: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transcript_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Poll until terminal
&lt;/span&gt;&lt;span class="n"&gt;polling_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/v2/transcript/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transcript_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;polling_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Transcription failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;JavaScript&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASSEMBLYAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.assemblyai.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Submit the job&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v2/transcript`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;audio_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://assembly.ai/wildfires.mp3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Submitted: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Poll until terminal&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pollingUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v2/transcript/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pollingUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Transcription failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three implementations, one pattern: submit, grab the id, poll on a 3-second interval, branch on the terminal states. When you're ready to skip the loop entirely, &lt;a href="https://www.assemblyai.com/docs/api-reference/overview" rel="noopener noreferrer"&gt;webhooks&lt;/a&gt; push the result to you instead — more on that below.&lt;/p&gt;

&lt;p&gt;For the full endpoint contracts, see &lt;a href="https://www.assemblyai.com/docs/pre-recorded-audio/getting-started/transcribe-an-audio-file" rel="noopener noreferrer"&gt;Transcribe an audio file&lt;/a&gt; and &lt;a href="https://www.assemblyai.com/docs/pre-recorded-audio/check-transcript-status" rel="noopener noreferrer"&gt;Check transcript status&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;4. Parsing the JSON response&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once status is completed, the transcript object is fully populated. Here's the canonical shape, trimmed to the essentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;106993b6-...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Smoke from hundreds of wildfires...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;language_code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio_duration&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;282&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confidence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;words&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Smoke&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;640&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confidence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;speaker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's walk the fields you'll actually use.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;id&lt;/strong&gt; — the job identifier. Persist it; you can GET the transcript again anytime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;status&lt;/strong&gt; — where the job is in its lifecycle. On a completed job, completed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;text&lt;/strong&gt; — the full transcript as one string. null until the job completes, so always gate on status first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;confidence&lt;/strong&gt; — an overall confidence score as a float from 0.0 to 1.0. In the example, 0.95.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;audio_duration&lt;/strong&gt; — the length of the audio in &lt;strong&gt;seconds&lt;/strong&gt; (an integer). 282 here means the file is 282 seconds long. This is what you meter usage against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;language_code&lt;/strong&gt; — the detected or specified language, e.g. "en". When you use language detection, you'll also get a language_confidence between 0 and 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;words[]&lt;/strong&gt; — every word as its own object: text, start, end, confidence, and speaker. &lt;strong&gt;The start and end timestamps are in milliseconds.&lt;/strong&gt; So start: 100, end: 640 means that word runs from 0.1s to 0.64s. This is what you build captions, search, and clip-to-timestamp features on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch the units — this is the single most common parsing bug. audio_duration is in &lt;strong&gt;seconds&lt;/strong&gt;; word timestamps are in &lt;strong&gt;milliseconds&lt;/strong&gt;. Mix them up and your captions drift by three orders of magnitude.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Utterances and speakers&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;When you enable speaker_labels or multichannel, the response gains an &lt;strong&gt;utterances[]&lt;/strong&gt; array. Each utterance groups contiguous speech by a single speaker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utterances&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;speaker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Smoke from hundreds of wildfires...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confidence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;words&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Smoke&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;640&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confidence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;speaker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same millisecond timestamps, plus a speaker label and a nested words[]. If you didn't request speaker labels or multichannel, utterances won't be present — so check before you iterate.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;5. Error handling&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Two failure modes, two different responses. Handle them differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP-level errors&lt;/strong&gt; show up as status codes on the request itself. A 400 means the request was malformed — bad JSON, a missing required field, an invalid option. Fix the request; retrying the same payload won't help. A 401 means your authorization header is missing or wrong. Server-side (5xx) errors can be transient — those are safe to resubmit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job-level errors&lt;/strong&gt; show up as status: "error" on a job you already submitted successfully. The request was accepted (200), but transcription failed. Read the &lt;strong&gt;error&lt;/strong&gt; field for the reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;106993b6-...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Download error, unable to download the audio file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common causes of an error status: an unsupported audio format, missing audio, or a URL the API couldn't reach. These are usually input problems — check that your audio_url is public and points at valid audio before resubmitting.&lt;/p&gt;

&lt;p&gt;A resilient loop distinguishes the two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Job&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="nx"&gt;failure&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;inspect&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;likely&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;bad&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;
    &lt;span class="nx"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrap the POST in retry-with-backoff for 5xxs, and never blind-retry a 400.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Next steps&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;You've got the fundamentals. The same submit-poll-parse flow carries every feature — you just add request options to the POST body:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;speaker_labels&lt;/strong&gt; — who spoke when, via the utterances[] array.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;language_detection&lt;/strong&gt; and &lt;strong&gt;language_code&lt;/strong&gt; — auto-detect or pin a language.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;punctuate&lt;/strong&gt; and &lt;strong&gt;format_text&lt;/strong&gt; — control formatting of the output text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;multichannel&lt;/strong&gt; — transcribe each audio channel separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;redact_pii&lt;/strong&gt; and &lt;strong&gt;filter_profanity&lt;/strong&gt; — clean the transcript for compliance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;webhook_url&lt;/strong&gt; — skip polling entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the natural upgrade. Instead of a GET loop, set webhook_url on submission and AssemblyAI POSTs the finished result to your endpoint when the job hits a terminal state. It's the right pattern for production and for high volume. The &lt;a href="https://www.assemblyai.com/docs/api-reference/overview" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; covers the webhook payload and every option in detail.&lt;/p&gt;

&lt;p&gt;From here, explore the full &lt;a href="https://www.assemblyai.com/products/speech-to-text" rel="noopener noreferrer"&gt;speech-to-text product&lt;/a&gt; and the deep dive on &lt;a href="https://www.assemblyai.com/blog/how-accurate-speech-to-text" rel="noopener noreferrer"&gt;how accurate speech-to-text is in 2026&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;View the full API reference&lt;/strong&gt; → &lt;a href="https://www.assemblyai.com/docs/api-reference/overview" rel="noopener noreferrer"&gt;assemblyai.com/docs/api-reference/overview&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Frequently asked questions&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How do I authenticate with the AssemblyAI API?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Send your API key in the authorization header on every request — authorization: . There's no Bearer prefix; the value is your raw key. Store it in an environment variable like ASSEMBLYAI_API_KEY and get your key from the &lt;a href="https://www.assemblyai.com/dashboard/home" rel="noopener noreferrer"&gt;dashboard&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Do I poll for results or get them back immediately?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Transcription is asynchronous, so you poll. POST /v2/transcript returns an id and a status immediately, but not the transcript. Fetch the result with GET /v2/transcript/{id} every 3 seconds until status is completed or error. To avoid polling entirely, set webhook_url on submission and AssemblyAI pushes the result to you.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What do the transcript status values mean?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;There are four: queued (waiting — usually only when rate-limited), processing (actively transcribing), completed (success, read text), and error (failed, read the error string). completed and error are the terminal states you poll toward.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How do I read confidence scores and word timestamps?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;confidence is an overall float from 0.0 to 1.0. Each object in words[] has its own confidence plus start and end timestamps in &lt;strong&gt;milliseconds&lt;/strong&gt;. Note that audio_duration is in &lt;strong&gt;seconds&lt;/strong&gt; while word timestamps are in milliseconds — don't mix the units.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Why is my transcript status error?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The job was accepted but transcription failed. Read the error field for the reason. Common causes are an unsupported audio format, missing audio, or a URL the API couldn't reach. That's different from a 400, which means the request itself was malformed and shouldn't be blindly retried; server (5xx) errors, by contrast, can be resubmitted.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What can I do beyond a basic transcript?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&amp;nbsp;Add request options to the POST body — speaker_labels, language_detection, punctuate, format_text, multichannel, redact_pii, filter_profanity, and webhook_url. The submit-poll-parse flow stays identical. See &lt;a href="https://www.assemblyai.com/docs/api-reference/transcripts/submit" rel="noopener noreferrer"&gt;Submit a transcript&lt;/a&gt; and &lt;a href="https://www.assemblyai.com/docs/api-reference/transcripts/get" rel="noopener noreferrer"&gt;Get a transcript&lt;/a&gt; for full field references.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>speechtotext</category>
      <category>api</category>
    </item>
    <item>
      <title>Open Banking APIs Explained: What PSD2 Compliance Actually Requires From Your Engineering Team</title>
      <dc:creator>Lycore Development</dc:creator>
      <pubDate>Wed, 22 Jul 2026 14:49:22 +0000</pubDate>
      <link>https://dev.to/lycore/open-banking-apis-explained-what-psd2-compliance-actually-requires-from-your-engineering-team-4l2o</link>
      <guid>https://dev.to/lycore/open-banking-apis-explained-what-psd2-compliance-actually-requires-from-your-engineering-team-4l2o</guid>
      <description>&lt;h2&gt;
  
  
  Open Banking APIs Explained: What PSD2 Compliance Actually Requires From Your Engineering Team
&lt;/h2&gt;

&lt;p&gt;Every fintech founder we talk to eventually says some version of the same sentence: "we just need to connect to the bank's API." It sounds like a single integration task on a roadmap. In practice, it's a compliance program with an API attached, and the gap between those two framings is where a lot of engineering timelines quietly blow up.&lt;/p&gt;

&lt;p&gt;We've built open banking integrations across payment initiation, account aggregation, and lending products in the UK and EU markets, plus US equivalents through providers like Plaid. This post is the technical walkthrough we wish more teams had before they scoped their first sprint — what PSD2 actually mandates at the API and architecture level, where the real engineering effort lives, and where teams consistently underestimate the work.&lt;/p&gt;

&lt;h3&gt;
  
  
  What PSD2 actually is, in engineering terms
&lt;/h3&gt;

&lt;p&gt;The Second Payment Services Directive is an EU regulation, not a technical spec. It mandates that banks (Account Servicing Payment Service Providers, or ASPSPs) provide regulated third parties with API access to customer account data and payment initiation, provided the customer has consented. The regulation itself doesn't dictate exact API shapes — that's what the technical standards built on top of it, primarily the Berlin Group's NextGenPSD2 framework and the UK's Open Banking Standard, are for.&lt;/p&gt;

&lt;p&gt;For an engineering team, PSD2 compliance breaks down into three functional API categories you'll be building against or exposing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AISP (Account Information Service Provider) access&lt;/strong&gt; — read-only access to account balances, transaction history, and account holder details, with explicit customer consent that expires and must be renewed (typically every 90 days under RTS rules).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PISP (Payment Initiation Service Provider) access&lt;/strong&gt; — the ability to initiate a payment directly from the customer's bank account, bypassing card networks entirely. This is the piece with the real product upside (lower interchange costs, direct settlement) and the real engineering complexity (strong customer authentication flows, payment status polling, handling partial failures mid-flow).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CBPII (Card-Based Payment Instrument Issuer) access&lt;/strong&gt; — confirmation of funds availability for card-based instruments, a narrower and less commonly implemented category unless you're specifically in the card issuing space.&lt;/p&gt;

&lt;p&gt;If you're building a product that needs bank data or payment initiation, you're consuming these APIs from banks (or from an aggregator abstracting many banks' APIs). If you're a bank or e-money institution being asked to expose these APIs to third parties, you're implementing the provider side. The engineering shape of these problems is almost entirely different, so the first architectural decision is figuring out honestly which side of that line your product sits on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strong Customer Authentication is where most timelines go wrong
&lt;/h3&gt;

&lt;p&gt;The single most underestimated piece of PSD2 work is Strong Customer Authentication (SCA). The regulation requires two-factor authentication for most account access and payment initiation — something the user knows (a PIN, password), something they have (a device, a hardware token), and something they are (biometrics) — with at least two of the three categories represented.&lt;/p&gt;

&lt;p&gt;In practice, SCA in open banking means redirect-based or decoupled authentication flows where your app hands control to the bank's own authentication interface, the user authenticates directly with their bank (not with you — you never see their banking credentials, which is itself a hard compliance requirement), and control returns to your app via a redirect or a webhook-style callback.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Your app calls the bank's consent endpoint, specifying scope (balances, transactions, payments) and redirect URI
2. Bank returns an authorization URL
3. User is redirected to the bank's own login page (outside your app's control entirely)
4. User authenticates with the bank + completes SCA challenge (bank's own 2FA)
5. Bank redirects back to your redirect URI with an authorization code
6. Your backend exchanges the code for an access token + refresh token
7. Subsequent API calls use the access token until it expires or is revoked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sounds straightforward until you build it against a second bank and discover their redirect flow has different timeout behavior, a different set of error codes for user-cancelled consent, and a different token refresh window. There is no single PSD2 API — there is a shared regulatory framework implemented slightly differently by every bank, and your integration layer has to absorb that variance. This is the actual engineering problem, and it's also exactly the layer where aggregators like Plaid, TrueLayer, or Tink earn their fee: they've already absorbed hundreds of these implementation differences so you don't have to build and maintain adapters for each bank individually.&lt;/p&gt;

&lt;p&gt;We generally advise clients to run the numbers honestly on build-vs-aggregate here. Direct bank integration removes a middleman fee and gives you more control, but it means owning an ongoing maintenance burden every time a bank updates its API version, changes its SCA flow, or has downtime — and banks are not known for graceful API versioning. Unless bank integration volume is genuinely core to your product's moat, an aggregator is very often the right call, at least for an initial launch, with a migration path to direct integrations for your highest-volume banks once you have the transaction volume to justify the engineering investment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consent management is a data model problem before it's a UI problem
&lt;/h3&gt;

&lt;p&gt;A recurring mistake we see in early-stage fintech codebases is treating consent as a boolean flag — "has_bank_consent: true/false" — rather than as a first-class, time-bound, scope-specific record. PSD2 consent has real structure that your data model needs to reflect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bank_consents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;institution_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scopes&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- e.g. ['balances', 'transactions', 'payments']&lt;/span&gt;
    &lt;span class="n"&gt;access_token_encrypted&lt;/span&gt; &lt;span class="n"&gt;BYTEA&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;refresh_token_encrypted&lt;/span&gt; &lt;span class="n"&gt;BYTEA&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;granted_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;revoked_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;consent_reference&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;  &lt;span class="c1"&gt;-- the bank's own consent ID, needed for status checks&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expiry field isn't decorative. RTS technical standards require re-authentication at least every 90 days even for an ostensibly "always on" data connection, and your product needs a real UX plan for what happens when a consent lapses mid-session — silently failing background jobs that pull transaction data is a bad default, but so is interrupting a user mid-task with a re-auth flow they weren't expecting. We generally build a proactive re-consent nudge that fires a week before expiry, with fallback handling for consents that lapse anyway.&lt;/p&gt;

&lt;p&gt;Revocation needs equal attention. Users can revoke consent directly with their bank, entirely outside your product's UI, and your system needs to detect and handle that gracefully rather than continuing to hammer a dead token until it starts generating 401s in a retry loop. Most aggregators surface a webhook for this; if you're integrating directly with a bank, check whether their API even offers a revocation notification or whether you're stuck polling token validity — this detail alone has changed which banks we recommend integrating with directly on more than one project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payment initiation needs an honest state machine
&lt;/h3&gt;

&lt;p&gt;Account data access is comparatively forgiving — a failed read can be retried. Payment initiation cannot, and this is where we've seen the most production incidents in fintech codebases we've inherited. A payment initiation request has more terminal and non-terminal states than most teams initially model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING → AUTHORIZED → ACCEPTED_SETTLEMENT_IN_PROCESS → ACCEPTED_SETTLEMENT_COMPLETED
                    ↘ REJECTED
PENDING → CANCELLED (user abandoned SCA)
PENDING → EXPIRED (SCA window timed out)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trap is treating "the API call returned 200" as "the payment happened." A 200 on payment initiation typically means the request was accepted for processing, not that funds moved — actual settlement confirmation comes through a separate status check or webhook, sometimes minutes later, occasionally longer during bank-side processing delays. We've inherited codebases that displayed "Payment Successful" to the user the moment the initiation call returned, only to discover — via a spike in support tickets — that a meaningful percentage of those payments were later rejected by the receiving bank for reasons the initiating API call couldn't have known about.&lt;/p&gt;

&lt;p&gt;The fix is building your product UX and your backend state machine around the real terminal states, not the optimistic ones: show "Payment Processing" rather than "Payment Successful" until you have actual settlement confirmation, build idempotent status-check polling with backoff, and — this is the part teams skip under deadline pressure — decide up front what your product does when a payment sits in an ambiguous state for longer than expected. Silent failure is worse than an honest "we're still confirming this" message.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security requirements that go beyond "use HTTPS"
&lt;/h3&gt;

&lt;p&gt;PSD2's RTS on Strong Customer Authentication and Common Secure Communication includes specific technical requirements that go well past generic API security hygiene:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Qualified certificates.&lt;/strong&gt; Communication with ASPSPs typically requires eIDAS-qualified certificates (QWAC for website authentication, QSEAL for request signing) issued by an accredited Qualified Trust Service Provider — not a standard TLS certificate from a normal CA. Budgeting time and cost for certificate acquisition is a compliance line item, not an engineering afterthought, and the acquisition process itself can take weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request signing.&lt;/strong&gt; Many bank APIs require signed requests (JWS) in addition to TLS, meaning your integration layer needs to build and attach a digital signature to outgoing requests using your QSEAL certificate — an extra step most standard HTTP client libraries don't handle out of the box, and one that needs its own test coverage since a malformed signature fails silently as an auth error that looks identical to a token problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token encryption at rest.&lt;/strong&gt; Access and refresh tokens for bank accounts are, functionally, keys to a customer's financial data. We treat them the way we'd treat any other high-sensitivity secret — encrypted at rest with envelope encryption, never logged (a mistake we've caught in code review more than once, usually in a debug log statement someone forgot to remove), and access-scoped so that a compromised application server doesn't hand over decryption keys alongside the encrypted tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fraud and anomaly monitoring.&lt;/strong&gt; Regulated payment initiation providers have transaction monitoring obligations under PSD2's fraud reporting requirements. Depending on your license type (or your relationship with a licensed partner, if you're operating under someone else's e-money license via an agency model), you may need real-time anomaly detection on payment patterns, not just after-the-fact reporting — this is a genuine data engineering workstream, not a checkbox.&lt;/p&gt;

&lt;h3&gt;
  
  
  The licensing question that determines your whole architecture
&lt;/h3&gt;

&lt;p&gt;Before any of the above, the foundational question is whether you're operating under your own PSD2 license (as an Authorised Payment Institution or Electronic Money Institution) or as an agent/distributor under someone else's license. This isn't primarily an engineering decision, but it determines your engineering scope enormously. Operating as an agent under an existing EMI's license (a common and often sensible path for early-stage fintechs) typically means integrating with their compliance and reporting APIs rather than building your own regulatory reporting pipeline from scratch — a substantial scope reduction that founders sometimes don't realize is available until well into a build.&lt;/p&gt;

&lt;p&gt;We always push clients to nail down licensing strategy with actual regulatory counsel before committing to an architecture, because "should we build our own compliance reporting pipeline" is a multi-month engineering question whose answer depends entirely on a legal decision that has nothing to do with code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building an abstraction layer that survives multiple banks
&lt;/h3&gt;

&lt;p&gt;Whether you're integrating directly with several banks or working through an aggregator that still exposes bank-specific quirks (aggregators smooth over authentication flows, not every data inconsistency), you need an internal abstraction layer that normalizes responses before they touch your product code. Without this, bank-specific field names, date formats, and transaction categorization conventions leak into your business logic, and every new bank integration becomes a scavenger hunt through conditional branches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NormalizedTransaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_source&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;  &lt;span class="c1"&gt;# always minor units, always signed (negative = debit)
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;  &lt;span class="c1"&gt;# ISO 4217
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;  &lt;span class="c1"&gt;# always UTC
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;  &lt;span class="c1"&gt;# normalized to your own taxonomy, not the bank's
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_source&lt;/span&gt;  &lt;span class="c1"&gt;# keep the original payload for debugging/audit
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BankAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_transactions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;NormalizedTransaction&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initiate_payment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment_request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PaymentResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BarclaysAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BankAdapter&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_transactions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_transactions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Data&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Transaction&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="c1"&gt;# bank-specific parsing lives here, never in product code
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern earns its cost the second bank you integrate, not the first — which is exactly why teams under deadline pressure skip it on bank one and then pay for the retrofit on bank three. We treat the adapter layer as non-negotiable scope on any open banking project regardless of how many banks are in the initial launch plan, because "we'll only ever support one bank" is rarely still true eighteen months later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sandbox testing has real limits — plan around them
&lt;/h3&gt;

&lt;p&gt;Every major bank and aggregator provides a sandbox environment, and it's tempting to treat sandbox test coverage as equivalent to production readiness. It isn't. Sandbox environments consistently under-represent the messiness of production: real accounts with years of transaction history and inconsistent categorization, real SCA challenges with real user hesitation and abandonment, and real bank-side downtime during maintenance windows that sandboxes don't simulate.&lt;/p&gt;

&lt;p&gt;We build a deliberate staged rollout for exactly this reason — a small cohort of real users on real bank connections before a full launch, specifically to surface the SCA abandonment rate (which is almost always higher than teams expect; redirecting a user out of your app to their bank's login and back is real friction, and a meaningful percentage of users don't complete the round trip) and the actual distribution of error codes your adapter layer needs to handle gracefully. Budgeting for this staged rollout as its own project phase, rather than treating "sandbox tests pass" as the finish line, has saved every fintech client we've worked with from a rockier production launch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Observability for a system you don't fully control
&lt;/h3&gt;

&lt;p&gt;The hardest part of operating an open banking integration long-term is that a meaningful chunk of your system's behavior is determined by infrastructure you don't own — the bank's API uptime, their SCA flow, their token expiry policy. Standard application monitoring (error rates, latency) still matters, but it needs to be paired with per-institution dashboards, because "our API error rate is elevated" is a much less actionable alert than "Barclays' consent endpoint has been returning elevated 503s for the last twenty minutes." We tag every metric and log line with the institution ID from day one, even when there's only one bank integrated, because retrofitting that tagging later means re-instrumenting a system that's already in production.&lt;/p&gt;

&lt;p&gt;Alert thresholds also need to be bank-aware rather than global. A bank with a smaller user base on your platform will have a noisier error rate on low absolute volume — five failed calls out of ten looks alarming as a percentage but might be a single user's session, not a systemic issue. We set alerting on absolute counts as well as rates specifically to avoid page fatigue from a single small institution's normal variance.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we tell teams starting this today
&lt;/h3&gt;

&lt;p&gt;Open banking integration is genuinely buildable by a competent engineering team — this isn't a warning to avoid it. But it's a compliance-and-architecture problem wearing an API integration costume, and teams that scope it as "connect to the bank API" consistently blow their timelines by a factor of two or three once SCA edge cases, consent lifecycle management, and payment state machines surface mid-build.&lt;/p&gt;

&lt;p&gt;The teams that ship this well start with the licensing and aggregator-vs-direct decisions before writing integration code, model consent and payment state as first-class, time-bound data rather than booleans, and build their UX around the actual terminal states banks return rather than the optimistic ones. Everything past that is solvable engineering work — it's just work that needs to be scoped honestly from day one, not discovered sprint by sprint.&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>api</category>
      <category>security</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
