<?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>Building on Zeeren in 2026: Unified MiniApp Provision, Durable Schedules, and the Composable Skills Layer</title>
      <dc:creator>Lin Zhu</dc:creator>
      <pubDate>Tue, 12 May 2026 14:13:44 +0000</pubDate>
      <link>https://dev.to/lin_zhu_c66f7f4c200d3a186/building-on-zeeren-in-2026-unified-miniapp-provision-durable-schedules-and-the-composable-skills-4146</link>
      <guid>https://dev.to/lin_zhu_c66f7f4c200d3a186/building-on-zeeren-in-2026-unified-miniapp-provision-durable-schedules-and-the-composable-skills-4146</guid>
      <description>&lt;h2&gt;
  
  
  A quick orientation
&lt;/h2&gt;

&lt;p&gt;Zeeren is an agentic overseas-ops platform. The core abstraction is a &lt;strong&gt;Work&lt;/strong&gt; — a persistent, collaborative context that holds a resource tree, runs agent nodes, and owns schedules, MiniApps, and data assets. Think of it like a long-lived project container that agents can read, write, and schedule work into.&lt;/p&gt;

&lt;p&gt;If you're building on top of Zeeren, there are three recent changes worth knowing about.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Unified MiniApp provision: one call instead of 14
&lt;/h2&gt;

&lt;p&gt;The old flow (&lt;code&gt;pre-2026-04&lt;/code&gt;) required chaining 14 discrete API calls to scaffold a MiniApp — create the record, create the resource tree, link the Work, apply migrations, spin the dev container, and more. Any partial failure left you in an inconsistent state.&lt;/p&gt;

&lt;p&gt;The new endpoint, &lt;code&gt;POST /api/v1/miniapps/provision&lt;/code&gt; (gated by &lt;code&gt;MINIAPP_UNIFIED_PROVISION=true&lt;/code&gt;), wraps the entire transaction. One body, one round-trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"project-tracker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Project Tracker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"work_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5821&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"frontend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"backend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python-fastapi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"runtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docker"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data_mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tenant_db"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"projects"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"columns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"nullable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"'open'"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tenant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"visibility"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"private"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response comes back with the dev container's &lt;code&gt;host_port&lt;/code&gt;, a live &lt;code&gt;preview_url&lt;/code&gt;, and the materialised workdir path — everything you need to start generating code immediately.&lt;/p&gt;

&lt;p&gt;A few sharp edges worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SLUG_CONFLICT&lt;/code&gt; (409)&lt;/strong&gt;: append &lt;code&gt;-N&lt;/code&gt; and retry once. The platform won't do this for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;provision_failed&lt;/code&gt; (500)&lt;/strong&gt;: the transaction rolled back cleanly. Same body is safe to retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WORK_ALREADY_AUTHORING&lt;/code&gt; (409)&lt;/strong&gt;: don't retry — the Work already has a MiniApp. Call &lt;code&gt;/miniapps/{id}/status&lt;/code&gt; to find where to resume.&lt;/li&gt;
&lt;li&gt;The platform auto-adds &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;updated_at&lt;/code&gt;. Don't declare them; you'll get a 422.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The legacy 14-action surface still exists for Works provisioned before April 2026, but new Works should use the unified endpoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. WorkSchedules with multi-trigger support
&lt;/h2&gt;

&lt;p&gt;Scheduling in Zeeren is handled through &lt;code&gt;WorkSchedule&lt;/code&gt; records. What's useful is the range of &lt;code&gt;trigger_type&lt;/code&gt; options — it's not just cron:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trigger type&lt;/th&gt;
&lt;th&gt;When to use it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CRON&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fixed calendar cadence: "every weekday at 9am"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INTERVAL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rolling repeat: "every 5 minutes"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WEBHOOK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;External system POSTs to a generated endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EVENT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Internal platform event, e.g. &lt;code&gt;log.error&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CHAT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When a user DMs a Work with a specific prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MQ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kafka/message-queue topic subscriber&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;execution_mode&lt;/code&gt; field controls whether each fire opens a new Work (&lt;code&gt;NEW_WORK&lt;/code&gt;) or appends a node to the existing one (&lt;code&gt;APPEND_NODE&lt;/code&gt;). The latter is the right default when you want the schedule's output to stay in the same conversational context as the Work that owns it.&lt;/p&gt;

&lt;p&gt;A minimal CRON body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"owner_scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"owner_work_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;653&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Daily standup digest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trigger_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CRON"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trigger_config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cron"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0 9 * * 1-5"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"execution_mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"APPEND_NODE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload_template"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Generate today's standup digest."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;WEBHOOK&lt;/code&gt; triggers, the 201 response includes an &lt;code&gt;hmac_secret_plaintext&lt;/code&gt; that is &lt;strong&gt;not retrievable later&lt;/strong&gt;. Store it on creation.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The skills layer: composable, declarative, skill-card-driven
&lt;/h2&gt;

&lt;p&gt;The thing that ties Work + MiniApp + Schedule together is the &lt;strong&gt;skills layer&lt;/strong&gt;. A skill is a declarative unit (&lt;code&gt;SKILL.md&lt;/code&gt; + optional scripts) that defines inputs, outputs, estimated cost, and a procedure for an agent to follow. Skills compose: a meta-skill like &lt;code&gt;zeeren-overseas-retention&lt;/code&gt; calls &lt;code&gt;zeeren-goal-decomposer&lt;/code&gt;, which fans out into content, concern-triage, and attribution skills.&lt;/p&gt;

&lt;p&gt;From a developer perspective, a skill card looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;brief&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;channel_drafts&lt;/span&gt;
    &lt;span class="na"&gt;output_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;draft_action&lt;/span&gt;
    &lt;span class="na"&gt;action_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publish_&amp;lt;channel&amp;gt;_post"&lt;/span&gt;
&lt;span class="na"&gt;estimated_cost_usd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.12&lt;/span&gt;
&lt;span class="na"&gt;estimated_duration_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;45&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skills target either a &lt;code&gt;WORK_DOMAIN&lt;/code&gt; agent (flexible reasoning, tool access) or a deterministic &lt;code&gt;SKILL&lt;/code&gt; runner (fixed procedure, no LLM calls beyond the skill's own prompts). Schedules can invoke either.&lt;/p&gt;

&lt;p&gt;The skill model makes it straightforward to build reliable automated workflows without wiring together microservices — the platform handles routing, retries, and output persistence into the Work's resource tree.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;The three moving parts — Works, MiniApps, and Schedules — are enough to build most internal tooling. Start with the unified provision endpoint to scaffold your app, use &lt;code&gt;APPEND_NODE&lt;/code&gt; schedules to run recurring tasks inside the same Work context, and use skill cards to define the agent procedures your schedules will invoke.&lt;/p&gt;

&lt;p&gt;The legacy 14-action surface is still there if you're maintaining an older Work, but new development should default to the unified API.&lt;/p&gt;

</description>
      <category>zeeren</category>
      <category>ai</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Free vs Paid Financial APIs: A Selection Roadmap for Individual Quants, Startups, and Enterprises</title>
      <dc:creator>San Si wu</dc:creator>
      <pubDate>Tue, 12 May 2026 14:10:02 +0000</pubDate>
      <link>https://dev.to/san_siwu_f08e7c406830469/free-vs-paid-financial-apis-a-selection-roadmap-for-individual-quants-startups-and-enterprises-1eke</link>
      <guid>https://dev.to/san_siwu_f08e7c406830469/free-vs-paid-financial-apis-a-selection-roadmap-for-individual-quants-startups-and-enterprises-1eke</guid>
      <description>&lt;p&gt;In the data-driven era of quantitative trading, the quality and stability of financial data directly determine the success or failure of strategies. In September 2025, yfinance collapsed entirely due to Yahoo's backend validation upgrade—all scripts without persistent cookies returned 401 errors. In October of the same year, Tushare Pro suddenly suspended operations, catching countless developers who relied on this interface off guard. By 2026, "unauthorized high-frequency scraping" was further clearly defined. The selection of financial data interfaces is evolving from "use whichever is cheapest" into an engineering decision that requires careful consideration.&lt;/p&gt;

&lt;p&gt;This article will provide a complete selection roadmap for individual quantitative researchers, startup teams, and enterprise-level users from two major dimensions—free and paid—combining four core indicators: latency, coverage, stability, and cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  I. Free Financial APIs: The Best Choice for Beginners, But Don't Make Them Your "Staple Food"
&lt;/h2&gt;

&lt;p&gt;The biggest attraction of free financial APIs is undoubtedly "zero cost." For personal learning, lightweight projects, or strategy prototype validation, they are the best starting choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AKShare&lt;/strong&gt; is a star in the Chinese open-source community. This Python library has garnered 14,000+ stars on GitHub, allowing access to comprehensive data including stocks, funds, futures, and macroeconomics with just one line of code. One developer even built TickFlow based on it, proving that quantitative researchers have strong demand for free data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tushare&lt;/strong&gt; takes a different approach—following the Data-as-a-Service model, returning standardized DataFrames with strongly typed fields, making it suitable for direct integration with Pandas. It significantly outperforms AKShare in data quality and API stability. However, Tushare adopts a paid points system for some advanced data, and long-term usage costs gradually become apparent.&lt;/p&gt;

&lt;p&gt;In the international market, &lt;strong&gt;Alpha Vantage&lt;/strong&gt; has become a favorite among AI financial assistant developers thanks to its clear documentation, multi-language SDK support, and early adoption of MCP (Model Context Protocol) support. &lt;strong&gt;Yahoo Finance API&lt;/strong&gt; is widely favored by individual investors and lightweight projects for its broad data sources and convenience of requiring no registration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;market-feed&lt;/strong&gt; is a noteworthy new tool—it uniformly encapsulates free APIs like Yahoo Finance, Alpha Vantage, Polygon.io, Finnhub, Twelve Data, and Tiingo under a single TypeScript client, with built-in caching and automatic fallback mechanisms, significantly reducing migration costs between multiple data sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hidden Costs of Free APIs
&lt;/h3&gt;

&lt;p&gt;No free service is truly "free." The hidden costs of free APIs mainly manifest in the following aspects:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, stability is a major weakness.&lt;/strong&gt; AKShare is essentially a distributed crawler collection that routes requests directly to source sites like East Money and Sina. Once the frontend HTML structure changes, crawlers immediately fail, requiring continuous maintenance and fixes. Quantitative enthusiasts bluntly state on forums that "sometimes it's genuinely unstable," and troubleshooting issues even requires writing specialized testing tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, request frequency is strictly limited.&lt;/strong&gt; Marketstack's free plan provides only 100 API requests per month, with historical data backtracking insufficient for 12 months. Alpha Vantage's free tier also has very limited request frequency. For scenarios like quantitative strategy backtesting that require large amounts of data, the ceiling for free quotas is quite low.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third, lack of SLA guarantees.&lt;/strong&gt; Whether Tencent Cloud or other cloud platforms' free APIs, they typically don't have Service Level Agreements. The new version of the "Cybersecurity Law" effective in 2026 clearly defines high-concurrency scraping. Continuing to run high-concurrency scrapers in production environments not only faces technical risks of IP blocking but also compliance risks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What scenarios are free APIs suitable for?&lt;/strong&gt; Personal quantitative learning, teaching demonstrations, strategy prototype validation, and low-frequency historical data backtesting. However, for trading systems that need to run stably long-term, free APIs should not be the sole data source.&lt;/p&gt;

&lt;h2&gt;
  
  
  II. Paid Financial APIs: From Budget-Friendly Options to Top-Tier Enterprise Choices
&lt;/h2&gt;

&lt;p&gt;The paid market shows clear stratification—from affordable services individuals can bear to institutional-grade high-end solutions, there's a clear "price-quality" ladder.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 Budget-Friendly Paid: Cost-Effective Choices for Individuals and Startup Teams
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://itick.org/en" rel="noopener noreferrer"&gt;&lt;strong&gt;iTick&lt;/strong&gt;&lt;/a&gt; is a dark horse in the 2026 market, focusing on "single interface covering the globe," encompassing six asset classes: stocks (A-shares/US shares/HK shares), forex, cryptocurrencies, indices, futures, and funds, while providing multi-language SDK and MCP Server support. Its real-time interface response time is ≤100ms, with unified interface specifications and concise return formats. Basic features are free, while advanced features are billed by call count.&lt;/p&gt;

&lt;p&gt;Integrating iTick is very simple—you only need to carry a Token in the HTTP request header to obtain real-time quotes. Here's Python code to get real-time quotes for Apple Inc. (AAPL) US stock:&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="n"&gt;API_TOKEN&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_api_token_here&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;accept&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&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_TOKEN&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;get_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="n"&gt;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="s"&gt;https://api.itick.org/stock/quote?region=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;code=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;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;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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&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;200&lt;/span&gt; &lt;span class="ow"&gt;and&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="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;code&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="mi"&gt;0&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;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;data&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; Latest Price: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ld&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="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Change %: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;chp&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="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="nf"&gt;get_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&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;AAPL&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;For scenarios requiring batch monitoring of investment portfolios, iTick also provides batch interfaces, allowing you to obtain the latest prices for multiple stocks in a single request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EODHD&lt;/strong&gt; and &lt;strong&gt;FCS API&lt;/strong&gt; are rated as "budget killers" in the market, especially suitable for multi-asset retail investors or startup teams. EODHD provides historical data covering 60+ exchanges spanning 30 years, with affordable personal plans. FCS API covers 2000+ forex pairs with high cost-effectiveness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Market price reference&lt;/strong&gt;: Annual fees for such solutions typically range from tens to hundreds of dollars, with quantitative platform annual fees between 0 - 20,000 RMB.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 Mid-to-High End Paid: Enterprise-Level Data Quality Assurance
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Polygon.io&lt;/strong&gt; is the king of US stock high-frequency trading, with WebSocket latency achievable below 20ms, making tick-level data unmatched. For strategy teams focused on US stock high-frequency trading, it's the first choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bloomberg Terminal&lt;/strong&gt; and &lt;strong&gt;Reuters Eikon&lt;/strong&gt; are the ceilings of traditional data terminals, with annual fees as high as $20,000 to $24,000/year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Domestic market price tiers&lt;/strong&gt;: Wind terminal annual fees are typically at the level of hundreds of thousands of RMB, with publicly disclosed winning bid prices of $45,360/year (approximately 330,000 RMB). Tonghuashun iFinD intermediary edition industry promotional price is about 12,800 RMB/year, while East Money Choice institutional edition annual fees range from tens of thousands to hundreds of thousands of RMB.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3 Core Value of Paid APIs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;First, WebSocket low-latency push.&lt;/strong&gt; WebSocket persistent connections can reduce end-to-end latency to within 100ms, more than 90% lower compared to HTTP polling. iTick's WebSocket push also supports millisecond-level latency. Here's a simple example of subscribing to real-time trade data:&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;websocket&lt;/span&gt;

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

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

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

&lt;span class="n"&gt;API_TOKEN&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_api_token_here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;WS_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;wss://api.itick.org/stock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# Stock data endpoint
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Handle received real-time data&lt;/span&gt;&lt;span class="sh"&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&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="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&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="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&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="n"&gt;typ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&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;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&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;s&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;typ&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

            &lt;span class="c1"&gt;# Quote update: print latest price and volume
&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;[QUOTE] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; Last: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&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;ld&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="s"&gt; Vol: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&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;v&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="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;typ&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tick&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

            &lt;span class="c1"&gt;# Individual trades: print trade price and volume
&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;[TRADE] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; Price: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&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;p&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="s"&gt; Size: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&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;v&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="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;e&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;Parse error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Send subscription message after successful connection&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WebSocket connected, subscribing...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;sub&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;ac&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;subscribe&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;params&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;AAPL$US,MSFT$US,GOOGL$US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Subscribed symbols, format: code$market
&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;types&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;quote,tick&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# Subscribed data types
&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&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="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sub&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;on_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&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;WebSocket error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close_status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close_msg&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WebSocket closed&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;run_websocket&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Start WebSocket connection with authentication headers&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;token&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_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebSocketApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;

        &lt;span class="n"&gt;WS_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="n"&gt;header&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;on_open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="n"&gt;on_error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="n"&gt;on_close&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_close&lt;/span&gt;

    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="nf"&gt;run_websocket&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make the code more suitable for production environments, you also need to pay attention to the following four points:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correct WebSocket endpoints&lt;/strong&gt;: WebSocket endpoints vary by asset class.&lt;/p&gt;

&lt;p&gt;Stock data endpoint is &lt;code&gt;wss://api.itick.org/stock&lt;/code&gt;,&lt;/p&gt;

&lt;p&gt;Futures endpoint is &lt;code&gt;wss://api.itick.org/future&lt;/code&gt;,&lt;/p&gt;

&lt;p&gt;Index data endpoint is &lt;code&gt;wss://api.itick.org/index&lt;/code&gt;,&lt;/p&gt;

&lt;p&gt;Forex data endpoint is &lt;code&gt;wss://ws.itick.org/forex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fund data endpoint is &lt;code&gt;wss://api.itick.org/fund&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token delivery method&lt;/strong&gt;: Tokens should be placed in &lt;strong&gt;HTTP request headers&lt;/strong&gt; for authentication, with parameter name &lt;code&gt;token&lt;/code&gt;, rather than appended as query strings in URLs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bidirectional message protocol&lt;/strong&gt;: iTick's WebSocket interface is a &lt;strong&gt;bidirectional message protocol&lt;/strong&gt;, not simply "subscribe and push." After successful connection, you &lt;strong&gt;must&lt;/strong&gt; first send a JSON message containing &lt;code&gt;ac&lt;/code&gt; and &lt;code&gt;params&lt;/code&gt; fields to perform authentication (&lt;code&gt;auth&lt;/code&gt;) and data &lt;strong&gt;subscription (subscribe)&lt;/strong&gt; operations before you can start receiving pushed data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection keepalive (heartbeat) mechanism&lt;/strong&gt;: To prevent connections from being closed by the server due to idleness, you need to establish a timer that sends heartbeat messages &lt;code&gt;{"ac": "ping", "params": "{timestamp}"}&lt;/code&gt; to the server approximately every &lt;strong&gt;30 seconds&lt;/strong&gt; to maintain persistent connections. More detailed mechanisms can be referenced in iTick's official documentation.&lt;/p&gt;

&lt;p&gt;In high-frequency trading scenarios, every millisecond difference can affect profits and losses, making WebSocket the standard configuration for serious traders.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, data standardization and completeness.&lt;/strong&gt; Paid services provide cleaned, dividend-adjusted standardized data, significantly reducing ETL and maintenance costs. Tushare Pro's core value lies precisely here—returning standardized DataFrames with strongly typed fields, suitable for direct integration with Pandas/Numpy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third, compliance and SLA.&lt;/strong&gt; Paid APIs provide clear Service Level Agreements and technical support, avoiding legal risks associated with crawling methods.&lt;/p&gt;

&lt;h2&gt;
  
  
  III. Complete Selection Roadmap for Three User Types
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Individual Quantitative Researchers: Low-Cost Trial and Error, Gradual Upgrades
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Starting point&lt;/strong&gt;: Begin with AKShare (free) or Tushare (free basic version) for strategy research and historical backtesting. If cross-border needs exist, use Yahoo Finance or Alpha Vantage (free versions).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advancement&lt;/strong&gt;: When strategies prove effective and data stability becomes a concern, switch to iTick or EODHD entry-level paid packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advanced&lt;/strong&gt;: If venturing into US stock high-frequency strategies, upgrade to Polygon.io or IEX Cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommended solution&lt;/strong&gt;: Tushare (A-share data foundation) + Yahoo Finance (international markets) + market-feed (multi-source fallback backup), keeping annual investment under 500 RMB.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Startup Teams/Small-to-Medium Quantitative Institutions: Pursuing Optimal Cost-Effectiveness with Stability
&lt;/h3&gt;

&lt;p&gt;Startup teams' core requirement is "optimal cost-effectiveness" with "SLA backing." Open-source solutions can be used during strategy validation phases, but paid data sources must be integrated when entering live trading phases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Domestic market&lt;/strong&gt;: First choice is Tushare Pro (points system) combined with MyQuant as a local deployment solution. MyQuant supports multi-language development, covers multiple markets including stocks and futures, and integrates backtesting with live trading functionality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;International market&lt;/strong&gt;: The combination of iTick (full category coverage) + Polygon.io (US stock high-frequency), with annual budget around $2,000 - $5,000.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brokerage ecosystem&lt;/strong&gt;: For domestic quantitative teams, brokerage quantitative platforms like Huatai Securities QMT and Guojin Securities are important options, supporting Python/C++/C# interfaces, with some already offering miniQMT (known to have lower entry thresholds).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommended solution&lt;/strong&gt;: AKShare/Tushare Pro (strategy development) + iTick (live trading quotes) + QMT/MyQuant (live trading), with annual investment around 5,000 - 20,000 RMB.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 Enterprise/Institutional Users: Spare No Expense, Only Seek Stability and Compliance
&lt;/h3&gt;

&lt;p&gt;Enterprise users' core requirements are data quality, compliance, and SLA. At this level, cost is not the primary consideration.&lt;/p&gt;

&lt;p&gt;Domestically, Wind terminal is the top recommendation, dominating institutional research with comprehensive data coverage and powerful API integration capabilities (supporting C++/Python, etc.). Tonghuashun iFinD and East Money Choice are more cost-effective alternatives, with annual fees ranging from tens of thousands to hundreds of thousands of RMB.&lt;/p&gt;

&lt;p&gt;In high-frequency trading scenarios, Xuntou QMT already covers 80+ brokerages (Guojin, CICC, CITIC Construction Investment, etc.), with latency reaching millisecond levels, making it the most widely used brokerage quantitative system domestically. For international high-frequency trading, ultra-low latency systems like NautilusTrader achieve latency below 1ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommended solution&lt;/strong&gt;: Wind/Tonghuashun iFinD (global data) + QMT/brokerage counter direct connection (live trading) + Polygon.io (US stock international data), with annual investment exceeding 100,000 RMB.&lt;/p&gt;

&lt;h2&gt;
  
  
  IV. Technology Trends and Selection Outlook
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trend 1: MCP Becomes New Standard.&lt;/strong&gt; API selection in 2026 no longer just discusses "whether REST API exists"—whether AI agents can directly call APIs through natural language to obtain real-time quotes has become a new battlefield. Alpha Vantage and iTick have both pioneered MCP Server support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trend 2: WebSocket is the Baseline.&lt;/strong&gt; In 2026, any financial API that doesn't support WebSocket can basically be excluded from serious trading. For T+0 products like forex and precious metals, whether disconnection reconnection and heartbeat keepalive mechanisms are natively supported has become a hard indicator for technical selection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trend 3: Don't Put All Eggs in One Basket.&lt;/strong&gt; The Tushare Pro suspension incident taught all developers an important lesson—prepare backup data sources for critical business. It's strongly recommended to quickly switch when primary data sources encounter problems, ensuring business continuity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trend 4: Brokerage Direct Connection is Being "Democratized" to Individual Users.&lt;/strong&gt; Previously, only institutions could apply for brokerage API trading interfaces, but now more and more brokerages are opening personal quantitative interfaces, significantly lowering thresholds. A small-scale quantitative closed loop of "open-source data + low-cost API + brokerage direct connection" is becoming reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  V. Conclusion: What Fits You Best is Truly the Best
&lt;/h2&gt;

&lt;p&gt;There's no universal solution for financial data API selection, only scenario-based solutions. Here are quick-reference recommendations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Personal Learning/Backtesting&lt;/strong&gt;: AKShare / Tushare Free / Yahoo Finance, budget &amp;lt;500 RMB/year, backup with market-feed multi-source fallback.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Strategy Development/Simulation&lt;/strong&gt;: Tushare Pro / iTick Entry Version, budget 500-2,000 RMB/year, backup with AKShare + local MySQL cache.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Live Trading/Small-to-Medium Teams&lt;/strong&gt;: iTick / EODHD / IEX Cloud, budget 2,000-20,000 RMB/year, backup with MyQuant / brokerage QMT.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Institutional/High-Frequency&lt;/strong&gt;: Wind / Polygon.io / QMT, budget &amp;gt;100,000 RMB/year, backup with Tonghuashun iFinD / Bloomberg.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regardless of which solution you choose, ensure proper exception handling and retry mechanisms, establish data quality monitoring, and always maintain a backup data source—in the world of financial data, there's no eternal "stability," only continuously optimized "reliability."&lt;/p&gt;

&lt;p&gt;Reference documentation: &lt;a href="https://blog.itick.org/en/stock-api/hkus-stock-api-comparison-guide" rel="noopener noreferrer"&gt;https://blog.itick.org/stock-api/hkus-stock-api-comparison-guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/itick-org" rel="noopener noreferrer"&gt;https://github.com/itick-org/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>python</category>
      <category>api</category>
    </item>
    <item>
      <title>免费 vs 付费金融 API：个人量化、创业公司、企业级选型路线图</title>
      <dc:creator>San Si wu</dc:creator>
      <pubDate>Tue, 12 May 2026 13:59:53 +0000</pubDate>
      <link>https://dev.to/san_siwu_f08e7c406830469/mian-fei-vs-fu-fei-jin-rong-apige-ren-liang-hua-chuang-ye-gong-si-qi-ye-ji-xuan-xing-lu-xian-tu-3pko</link>
      <guid>https://dev.to/san_siwu_f08e7c406830469/mian-fei-vs-fu-fei-jin-rong-apige-ren-liang-hua-chuang-ye-gong-si-qi-ye-ji-xuan-xing-lu-xian-tu-3pko</guid>
      <description>&lt;p&gt;在数据驱动的量化交易时代，金融数据的质量和稳定性直接决定了策略的成败。2025 年 9 月，yfinance 因雅虎后端校验升级而全线崩溃——未持久化 Cookie 的脚本全部返回 401 错误，同年 10 月，Tushare Pro 突发停运事件，让无数依赖该接口的开发者措手不及。2026 年进一步对“非授权高频抓取”明确了界定。金融数据接口的选型，正在从“哪个便宜用哪个”，演变为一个需要认真权衡的工程决策。&lt;br&gt;
本文将从免费与付费两大维度，结合延迟、覆盖范围、稳定性、成本四个核心指标，为个人量化研究者、创业团队和企业级用户提供一份完整的选型路线图。&lt;/p&gt;
&lt;h2&gt;
  
  
  一、免费金融 API：入门的首选，但也别当“主粮”
&lt;/h2&gt;

&lt;p&gt;免费金融 API 的最大吸引力无疑是“零成本”。对于个人学习、轻量级项目或策略原型验证来说，它们是起步的最佳选择。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AKShare&lt;/strong&gt; 是中文开源社区中的明星。这个 Python 库在 GitHub 上收获了 14000+ 星标，通过简单一行代码即可访问股票、基金、期货、宏观经济等全方位数据。一位开发者甚至以此为基础开发了 TickFlow，证明量化研究者对免费数据的需求极为旺盛。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tushare&lt;/strong&gt; 则是另一种思路——走 Data-as-a-Service 路线，返回标准化的 DataFrame，字段类型强类型化，适合直接对接 Pandas，在数据质量和 API 稳定性上明显优于 AKShare。不过，Tushare 对部分高级数据采用付费积分制，长期使用成本会逐渐显现。&lt;/p&gt;

&lt;p&gt;国际市场上，&lt;strong&gt;Alpha Vantage&lt;/strong&gt; 凭借清晰的文档和多语言 SDK，以及率先普及的 MCP（Model Context Protocol）支持，成为 AI 金融助理开发者的宠儿。&lt;strong&gt;Yahoo Finance API&lt;/strong&gt; 则以数据来源广、无需注册即可使用的便利性，广受个人投资者和轻量级项目的青睐。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;market-feed&lt;/strong&gt; 是一个值得关注的新工具——它将 Yahoo Finance、Alpha Vantage、Polygon.io、Finnhub、Twelve Data、Tiingo 等免费 API 统一封装在同一个 TypeScript 客户端下，内置缓存和自动降级机制，大幅降低了在多数据源间迁移的成本。&lt;/p&gt;
&lt;h3&gt;
  
  
  免费 API 的隐性成本
&lt;/h3&gt;

&lt;p&gt;没有任何免费的服务是真正“免费”的。免费 API 的隐性成本主要体现在以下方面：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第一，稳定性是硬伤。&lt;/strong&gt; AKShare 本质上是一个分布式爬虫集合，将请求直接路由到东方财富、新浪等源站，一旦前端 HTML 结构变更，爬虫立即失效，需要持续维护和修复。量化爱好者在论坛中直言“有的时候真心不稳定”，排查问题甚至需要专门编写测试工具。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第二，请求频率严格受限。&lt;/strong&gt; Marketstack 的免费计划每月仅提供 100 次 API 请求，历史数据回溯不足 12 个月。Alpha Vantage 免费层请求频率同样非常有限。对于量化策略回测这类需要大量数据的场景来说，免费额度的天花板相当低。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第三，缺乏 SLA 保障。&lt;/strong&gt; 无论是腾讯云还是其他云平台的免费 API，通常都没有服务等级协议。2026 年生效的新版《网络安全法》对高并发爬虫进行了明确界定，继续在生产环境运行高并发爬虫不仅面临 IP 被封的技术风险，更面临合规风险。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;免费 API 适合什么场景？&lt;/strong&gt; 个人量化学习、教学演示、策略原型验证、低频的历史数据回测。但对于需要长期稳定运行的交易系统，免费 API 不应作为唯一数据源。&lt;/p&gt;
&lt;h2&gt;
  
  
  二、付费金融 API：从平价款到顶尖企业的选择
&lt;/h2&gt;

&lt;p&gt;付费市场呈现出明显的分层——从个人可承受的平价服务到机构级的高端方案，有一条清晰的“价格-质量”阶梯。&lt;/p&gt;
&lt;h3&gt;
  
  
  2.1 平价付费：个人和创业团队的性价比之选
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://itick.org/zh-cn" rel="noopener noreferrer"&gt;&lt;strong&gt;iTick&lt;/strong&gt;&lt;/a&gt; 是 2026 年市场中的一匹黑马，主打“单一接口覆盖全球”，涵盖股票（A 股/美股/港股）、外汇、加密货币、指数、期货、基金六大资产类别，并提供多语言 SDK 和 MCP Server 支持。其实时接口响应时间 ≤100ms，接口规范统一、返回格式简洁，基础功能免费，高级功能按调用次数计费。&lt;/p&gt;

&lt;p&gt;接入 iTick 非常简单，只需要在 HTTP 请求头中携带 Token 即可获取实时行情。下面是一段获取美股苹果公司（AAPL）实时报价的 Python 代码：&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="n"&gt;API_TOKEN&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_api_token_here&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;accept&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&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_TOKEN&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;get_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

&lt;span class="n"&gt;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="s"&gt;https://api.itick.org/stock/quote?region=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;code=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;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;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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&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;200&lt;/span&gt;  &lt;span class="ow"&gt;and&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="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;code&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="mi"&gt;0&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="n"&gt;data&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;data&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; 最新价: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ld&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="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, 涨跌幅: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;chp&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="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;

 &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="nf"&gt;get_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&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;AAPL&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;对于需要批量监控投资组合的场景，iTick 也提供了批量接口，一次请求即可获得多只股票的最新价格。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EODHD&lt;/strong&gt; 和 &lt;strong&gt;FCS API&lt;/strong&gt; 被市场评为“平价屠夫”，尤其适合多资产散户或初创团队。EODHD 提供覆盖 60+ 交易所、长达 30 年的历史数据，个人方案价格亲民。FCS API 覆盖 2000+ 外汇对，性价比较高。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;市场价位参考&lt;/strong&gt;：这类方案的年度费用通常在数十到数百美元之间，量化平台年费在 0 - 2 万人民币之间。&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 中高端付费：企业级的数据质量保证
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Polygon.io&lt;/strong&gt; 是美股高频领域的王者，WebSocket 延迟可达到 20ms 以下，tick 级数据无人能敌。对于专注美股高频交易的策略团队来说，它是首选。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bloomberg Terminal&lt;/strong&gt; 和 &lt;strong&gt;Reuters Eikon&lt;/strong&gt; 则是传统数据终端的天花板，年费高达 2 万到 2.4 万美元/年。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;国内市场的价格梯队&lt;/strong&gt;：Wind 终端年费通常在数十万人民币级别，已有公开的中标价为 45,360 美元/年（约 33 万人民币）。同花顺 iFinD 中介机构版的行业优惠价约为 12,800 元/年，而东方财富 Choice 机构版年费从数万元到数十万元不等。&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3 付费 API 的核心价值
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;第一，WebSocket 低延迟推送。&lt;/strong&gt; WebSocket 长连接的端到端延迟可降至 100ms 以内，相比 HTTP 轮询降低超过 90%。iTick 的 WebSocket 推送同样支持毫秒级延迟，下面是一个订阅实时成交数据的简单示例：&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;websocket&lt;/span&gt;

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

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

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

&lt;span class="n"&gt;API_TOKEN&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_api_token_here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;WS_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;wss://api.itick.org/stock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# 股票数据端点
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;  &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

 &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;处理接收到的实时数据&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

 &lt;span class="k"&gt;try&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="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

 &lt;span class="k"&gt;if&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="ow"&gt;not&lt;/span&gt;  &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&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="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="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&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="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="n"&gt;typ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&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;type&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&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;s&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;typ&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

 &lt;span class="c1"&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;[QUOTE] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; Last: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&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;ld&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="s"&gt; Vol: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&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;v&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="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;typ&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tick&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

 &lt;span class="c1"&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;[TRADE] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; Price: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&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;p&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="s"&gt; Size: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&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;v&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="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;e&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;Parse error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&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="k"&gt;def&lt;/span&gt;  &lt;span class="nf"&gt;on_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

 &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;连接成功后发送订阅消息&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

 &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WebSocket connected, subscribing...&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;sub&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;ac&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;subscribe&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;params&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;AAPL$US,MSFT$US,GOOGL$US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# 订阅的标的，格式为 代码$市场
&lt;/span&gt;
 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;types&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;quote,tick&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&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="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="n"&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="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sub&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;on_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&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;WebSocket error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&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="k"&gt;def&lt;/span&gt;  &lt;span class="nf"&gt;on_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close_status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close_msg&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WebSocket closed&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;run_websocket&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

 &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;启动WebSocket连接，加入认证头&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&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;token&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_TOKEN&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="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebSocketApp&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="n"&gt;WS_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

 &lt;span class="n"&gt;header&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;on_open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

 &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

 &lt;span class="n"&gt;on_error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

 &lt;span class="n"&gt;on_close&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_close&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="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;run_websocket&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;为了让代码更适用于生产环境，你还需要注意下面这四点：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;正确的 WebSocket 端点&lt;/strong&gt;：不同资产类别的 WebSocket 端点各不相同。&lt;/p&gt;

&lt;p&gt;股票数据对应的端点是&lt;code&gt;wss://api.itick.org/stock&lt;/code&gt;，&lt;/p&gt;

&lt;p&gt;期货端点是&lt;code&gt;wss://api.itick.org/future&lt;/code&gt;，&lt;/p&gt;

&lt;p&gt;指数数据对应的端点是&lt;code&gt;wss://api.itick.org/index&lt;/code&gt;，&lt;/p&gt;

&lt;p&gt;外汇数据对应的端点是&lt;code&gt;wss://ws.itick.org/forex&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;基金数据对应的端点是&lt;code&gt;wss://api.itick.org/fund&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token 的传递方式&lt;/strong&gt;：Token 应放在 &lt;strong&gt;HTTP 请求头（Header）&lt;/strong&gt; 中进行认证，参数名为&lt;code&gt;token&lt;/code&gt;，而非在 URL 中以查询字符串（Query String）的形式拼写。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;双向的消息协议&lt;/strong&gt;：iTick 的 WebSocket 接口是一个&lt;strong&gt;双向消息协议&lt;/strong&gt;，并非简单的“订阅即推送”。连接成功后，你&lt;strong&gt;必须&lt;/strong&gt;先发送一条包含&lt;code&gt;ac&lt;/code&gt;和&lt;code&gt;params&lt;/code&gt;字段的 JSON 消息，来执行鉴权（&lt;code&gt;auth&lt;/code&gt;）和数据&lt;strong&gt;订阅（subscribe）&lt;/strong&gt; 操作，才能开始接收推送数据。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;连接保活（心跳）机制&lt;/strong&gt;：为防止连接因空闲而被服务端关闭，你需要建立一个定时器，大概每&lt;strong&gt;30 秒&lt;/strong&gt;向服务端发送一次心跳消息&lt;code&gt;{"ac": "ping", "params": "{timestamp}"}&lt;/code&gt;来维持长连接。更详细的机制可以参考 iTick 的官方文档。&lt;/p&gt;

&lt;p&gt;在高频交易场景中，每一毫秒的差异都可能影响盈亏，WebSocket 已经成为严肃交易者的标准配置。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第二，数据标准化与完整性。&lt;/strong&gt; 付费服务提供经过清洗、除权除息的标准化数据，大幅降低 ETL 和维护成本。Tushare Pro 的核心价值正在于此——返回标准化的 DataFrame，字段类型强类型化，适合直接对接 Pandas/Numpy。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第三，合规与 SLA。&lt;/strong&gt; 付费 API 提供明确的服务等级协议和技术支持，规避了爬虫手段的法律风险。&lt;/p&gt;

&lt;h2&gt;
  
  
  三、三种用户类型的完整选型路线图
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 个人量化研究者：低成本试错，逐步升级
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;起点&lt;/strong&gt;：从 AKShare（免费）或 Tushare（免费基础版）入手进行策略研究和历史回测。若有跨境需求，可用 Yahoo Finance 或 Alpha Vantage（免费版）。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;进阶&lt;/strong&gt;：当策略验证有效、开始关注数据稳定性时，切换到 iTick 或 EODHD 的入门付费套餐。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;高阶&lt;/strong&gt;：若涉足美股高频策略，升级到 Polygon.io 或 IEX Cloud。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;推荐方案&lt;/strong&gt;：Tushare（A 股数据基础）+ Yahoo Finance（国际市场）+ market-feed（多源降级备份），年投入控制在 500 元以内。&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 创业团队/中小量化机构：追求性价比与稳定性兼顾
&lt;/h3&gt;

&lt;p&gt;创业团队的核心需求是“性价比最优”且“有 SLA 兜底”。在策略验证阶段可以沿用开源方案，进入实盘阶段后必须接入付费数据源。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;国内市场&lt;/strong&gt;：首选 Tushare Pro（积分制）配合掘金量化（MyQuant）作为本地部署方案。掘金支持多语言开发，覆盖股票、期货等多市场，集成回测与实盘功能。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;国际市场&lt;/strong&gt;：iTick（全品类覆盖）+ Polygon.io（美股高频）的组合，年度预算约 2000 - 5000 美元。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;券商生态&lt;/strong&gt;：对于国内量化团队，华泰证券 QMT、国金证券等券商量化平台是重要选项，支持 Python/C++/C#接口，有的已开放 miniQMT（准入门槛已知较低）。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;推荐方案&lt;/strong&gt;：AKShare/Tushare Pro（策略研发） + iTick（实盘行情）+ QMT/掘金（实盘交易），年投入约 5000 - 20000 元。&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 企业级/机构用户：不计代价，只求稳定与合规
&lt;/h3&gt;

&lt;p&gt;企业级用户的核心需求是数据质量、合规与 SLA。在这个级别，费用不是首要考虑因素。&lt;/p&gt;

&lt;p&gt;国内首推 Wind 终端，凭借全面的数据覆盖和强大的 API 接入能力（支持 C++/Python 等），在机构研究中占据主导地位。同花顺 iFinD 和东方财富 Choice 则是性价比更高的备选，年费数万到数十万元人民币。&lt;/p&gt;

&lt;p&gt;高频交易场景下，迅投 QMT 已覆盖 80+ 券商（国金、中金、中信建投等），延迟可达毫秒级，是目前国内最广泛使用的券商量化系统。国际高频方面，NautilusTrader 等极低延迟系统延迟低于 1ms。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;推荐方案&lt;/strong&gt;：Wind/同花顺 iFinD（全局数据） + QMT/券商柜台直连（实盘交易） + Polygon.io（美股国际数据），年投入 10 万元以上。&lt;/p&gt;

&lt;h2&gt;
  
  
  四、技术趋势与选型前瞻
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;趋势一：MCP 成为新标配。&lt;/strong&gt; 2026 年的 API 选型已经不只讨论“有没有 REST API”，AI 代理能否通过自然语言直接调用 API 获取实时行情成为新战场。Alpha Vantage 和 iTick 均已率先提供了 MCP Server 支持。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;趋势二：WebSocket 是底线。&lt;/strong&gt; 在 2026 年，任何不支持 WebSocket 的金融 API 基本可以被排除在严肃交易之外。对于外汇和贵金属这种 T+0 品种，断线重连、心跳保活机制是否原生支持，是技术选型的硬指标。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;趋势三：不把鸡蛋放在一个篮子里。&lt;/strong&gt; Tushare Pro 的停运事件给所有开发者上了重要一课——为关键业务准备备用数据源。强烈建议在主数据源出现问题时快速切换，保证业务连续性。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;趋势四：券商直连正在「下放」个人用户。&lt;/strong&gt; 以前只有机构才能申请券商 API 交易接口，现在越来越多券商开放了个人量化接口，门槛大幅降低。一个“开源数据 + 低价 API + 券商直连”的小型量化闭环正在成为现实。&lt;/p&gt;

&lt;h2&gt;
  
  
  五、总结：适合自己的，才是最好的
&lt;/h2&gt;

&lt;p&gt;金融数据 API 的选型没有通用解，只有场景解。以下是速查版建议：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;个人学习/回测&lt;/strong&gt;：AKShare / Tushare 免费 / Yahoo Finance，预算 &amp;lt;500 元/年，备用 market-feed 多源降级。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;策略研发/模拟&lt;/strong&gt;：Tushare Pro / iTick 入门版，预算 500-2000 元/年，备用 AKShare + 本地 MySQL 缓存。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;实盘/中小团队&lt;/strong&gt;：iTick / EODHD / IEX Cloud，预算 2000-20000 元/年，备用掘金量化 / 券商 QMT。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;机构级/高频&lt;/strong&gt;：Wind / Polygon.io / QMT，预算 &amp;gt;10 万元/年，备用同花顺 iFinD / Bloomberg。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;无论选择哪种方案，确保做好异常处理和重试机制，建立数据质量监控，并始终保留一个备用数据源——在金融数据的世界里，没有永远的“稳定”，只有不断优化的“可靠”。&lt;/p&gt;

&lt;p&gt;参考文档：&lt;a href="https://blog.itick.org/zh-cn/stock-api/hkus-stock-api-comparison-guide" rel="noopener noreferrer"&gt;https://blog.itick.org/stock-api/hkus-stock-api-comparison-guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub：&lt;a href="https://github.com/itick-org" rel="noopener noreferrer"&gt;https://github.com/itick-org/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>python</category>
      <category>api</category>
    </item>
    <item>
      <title>Test Cal.com booking flows without calendar chaos</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Tue, 12 May 2026 13:45:00 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/test-calcom-booking-flows-without-calendar-chaos-2d2c</link>
      <guid>https://dev.to/fetchsandbox/test-calcom-booking-flows-without-calendar-chaos-2d2c</guid>
      <description>&lt;p&gt;Scheduling integrations do not usually break on the first request.&lt;/p&gt;

&lt;p&gt;They break after the booking exists.&lt;/p&gt;

&lt;p&gt;The user gets an email. The host has Google Calendar connected. Your app sends its own ICS file. Then somebody reschedules or cancels and suddenly there are two calendar events, or one event refuses to disappear.&lt;/p&gt;

&lt;p&gt;That is not a generic "calendar APIs are hard" problem.&lt;/p&gt;

&lt;p&gt;It is a workflow identity problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The specific workflow
&lt;/h2&gt;

&lt;p&gt;The narrow Cal.com flow I care about is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get available slots&lt;/li&gt;
&lt;li&gt;Create a booking&lt;/li&gt;
&lt;li&gt;Fetch the booking&lt;/li&gt;
&lt;li&gt;Reschedule it&lt;/li&gt;
&lt;li&gt;Cancel it&lt;/li&gt;
&lt;li&gt;Verify the calendar and webhook side effects still point at the same booking&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In API terms, the happy path looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET  /v2/slots
POST /v2/bookings
GET  /v2/bookings/{bookingUid}
POST /v2/bookings/{bookingUid}/reschedule
POST /v2/bookings/{bookingUid}/cancel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the test.&lt;/p&gt;

&lt;p&gt;Not just "can I create a booking?"&lt;/p&gt;

&lt;p&gt;The real question is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;slot -&amp;gt; booking -&amp;gt; calendar event -&amp;gt; reschedule -&amp;gt; cancel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do all of those still refer to the same thing?&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug shape
&lt;/h2&gt;

&lt;p&gt;Here is the kind of bug this flow catches.&lt;/p&gt;

&lt;p&gt;Your app creates a booking through Cal.com. The attendee has a connected Google Calendar. Cal.com syncs the event into Google.&lt;/p&gt;

&lt;p&gt;But your app also sends custom emails with its own ICS attachment.&lt;/p&gt;

&lt;p&gt;If the ICS UID from the Booking API and the synced Google Calendar event identity do not line up, the user can end up with two events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event from Cal.com sync
event from your custom ICS invite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then cancellation gets messy because your app cancels one identity while the other one remains in the calendar.&lt;/p&gt;

&lt;p&gt;This is the kind of integration bug that is easy to miss if you only test &lt;code&gt;POST /bookings&lt;/code&gt; in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would assert
&lt;/h2&gt;

&lt;p&gt;For a scheduling API, I want the test to preserve identity across the whole lifecycle.&lt;/p&gt;

&lt;p&gt;The assertions should be closer to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;booking created -&amp;gt; booking uid exists
calendar reference exists -&amp;gt; external event id is attached
reschedule uses same booking identity
cancel removes or updates the same calendar event
webhook events describe the same lifecycle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if your product sends custom ICS files, there is one extra check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Booking API iCalUID matches the calendar event your user will update/cancel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last part is where teams usually learn the hard way that a "successful booking" does not mean a safe scheduling integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why static mocks miss it
&lt;/h2&gt;

&lt;p&gt;A static mock can return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /bookings -&amp;gt; 201
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not enough.&lt;/p&gt;

&lt;p&gt;It cannot easily prove that a later reschedule call still points at the booking created two steps earlier. It cannot prove that your cancellation code is updating the same calendar event your invite email created. It cannot prove that webhook handlers and calendar references agree about the booking identity.&lt;/p&gt;

&lt;p&gt;The state between calls is the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where FetchSandbox fits
&lt;/h2&gt;

&lt;p&gt;FetchSandbox has a Cal.com sandbox flow for the booking lifecycle:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com/cal-com/test-booking-lifecycle-locally" rel="noopener noreferrer"&gt;https://fetchsandbox.com/cal-com/test-booking-lifecycle-locally&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The broader Cal.com sandbox is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com/cal-com" rel="noopener noreferrer"&gt;https://fetchsandbox.com/cal-com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The workflow is intentionally narrow because that is where integration bugs hide: slots, booking creation, booking lookup, reschedule, cancel, and the events around those changes.&lt;/p&gt;

&lt;p&gt;You can also connect your IDE to FetchSandbox through MCP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fetchsandbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fetchsandbox-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask an agent something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use FetchSandbox MCP to run the Cal.com booking lifecycle workflow.
Explain which identifiers my app should preserve across create, reschedule, cancel, and calendar sync.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the direction I think API testing should move:&lt;/p&gt;

&lt;p&gt;not just docs,&lt;br&gt;
not just static responses,&lt;br&gt;
but runnable workflow behavior from inside the developer's own environment.&lt;/p&gt;

&lt;p&gt;For Cal.com-style integrations, the useful question is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can my app create, move, and cancel a booking without leaving calendar ghosts behind?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That should be testable before production users find it.&lt;/p&gt;

</description>
      <category>calcom</category>
      <category>api</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What Makes the Best Tools for API Testing and API Debugging Stand Out in 2026?</title>
      <dc:creator>luc</dc:creator>
      <pubDate>Tue, 12 May 2026 13:41:45 +0000</pubDate>
      <link>https://dev.to/luc_36689a31b94d68d29f9e9/what-makes-the-best-tools-for-api-testing-and-api-debugging-stand-out-in-2026-4lm4</link>
      <guid>https://dev.to/luc_36689a31b94d68d29f9e9/what-makes-the-best-tools-for-api-testing-and-api-debugging-stand-out-in-2026-4lm4</guid>
      <description>&lt;p&gt;In 2026, developers are moving toward more flexible, collaborative, and generous &lt;strong&gt;Postman-like tools&lt;/strong&gt;. This article presents the &lt;strong&gt;top 20 online Postman-like tools for API testing and API debugging&lt;/strong&gt; that the community is actively using this year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Download Apidog for free&lt;/strong&gt; today and experience a powerful all-in-one platform built for modern &lt;a href="http://apidog.com/?utm_source=dev.to&amp;amp;utm_medium=herve&amp;amp;utm_content=postman-alternatives"&gt;API testing and API debugging&lt;/a&gt;. Apidog combines design, testing, debugging, mocking, and documentation in one smooth workspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Online Tools Win for API Testing and API Debugging
&lt;/h2&gt;

&lt;p&gt;Teams prefer online and hybrid tools for &lt;strong&gt;API testing and API debugging&lt;/strong&gt; because they eliminate installation barriers, enable instant access from any device, and support real-time collaboration. These platforms handle environment variables, mock servers, CI/CD integration, and team workspaces effectively.&lt;/p&gt;

&lt;p&gt;Small improvements in your chosen tool often lead to big differences in daily velocity and code quality.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. &lt;a href="http://apidog.com/?utm_source=dev.to&amp;amp;utm_medium=herve&amp;amp;utm_content=postman-alternatives"&gt;Apidog&lt;/a&gt; The #1 All-in-One Platform for Comprehensive API Testing and API Debugging in 2026
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fotpf2mplha5z3sinthr0.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.amazonaws.com%2Fuploads%2Farticles%2Fotpf2mplha5z3sinthr0.png" alt=" " width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="http://apidog.com/?utm_source=dev.to&amp;amp;utm_medium=herve&amp;amp;utm_content=postman-alternatives"&gt;Apidog&lt;/a&gt; stands as the &lt;strong&gt;clear leader&lt;/strong&gt; among Postman alternatives in 2026. It delivers a complete, unified workspace that brings together API design, testing, debugging, mocking, documentation, and code generation all without the restrictive free plan limitations that many other tools now impose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Apidog Excels at API Testing and API Debugging
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Key strengths that make Apidog the top choice:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-time collaborative workspaces&lt;/strong&gt; Multiple team members can edit, comment, and review the same collection simultaneously&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced mock servers&lt;/strong&gt; Generate realistic, customizable responses in seconds (with dynamic data, rules, and delays)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Powerful environment &amp;amp; variable management&lt;/strong&gt; Switch effortlessly between development, staging, and production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native CI/CD integration&lt;/strong&gt; Automate your API testing pipelines with ease&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic interactive documentation&lt;/strong&gt; Always up-to-date with code samples in multiple languages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full support for REST, GraphQL, WebSocket, and more&lt;/strong&gt; Future-proof for modern API architectures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich request/response visualization&lt;/strong&gt; Debug faster with beautiful JSON formatting, schema validation, and performance insights&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apidog shines in real-world workflows. You can design an endpoint with OpenAPI specs, instantly generate realistic mocks, write automated tests with advanced assertions (status codes, schemas, response time, custom logic), and debug failures with detailed step-through inspection &lt;strong&gt;all inside the same tab&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Teams switching to Apidog report significantly less context switching, faster onboarding for new developers, higher test coverage, and quicker issue resolution during &lt;strong&gt;API debugging&lt;/strong&gt; sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="http://apidog.com/?utm_source=dev.to&amp;amp;utm_medium=herve&amp;amp;utm_content=postman-alternatives"&gt;Download Apidog for free&lt;/a&gt;&lt;/strong&gt; and see why it has become the preferred platform for developers and teams serious about &lt;strong&gt;API testing and API debugging&lt;/strong&gt; in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;a href="https://hoppscotch.io/" rel="noopener noreferrer"&gt;Hoppscotch&lt;/a&gt; Lightweight, Fast, and Open-Source API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2F2p6i18k7oduuazz1l3dt.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.amazonaws.com%2Fuploads%2Farticles%2F2p6i18k7oduuazz1l3dt.png" alt=" " width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hoppscotch.io/" rel="noopener noreferrer"&gt;Hoppscotch&lt;/a&gt; is a favorite for developers who value speed and simplicity. This open-source, browser-first tool supports REST, GraphQL, WebSocket, and more with zero installation.&lt;/p&gt;

&lt;p&gt;Perfect for quick exploratory &lt;strong&gt;API testing and API debugging&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;a href="https://reqbin.com/" rel="noopener noreferrer"&gt;ReqBin&lt;/a&gt; Straightforward Instant API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2F9b5zibrwhuib1rzg3yqu.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.amazonaws.com%2Fuploads%2Farticles%2F9b5zibrwhuib1rzg3yqu.png" alt=" " width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://reqbin.com/" rel="noopener noreferrer"&gt;ReqBin&lt;/a&gt; offers a clean, no-account-needed interface for sending requests and inspecting responses. Its code generation in multiple languages speeds up integration after successful API testing.&lt;/p&gt;

&lt;p&gt;Excellent for urgent &lt;strong&gt;API debugging&lt;/strong&gt; during production incidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;a href="https://beeceptor.com/" rel="noopener noreferrer"&gt;Beeceptor&lt;/a&gt; Powerful Mocking for API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Flanif05ltvs53dwrmvjp.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.amazonaws.com%2Fuploads%2Farticles%2Flanif05ltvs53dwrmvjp.png" alt=" " width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://beeceptor.com/" rel="noopener noreferrer"&gt;Beeceptor&lt;/a&gt; excels at creating temporary mock endpoints. Define rules, dynamic responses, and inspect traffic in real time perfect when the real backend isn’t ready yet.&lt;/p&gt;

&lt;p&gt;It accelerates parallel development and early-stage &lt;strong&gt;API testing and API debugging&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;a href="https://swagger.io/" rel="noopener noreferrer"&gt;SwaggerHub&lt;/a&gt; Specification-First API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fn94692bxekisr3qms2dd.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.amazonaws.com%2Fuploads%2Farticles%2Fn94692bxekisr3qms2dd.png" alt=" " width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swagger.io/" rel="noopener noreferrer"&gt;SwaggerHub&lt;/a&gt; (by SmartBear) is the go-to for teams following OpenAPI standards. Design, mock, test, document, and govern APIs in one governed platform.&lt;/p&gt;

&lt;p&gt;Strong choice for enterprise &lt;strong&gt;API testing and API debugging&lt;/strong&gt; with auditability.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. &lt;a href="https://firecamp.io/" rel="noopener noreferrer"&gt;Firecamp&lt;/a&gt; Multi-Protocol Support for Versatile API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fbdg48mwhfk4736ho7njl.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.amazonaws.com%2Fuploads%2Farticles%2Fbdg48mwhfk4736ho7njl.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://firecamp.io/" rel="noopener noreferrer"&gt;Firecamp&lt;/a&gt; unifies REST, GraphQL, WebSocket, and other protocols in one polished interface. Real-time collaboration makes it a solid daily driver.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. &lt;a href="https://httpie.io/" rel="noopener noreferrer"&gt;HTTPie&lt;/a&gt; Minimalist Yet Effective API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fh528jp3wi5o645nzhfbl.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.amazonaws.com%2Fuploads%2Farticles%2Fh528jp3wi5o645nzhfbl.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://httpie.io/" rel="noopener noreferrer"&gt;HTTPie&lt;/a&gt; (online version available) focuses on clarity and speed with a human-friendly syntax. Great for quick, clutter-free &lt;strong&gt;API testing and API debugging&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. &lt;a href="https://stoplight.io/" rel="noopener noreferrer"&gt;Stoplight&lt;/a&gt; Design-First Approach to API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Ft49uy700lt2bec3ikrw8.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.amazonaws.com%2Fuploads%2Farticles%2Ft49uy700lt2bec3ikrw8.png" alt=" " width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stoplight.io/" rel="noopener noreferrer"&gt;Stoplight&lt;/a&gt; lets teams start with design (OpenAPI/JSON Schema), then move into mocking, testing, and governance with strong analytics for &lt;strong&gt;API debugging&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. &lt;a href="https://apiary.io/" rel="noopener noreferrer"&gt;Apiary&lt;/a&gt; Full Lifecycle Management for API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fabrmxcsoqhajlejd3rmp.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.amazonaws.com%2Fuploads%2Farticles%2Fabrmxcsoqhajlejd3rmp.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apiary.io/" rel="noopener noreferrer"&gt;Apiary&lt;/a&gt; (by Oracle) provides complete API lifecycle support with good collaboration and monitoring features.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. &lt;a href="https://assertible.com/" rel="noopener noreferrer"&gt;Assertible&lt;/a&gt; Automated Scheduled API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2F2basygoudyapmt4u9rkb.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.amazonaws.com%2Fuploads%2Farticles%2F2basygoudyapmt4u9rkb.png" alt=" " width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://assertible.com/" rel="noopener noreferrer"&gt;Assertible&lt;/a&gt; specializes in continuous validation through scheduled tests, assertions, and deep CI/CD integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. &lt;a href="https://extendsclass.com/api-tester.html" rel="noopener noreferrer"&gt;ExtendsClass API Tester&lt;/a&gt; Quick and Practical API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fiowggal6lr2ib7k5rtex.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.amazonaws.com%2Fuploads%2Farticles%2Fiowggal6lr2ib7k5rtex.png" alt=" " width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://extendsclass.com/api-tester.html" rel="noopener noreferrer"&gt;ExtendsClass API Tester&lt;/a&gt; is a simple yet effective online HTTP client. Ideal for fast requests, formatted responses, and code generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. &lt;a href="https://apipheny.io/" rel="noopener noreferrer"&gt;Apipheny&lt;/a&gt; Data-Driven API Testing and API Debugging in Google Sheets
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fn5xepn0nvndjmie12vwj.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.amazonaws.com%2Fuploads%2Farticles%2Fn5xepn0nvndjmie12vwj.png" alt=" " width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apipheny.io/" rel="noopener noreferrer"&gt;Apipheny&lt;/a&gt; connects APIs directly to Google Sheets. Perfect for analysts and developers who need to pull, refresh, and validate API data easily.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. &lt;a href="https://www.telerik.com/fiddler" rel="noopener noreferrer"&gt;Telerik Fiddler Everywhere&lt;/a&gt; Deep Traffic Inspection for Advanced API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fyfqbxpdexx061rtjn7rd.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.amazonaws.com%2Fuploads%2Farticles%2Fyfqbxpdexx061rtjn7rd.png" alt=" " width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.telerik.com/fiddler" rel="noopener noreferrer"&gt;Telerik Fiddler Everywhere&lt;/a&gt; captures and inspects HTTP/HTTPS traffic with precision. Powerful for complex &lt;strong&gt;API debugging&lt;/strong&gt; scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. &lt;a href="https://insomnia.rest/" rel="noopener noreferrer"&gt;Insomnia&lt;/a&gt; Polished Collaboration for API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Faaflcnnt7ms662ycyaub.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.amazonaws.com%2Fuploads%2Farticles%2Faaflcnnt7ms662ycyaub.png" alt=" " width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://insomnia.rest/" rel="noopener noreferrer"&gt;Insomnia&lt;/a&gt; offers a beautiful UI, strong GraphQL support, plugins, and team features. Excellent for both solo and collaborative &lt;strong&gt;API testing and API debugging&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. &lt;a href="https://www.usebruno.com/" rel="noopener noreferrer"&gt;Bruno&lt;/a&gt; Git-Native Version-Controlled API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fo9i71ak9wkx2ktjm1vpb.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.amazonaws.com%2Fuploads%2Farticles%2Fo9i71ak9wkx2ktjm1vpb.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.usebruno.com/" rel="noopener noreferrer"&gt;Bruno&lt;/a&gt; stores collections as plain text files in Git. Ideal for teams that want full version control and offline-first &lt;strong&gt;API testing and API debugging&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  16. &lt;a href="https://www.thunderclient.com/" rel="noopener noreferrer"&gt;Thunder Client&lt;/a&gt; Lightweight In-Editor API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Frwzmd37ot5ygwlkzjyea.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.amazonaws.com%2Fuploads%2Farticles%2Frwzmd37ot5ygwlkzjyea.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.thunderclient.com/" rel="noopener noreferrer"&gt;Thunder Client&lt;/a&gt; integrates directly into VS Code. Fast and context-aware reduces tab switching for many developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  17. &lt;a href="https://www.soapui.org/" rel="noopener noreferrer"&gt;SoapUI&lt;/a&gt; Enterprise-Grade Automation for Comprehensive API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Ft9a2q4d4wbh3xemesgkn.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.amazonaws.com%2Fuploads%2Farticles%2Ft9a2q4d4wbh3xemesgkn.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.soapui.org/" rel="noopener noreferrer"&gt;SoapUI&lt;/a&gt; remains strong for SOAP, REST, functional, security, and load testing in enterprise environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  18. &lt;a href="https://keploy.io/" rel="noopener noreferrer"&gt;Keploy&lt;/a&gt; AI-Powered Automatic Test Generation for API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2F5sqfto4q89dujprz6yny.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.amazonaws.com%2Fuploads%2Farticles%2F5sqfto4q89dujprz6yny.png" alt=" " width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://keploy.io/" rel="noopener noreferrer"&gt;Keploy&lt;/a&gt; records real traffic and auto-generates tests and mocks. Excellent for boosting coverage in CI/CD pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  19. &lt;a href="https://scalar.com/" rel="noopener noreferrer"&gt;Scalar&lt;/a&gt; Beautiful OpenAPI-Centric API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fulbl9bbh48z8ebrdz11v.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.amazonaws.com%2Fuploads%2Farticles%2Fulbl9bbh48z8ebrdz11v.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://scalar.com/" rel="noopener noreferrer"&gt;Scalar&lt;/a&gt; provides elegant interactive documentation combined with strong testing capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  20. &lt;a href="https://rapidapi.com/" rel="noopener noreferrer"&gt;RapidAPI&lt;/a&gt; Marketplace + Testing Platform for API Testing and API Debugging
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fvdsvw2e8h0a14n7fip3v.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.amazonaws.com%2Fuploads%2Farticles%2Fvdsvw2e8h0a14n7fip3v.png" alt=" " width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rapidapi.com/" rel="noopener noreferrer"&gt;RapidAPI&lt;/a&gt; helps discover, test, and manage thousands of APIs in one place useful for both consumers and providers.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Choose the Right Tool for Your API Testing and API Debugging Needs
&lt;/h2&gt;

&lt;p&gt;Consider your team size, primary protocols (REST/GraphQL/WebSocket), collaboration needs, Git workflow preferences, and budget. Always test tools with your actual APIs the best one is the one that fits &lt;em&gt;your&lt;/em&gt; workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices for Effective API Testing and API Debugging
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Organize collections logically&lt;/li&gt;
&lt;li&gt;Use environment variables consistently&lt;/li&gt;
&lt;li&gt;Add schema validation and performance assertions&lt;/li&gt;
&lt;li&gt;Combine tools when beneficial (e.g., Apidog for collaboration + Bruno for Git)&lt;/li&gt;
&lt;li&gt;Review and update tests regularly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;2026 offers excellent choices for &lt;strong&gt;API testing and API debugging&lt;/strong&gt;. The days of being stuck with one restrictive tool are behind us.&lt;/p&gt;

&lt;p&gt;Have you tried any of these recently? What is your current go-to tool for &lt;strong&gt;API testing and API debugging&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Download Apidog for free here&lt;/strong&gt;: &lt;a href="http://apidog.com/?utm_source=dev.to&amp;amp;utm_medium=herve&amp;amp;utm_content=postman-alternatives"&gt;Apidog&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drop your thoughts, experiences, and recommendations in the comments. Happy testing and debugging!&lt;/p&gt;




</description>
      <category>ai</category>
      <category>api</category>
      <category>testing</category>
      <category>debugging</category>
    </item>
    <item>
      <title>I tested 5 managed video APIs back-to-back — here's the rig and what shipped</title>
      <dc:creator>Mason K</dc:creator>
      <pubDate>Tue, 12 May 2026 13:29:49 +0000</pubDate>
      <link>https://dev.to/masonwritescode/i-tested-5-managed-video-apis-back-to-back-heres-the-rig-and-what-shipped-15hf</link>
      <guid>https://dev.to/masonwritescode/i-tested-5-managed-video-apis-back-to-back-heres-the-rig-and-what-shipped-15hf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📦 Code: github.com/USER/video-api-bakeoff — replace before publishing.&lt;/p&gt;
&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Same source file, same network, same code. I built a tiny Node + React rig that uploads a test video to five managed video APIs (Mux, Cloudflare Stream, api.video, FastPix, AWS), waits for ready, and logs upload time, time-to-ready, and time-to-first-frame. The result isn't a single winner — it's a decision tree.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;A repeatable test harness, end-to-end:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Node script that uploads &lt;code&gt;tearsofsteel.mp4&lt;/code&gt; to each provider's create-asset endpoint, polls for &lt;code&gt;ready&lt;/code&gt;, and logs timing.&lt;/li&gt;
&lt;li&gt;A small React page that loads the resulting HLS manifest in HLS.js and measures time-to-first-frame from the browser.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;results.csv&lt;/code&gt; you can run yourself and disagree with me from.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You'll want Node 22, FFmpeg 7.0+ for the source generation, and an account on each provider. Yes, that's five signups. They're all self-serve and most have free credits, so it's an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Set up the project 🛠️
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;video-api-bakeoff &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;video-api-bakeoff
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;dotenv axios form-data hls.js
&lt;span class="nb"&gt;mkdir &lt;/span&gt;scripts samples results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop a &lt;code&gt;.env&lt;/code&gt; next to &lt;code&gt;package.json&lt;/code&gt;:&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="c"&gt;# .env&lt;/span&gt;
&lt;span class="nv"&gt;MUX_TOKEN_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
&lt;span class="nv"&gt;MUX_TOKEN_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
&lt;span class="nv"&gt;CF_ACCOUNT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
&lt;span class="nv"&gt;CF_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
&lt;span class="nv"&gt;APIVIDEO_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
&lt;span class="nv"&gt;FASTPIX_TOKEN_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
&lt;span class="nv"&gt;FASTPIX_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate the source file the same way for every test so you're not biasing anyone:&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="c"&gt;# scripts/make-source.sh&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; tearsofsteel-1080p.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; medium &lt;span class="nt"&gt;-crf&lt;/span&gt; 18 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 128k &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart &lt;span class="se"&gt;\&lt;/span&gt;
  samples/source.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-movflags +faststart&lt;/code&gt; moves the moov atom to the front of the file. Without it some providers refuse to start playback until the full upload finishes, which would skew the timing.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Build the upload harness ⚙️
&lt;/h2&gt;

&lt;p&gt;The pattern is identical across providers — create the asset, push the file, poll for ready. Here's the shape:&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="c1"&gt;// scripts/upload.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv/config&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;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&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;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;performance&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="s1"&gt;node:perf_hooks&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;timed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fn&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;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;fn&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;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t0&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;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;label&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;ms&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;s`&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&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;pollUntilReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checkFn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt; &lt;span class="o"&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;intervalMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opts&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;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&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="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;deadline&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;ready&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;checkFn&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;ready&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;ready&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="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="nx"&gt;intervalMs&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timed out waiting for ready&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each provider, you wire one &lt;code&gt;upload&lt;/code&gt; function and one &lt;code&gt;pollReady&lt;/code&gt; function. Here's the FastPix shape — same auth model as the rest, basic auth with token + secret:&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="c1"&gt;// scripts/providers/fastpix.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&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;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.fastpix.io/v1&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;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;username&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;FASTPIX_TOKEN_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;password&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;FASTPIX_SECRET_KEY&lt;/span&gt;&lt;span class="p"&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;createOnDemand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sourceUrl&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;data&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;axios&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/on-demand`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&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="s1"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sourceUrl&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="na"&gt;playbackPolicies&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="s1"&gt;public&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="nx"&gt;auth&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;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// { id, playback: { ids: [{ id: 'pid_xxx' }] } }&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;pollReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assetId&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;data&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;axios&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/on-demand/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;assetId&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;auth&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;data&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="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;'&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="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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;playbackUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playbackId&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="s2"&gt;`https://stream.fastpix.io/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;playbackId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.m3u8`&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;The Mux, api.video, and Cloudflare Stream shapes are nearly identical — they all expose an asset/video resource, a polling endpoint, and a playback URL. AWS is the odd one out: you'll be wiring &lt;code&gt;MediaConvert&lt;/code&gt; → &lt;code&gt;S3&lt;/code&gt; output → &lt;code&gt;CloudFront&lt;/code&gt; distribution yourself. Plan for an hour.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Note: Don't bake the access token into client-side code. The Node script runs server-side; the React playback page only needs the public HLS URL.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Measure time-to-ready 📊
&lt;/h2&gt;

&lt;p&gt;The harness loops over providers, uploads, polls until ready, and logs:&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="c1"&gt;// scripts/run.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;providers&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="s1"&gt;./providers/index.js&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;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&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;SOURCE&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;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;samples/source.mp4&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;results&lt;/span&gt; &lt;span class="o"&gt;=&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providers&lt;/span&gt;&lt;span class="p"&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uploadMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;asset&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="nf"&gt;timed&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;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] upload`&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;p&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="nx"&gt;SOURCE&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="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;readyMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ready&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="nf"&gt;timed&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;name&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="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;pollUntilReady&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;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pollReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;asset&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="na"&gt;intervalMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;upload_s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uploadMs&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;total_to_ready_s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;uploadMs&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;readyMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;playback_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;playbackUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ready&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="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;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;`[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&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;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="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&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;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;results/server.json&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;results&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="mi"&gt;2&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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--experimental-fetch&lt;/span&gt; scripts/run.js samples/source.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect output that looks roughly like:&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="o"&gt;[&lt;/span&gt;mux]       upload: 47.73s
&lt;span class="o"&gt;[&lt;/span&gt;mux]       ready:   5.58s
&lt;span class="o"&gt;[&lt;/span&gt;fastpix]   upload: 15.20s
&lt;span class="o"&gt;[&lt;/span&gt;fastpix]   ready:  14.22s
&lt;span class="o"&gt;[&lt;/span&gt;apivideo]  upload: 16.98s
&lt;span class="o"&gt;[&lt;/span&gt;apivideo]  ready:  32.23s
&lt;span class="o"&gt;[&lt;/span&gt;gumlet]    upload: 24.82s
&lt;span class="o"&gt;[&lt;/span&gt;gumlet]    ready:  244.07s
&lt;span class="o"&gt;[&lt;/span&gt;cloudflare] upload: 21.40s
&lt;span class="o"&gt;[&lt;/span&gt;cloudflare] ready:   8.10s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Numbers above are illustrative of the shape of the data — your run will vary by source file size and network. The Mux/FastPix/api.video numbers for a 177.2 MB TearsOfSteel test over 4G are publicly tracked at video-benchmark.fpvideo.co if you want a public reference point.)&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Measure time-to-first-frame in the browser 🎬
&lt;/h2&gt;

&lt;p&gt;Time-to-ready isn't the same as the viewer's first frame. For that you need the actual player. Drop this in a React page:&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="c1"&gt;// app/components/Probe.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&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="s1"&gt;react&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;Hls&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hls.js&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Probe&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;provider&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&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="nl"&gt;provider&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;videoRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLVideoElement&lt;/span&gt;&lt;span class="o"&gt;&amp;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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ttff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTtff&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;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="nf"&gt;useEffect&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;videoRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;hls&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;Hls&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;enableWorker&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="nx"&gt;hls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadSource&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;hls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;videoRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;onPlaying&lt;/span&gt; &lt;span class="o"&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="nf"&gt;setTtff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;videoRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPlaying&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&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="nx"&gt;videoRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&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="o"&gt;=&amp;gt;&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;hls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&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;url&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h3&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;provider&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;TTFF&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ttff&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;ttff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt;ms`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h3&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;videoRef&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;controls&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;640&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;Use the HLS.js 1.6.x line — it's the current stable, with LL-HLS support baked in. (1.7.0-alpha exists but I'd hold off for a benchmark.)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Tip: Run the page on a throttled connection (Chrome DevTools → Network → Slow 3G or Fast 3G) to compare cold-start behavior. Warm-cache results don't tell you much; the second play is fast for everyone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. What I actually found 🔍
&lt;/h2&gt;

&lt;p&gt;A summary table from the run (your numbers will differ — but this is the shape of the data):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Upload&lt;/th&gt;
&lt;th&gt;Time-to-ready&lt;/th&gt;
&lt;th&gt;Cold TTFF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FastPix&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;td&gt;mid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mux&lt;/td&gt;
&lt;td&gt;slow&lt;/td&gt;
&lt;td&gt;mid&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;api.video&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;td&gt;mid&lt;/td&gt;
&lt;td&gt;mid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Stream&lt;/td&gt;
&lt;td&gt;mid&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;td&gt;mid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS (MediaConvert)&lt;/td&gt;
&lt;td&gt;(DIY)&lt;/td&gt;
&lt;td&gt;(DIY)&lt;/td&gt;
&lt;td&gt;(DIY)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The reproducible takeaway: &lt;strong&gt;measure the thing the user sees on the device the user uses&lt;/strong&gt;. Time-to-ready matters during ingest (creator workflow). Cold TTFF matters at playback. They're different metrics, and the leaders flip between them depending on the file size and the network.&lt;/p&gt;

&lt;p&gt;For the 177.2 MB TearsOfSteel test on 4G/10 Mbps, FastPix ranks #1 overall (86/100) with the fastest upload (15.2s vs Mux 47.7s) and fastest time-to-ready (29.4s vs Mux 53.3s). Mux still has the fastest cold startup (905ms vs FastPix 1.9s) and the best viewer-experience composite (94 vs 81). Honest counterpoint: on a smaller 64.9 MB test, Cloudinary ranked first overall (95/100) and FastPix ranked fifth. The take-home is that platform strengths cluster by file size — bench against your actual workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Pricing notes that matter for benchmarking 💵
&lt;/h2&gt;

&lt;p&gt;Cost isn't really a per-API benchmark, but it changes what "best" means for your project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Stream:&lt;/strong&gt; $5 per 1,000 min stored, $1 per 1,000 min delivered, encoding free, ingress free. JavaScript player only, no DRM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mux Video:&lt;/strong&gt; per-minute encoding + delivery. &lt;strong&gt;Mux Data analytics is a separate SKU&lt;/strong&gt; — Media plan starts at $499/month for 1M monitoring views.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;api.video:&lt;/strong&gt; free encoding for unlimited minutes; storage as low as $0.00285/min; delivery as low as $0.0017/min.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.fastpix.io/pricing/video-live-stream" rel="noopener noreferrer"&gt;&lt;strong&gt;FastPix:&lt;/strong&gt;&lt;/a&gt; encoding is free on the standard plan; delivery ~$0.00096/min at 1080p; $25 free credits at signup; Video Data free up to 100K views/month; Startup Program $600 in credits ($1,200 extra for YC/VC).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS:&lt;/strong&gt; per-service billing across MediaConvert, S3, MediaPackage, CloudFront. Cheapest &lt;em&gt;if&lt;/em&gt; your team already runs AWS at depth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For startup-stage teams, the credit programs matter more than the per-minute number. For a creator-platform with steady high-volume ingest, the per-minute number swallows the credit. Run the actual math for your shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The rig is the point. Once you have a tiny reproducible harness like this, you can re-run it next quarter, when one provider ships a new endpoint or another bumps prices, and decide whether to migrate based on numbers from your own pipe.&lt;/p&gt;

&lt;p&gt;If you want the "least friction to ship" answer for a new project: pick the provider whose docs you can finish reading in a single sitting. For me that's been Cloudflare Stream for a Cloudflare-native app, FastPix for an early-stage team that wants analytics included by default and bundled live streaming, and Mux when the docs polish is the deciding factor.&lt;/p&gt;

&lt;p&gt;The point of the bake-off isn't to crown a winner. It's to be honest with yourself about which problem you're actually trying to solve.&lt;/p&gt;

</description>
      <category>video</category>
      <category>webdev</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Microsoft Graph API — What the Docs Don't Tell You About OneNote Rate Limiting</title>
      <dc:creator>Ensky</dc:creator>
      <pubDate>Tue, 12 May 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/ensky/microsoft-graph-api-what-the-docs-dont-tell-you-about-onenote-rate-limiting-2lgl</link>
      <guid>https://dev.to/ensky/microsoft-graph-api-what-the-docs-dont-tell-you-about-onenote-rate-limiting-2lgl</guid>
      <description>&lt;p&gt;I've been building &lt;a href="https://note-bridge.co/" rel="noopener noreferrer"&gt;Note Bridge&lt;/a&gt; for a while now — a tool that migrates OneNote notebooks to Notion. Going in, I assumed the hardest part would be content conversion — correctly translating OneNote's complex HTML into Notion. Turns out, dealing with Microsoft's rate limiting ended up taking even more time.&lt;/p&gt;

&lt;p&gt;This isn't a "here is a backoff snippet, paste it in" post. The Graph API has enough edge cases that a generic retry loop won't save you — you eventually need to model the limit system, not just react to 429s. Here's what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Rate Limits Across 3 × 2 Dimensions
&lt;/h2&gt;

&lt;p&gt;OneNote's rate limiting isn't a simple cap. It has three dimensions — per-minute, per-hour, and concurrent requests — and each of those is enforced at two scopes: per-user and per-app [1]. Multiply them out and you get six separate limits to respect, or 429s come knocking immediately.&lt;/p&gt;

&lt;p&gt;This means the common polynomial backoff strategy isn't enough: you might not be hitting the per-minute limit, but you're tripping the per-hour ceiling. Or a single user is fine, but the total across all users hits the app-wide cap. Each dimension needs its own accounting.&lt;/p&gt;

&lt;p&gt;That said, real-world experience shows the actual limits seem more generous than what the docs specify. But we code to the spec anyway — if Microsoft ever decides to enforce strictly, we don't want things to break.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. No Retry-After Header
&lt;/h2&gt;

&lt;p&gt;Most well-designed APIs include a &lt;code&gt;Retry-After&lt;/code&gt; header with their 429 responses so you know exactly when it's safe to retry. Not sure if it's because the mechanism is too complex, but the OneNote Graph API doesn't support this.&lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;Retry-After&lt;/code&gt;, simple backoff strategies don't cut it. You can increase wait time after each 429, but there's no guarantee the next attempt won't get throttled too. For a production application, this is a real problem. The only robust solution is to implement a rate limiter that follows Microsoft's spec — we built ours using Cloudflare Durable Objects, tracking usage across every dimension and implementing our own &lt;code&gt;Retry-After&lt;/code&gt; for both frontend and backend to consume.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Two Headers Almost Nobody Uses
&lt;/h2&gt;

&lt;p&gt;After we built the limiter, we discovered Graph actually does ship two response headers that help — they're just easy to miss because they're not on the 429 path.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;x-ms-throttle-limit-percentage&lt;/code&gt;&lt;/strong&gt; — present on &lt;em&gt;successful&lt;/em&gt; responses. It tells you how close you are to a limit (0.0–1.0). Above ~0.8 is a yellow zone; at 1.0 the next request will probably 429.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;x-ms-throttle-scope&lt;/code&gt;&lt;/strong&gt; — present on 429 responses. Values are &lt;code&gt;User&lt;/code&gt;, &lt;code&gt;Application&lt;/code&gt;, or both. This tells you &lt;em&gt;which&lt;/em&gt; of the two scopes you tripped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These changed the design. The first lets us trip a soft circuit breaker &lt;em&gt;before&lt;/em&gt; getting a 429, instead of waiting to be told. The second lets us route the 429 to the right breaker — if it's an application-scope trip, slowing one user down doesn't help; we have to slow every user down. If it's user-scope, the other tenants are fine to keep going.&lt;/p&gt;

&lt;p&gt;If you only react to 429 status codes, you're flying half-blind on a spec-compliant rate limiter.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Batch API Doesn't Really Help
&lt;/h2&gt;

&lt;p&gt;Microsoft Graph offers a batch mechanism to bundle multiple requests into one HTTP call. Intuitively this should count as a single request, but it doesn't. Each inner request in the batch is counted individually against rate limits [2].&lt;/p&gt;

&lt;p&gt;This makes &lt;code&gt;$batch&lt;/code&gt; largely pointless for our use case — at most it saves a bit of round-trip time.&lt;/p&gt;

&lt;p&gt;There's also a subtler trap. A &lt;code&gt;$batch&lt;/code&gt; POST can succeed with HTTP 200 at the outer level, while individual sub-responses inside it return 429, 502, or 503. We watched a production migration silently drop two sections because our retry loop only retried the outer batch — sub-response 5xx errors fell on the floor. If you use &lt;code&gt;$batch&lt;/code&gt;, the retry strategy has to operate on each sub-response, not the envelope.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The Death Spiral
&lt;/h2&gt;

&lt;p&gt;The most painful incident we ran into wasn't 429s — it was what happened &lt;em&gt;after&lt;/em&gt; the 429s. A user kicked off a 1,300-page migration. The first 200 pages went through fine. Then a 429. Our consumer caught it, asked Cloudflare Queues for a 30-second retry. So far so reasonable.&lt;/p&gt;

&lt;p&gt;But because all 1,100 remaining messages were already enqueued, every one of them woke up around the same time after that 30-second wait, slammed back into the rate limit, and got 429'd again. Each new wave of 429s extended the throttle window further. The graph showed a beautiful self-reinforcing loop: rate limit → retries → rate limit → more retries. No page made forward progress for half an hour.&lt;/p&gt;

&lt;p&gt;Two fixes turned out to matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A real circuit breaker at the limiter, not at each call site.&lt;/strong&gt; When the upstream returns sustained 429s, the limiter trips and &lt;em&gt;every&lt;/em&gt; caller gets 429'd locally for a cooldown window. This collapses N independent retry loops into one shared wait, instead of N callers each doing 30 retries × 5 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foreground vs. background separation.&lt;/strong&gt; Note Bridge has two workloads hitting Graph: interactive notebook scans (a user is waiting) and background migration jobs (no one is staring at the screen). If a 1,300-page migration drains the rate budget, the user trying to scan their notebook waits forever. Our fix is two rate limiter "lanes" with the background lane capped at ~50% of the budget. Foreground always has headroom.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  6. The Subtle retryAfterMs Bug
&lt;/h2&gt;

&lt;p&gt;One worth calling out because it took us a while to see. When the limiter checks two layers (per-user and per-app) and both are saturated, what &lt;code&gt;retryAfterMs&lt;/code&gt; should it return to the caller?&lt;/p&gt;

&lt;p&gt;We had &lt;code&gt;Math.min(userRetry, appRetry)&lt;/code&gt; — return the shorter wait, sounds friendly. It was wrong. If user-scope says "wait 2 seconds" but app-scope says "wait 30 seconds", retrying after 2 seconds gets you another instant 429. The right answer is &lt;code&gt;Math.max(userRetry, appRetry)&lt;/code&gt; — the request is only safe to send when &lt;em&gt;both&lt;/em&gt; layers have capacity. A one-character fix that ended a category of phantom retries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Most rate-limiting tutorials end at "retry with exponential backoff and jitter." For Microsoft Graph that's not enough. You need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model the 3 × 2 limit matrix instead of guessing.&lt;/li&gt;
&lt;li&gt;Read &lt;code&gt;x-ms-throttle-limit-percentage&lt;/code&gt; and trip a soft breaker before you 429.&lt;/li&gt;
&lt;li&gt;Route 429s using &lt;code&gt;x-ms-throttle-scope&lt;/code&gt; so user trips don't punish the whole app (and vice versa).&lt;/li&gt;
&lt;li&gt;Retry inside &lt;code&gt;$batch&lt;/code&gt; sub-responses, not just on the envelope.&lt;/li&gt;
&lt;li&gt;Use a &lt;em&gt;shared&lt;/em&gt; circuit breaker, or your retry storms will outlast the actual throttle window.&lt;/li&gt;
&lt;li&gt;Separate foreground from background so interactive UX doesn't starve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Microsoft Graph's lack of &lt;code&gt;Retry-After&lt;/code&gt; is the headline annoyance, but the deeper lesson is that rate limiting is a system, not a header. Build it like one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[1] &lt;a href="https://learn.microsoft.com/en-us/graph/throttling-limits#onenote-service-limits" rel="noopener noreferrer"&gt;Microsoft Graph API spec&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://learn.microsoft.com/en-us/graph/json-batching?tabs=http#batch-size-limitations" rel="noopener noreferrer"&gt;Batch API spec&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://note-bridge.co/" rel="noopener noreferrer"&gt;&lt;em&gt;Note Bridge&lt;/em&gt;&lt;/a&gt; &lt;em&gt;migrates OneNote notebooks to Notion.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>microsoftgraph</category>
      <category>webdev</category>
      <category>cloudflarechallenge</category>
      <category>api</category>
    </item>
    <item>
      <title>Your Pipeline Is 28.6h Behind: Catching Software Sentiment Leads with Pulsebit</title>
      <dc:creator>Pulsebit News Sentiment API</dc:creator>
      <pubDate>Tue, 12 May 2026 12:33:10 +0000</pubDate>
      <link>https://dev.to/pulsebitapi/your-pipeline-is-286h-behind-catching-software-sentiment-leads-with-pulsebit-50n0</link>
      <guid>https://dev.to/pulsebitapi/your-pipeline-is-286h-behind-catching-software-sentiment-leads-with-pulsebit-50n0</guid>
      <description>&lt;h1&gt;
  
  
  Your Pipeline Is 28.6h Behind: Catching Software Sentiment Leads with Pulsebit
&lt;/h1&gt;

&lt;p&gt;We just spotted a fascinating anomaly: a 24h momentum spike of -0.713 in the software sector. This drop signals a notable shift in sentiment, and it’s crucial for us to pay attention to the underlying themes driving this change. Our analysis indicates a strong focus on acquisitions, particularly around the recent news of Rocket Software completing its acquisition of Vertica from OpenText, which has been clustered into two articles. This could be a pivotal moment, so how do we catch these shifts in our data pipelines?&lt;/p&gt;

&lt;p&gt;When your pipeline doesn’t account for multilingual origins or dominant entities, you risk missing crucial insights. In this case, your model missed this spike by 28.6 hours, specifically trailing the English press coverage that had zero lag time. If you’re only looking at data in one language or ignoring dominant narratives, you could be seriously out of sync with emerging trends. This isn’t just a theoretical issue; it’s a real gap that can cost you valuable time and insights.&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.amazonaws.com%2Fuploads%2Farticles%2Fo9egd9sffxeva25d1779.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.amazonaws.com%2Fuploads%2Farticles%2Fo9egd9sffxeva25d1779.png" alt="English coverage led by 28.6 hours. No at T+28.6h. Confidenc" width="800" height="423"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;English coverage led by 28.6 hours. No at T+28.6h. Confidence scores: English 0.85, French 0.85, Spanish 0.85 Source: Pulsebit /sentiment_by_lang.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let’s get into the code that can help us catch these sentiment shifts in real-time. Here’s a snippet that demonstrates how to leverage our API to filter by geographic origin and run a meta-sentiment analysis.&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;software&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="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_1778589189562&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;software&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="c1"&gt;# Step 1: Geographic origin filter
&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;software&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bearer YOUR_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;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="n"&gt;topic&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;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.713&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;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.600&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://api.pulsebit.com/v1/sentiment&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;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;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Step 2: Meta-sentiment moment
&lt;/span&gt;&lt;span class="n"&gt;cluster_reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clustered by shared themes: consolidated, sonata, software, final, dividend.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;meta_sentiment&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://api.pulsebit.com/v1/sentiment&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;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;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;cluster_reason&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;meta_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meta_sentiment&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code does two critical things: first, it pulls sentiment data filtered for English articles about software, and second, it runs the narrative framing of our clustered articles back through our API for a sentiment score. This dual approach keeps us attuned to the evolving landscape, ensuring we don’t miss critical shifts.&lt;/p&gt;

&lt;p&gt;Now, let’s talk about three specific builds we can implement using this sentiment spike as a signal.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Geo-Filtered Alert System&lt;/strong&gt;: Set up an alert that triggers when the momentum for the 'software' topic falls below a specific threshold (e.g., -0.600). Use the geographic filter to ensure you’re only alerted based on English-language sentiment.&lt;/li&gt;
&lt;/ol&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.amazonaws.com%2Fuploads%2Farticles%2F0fwmvrrj417ttj2ukpk9.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.amazonaws.com%2Fuploads%2Farticles%2F0fwmvrrj417ttj2ukpk9.png" alt="Geographic detection output for software. India leads with 1" width="800" height="424"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Geographic detection output for software. India leads with 1 articles and sentiment +0.85. Source: Pulsebit /news_recent geographic fields.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Meta-Sentiment Dashboard&lt;/strong&gt;: Create a dashboard that visualizes meta-sentiment scores for clustered narratives, focusing on emerging trends like “software” and “Google.” This will allow us to see how framing influences public perception and sentiment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sentiment Comparison Engine&lt;/strong&gt;: Build an endpoint that compares the current sentiment of articles against a historical baseline for topics like "software" or "Google." Incorporate the meta-sentiment scores to evaluate narrative strength against mainstream themes such as "consolidated" or "sonata."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By implementing these builds, we leverage the data from our API to make informed decisions faster, staying ahead of the curve on trends that could impact our strategies.&lt;/p&gt;

&lt;p&gt;Ready to dive in? Check out our documentation at pulsebit.lojenterprise.com/docs. With this code snippet, you can get started and have your system running in under 10 minutes. Let’s catch those sentiment shifts together!&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>datascience</category>
      <category>nlp</category>
    </item>
    <item>
      <title>I shipped a live integration sandbox in 90 minutes instead of taking the partnership call. It changed the conversation.</title>
      <dc:creator>Perufitlife</dc:creator>
      <pubDate>Tue, 12 May 2026 12:15:35 +0000</pubDate>
      <link>https://dev.to/perufitlife/i-shipped-a-live-integration-sandbox-in-90-minutes-instead-of-taking-the-partnership-call-it-48df</link>
      <guid>https://dev.to/perufitlife/i-shipped-a-live-integration-sandbox-in-90-minutes-instead-of-taking-the-partnership-call-it-48df</guid>
      <description>&lt;p&gt;A CEO sent me a DM yesterday afternoon. Aviation learning platform. Wanted to talk about a partnership with my smaller aviation API (Rotatepilot — question banks, METAR decoder, airport lookups). Real founder, real platform, real intent.&lt;/p&gt;

&lt;p&gt;I had two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reply with my usual qualifying questions, schedule a 30 min call later in the week, do the meeting, agree on what to build, build it, send it for review, iterate.&lt;/li&gt;
&lt;li&gt;Build it now and send the link.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I went with option 2. 90 minutes of work. The conversation flipped from "show me what this looks like" to "what's the commercial shape."&lt;/p&gt;

&lt;p&gt;Here's how, and why I'll do this for every inbound partnership conversation from now on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DM that triggered this
&lt;/h2&gt;

&lt;p&gt;From the CEO of SkyX International (aviation learning platform, Public Beta on Product Hunt):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We are interested in your RotatePilot website, since it includes crucial tools that we think can help us develop a large selection of aviation tools. Your data bank (ATPL/PPL questions, API…) also fascinates us. We think a partnership between both platforms could benefit you and us. It could either be an API, embed, or any kind of licensing partnership. We are open and flexible. Before engaging into anything, I invite you to take a look at our platform, test out the features as you want, and feel free to give your suggestions."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Translation: "I want to integrate your API but I don't know what your API looks like yet, and you don't know what we'd actually consume."&lt;/p&gt;

&lt;p&gt;The default founder-to-founder script here is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Great, here's a Calendly link, let's do 30 min."&lt;/li&gt;
&lt;li&gt;On the call, you screen-share your API docs, they ask questions, you both end with "send me a sample integration."&lt;/li&gt;
&lt;li&gt;You build the sample. You send it. They review. Maybe respond in a week. Maybe not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I've done that loop probably 20 times in the last year. The conversion rate is awful. Most never come back. The ones that do, come back 2-3 weeks later by which time the energy has dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did instead
&lt;/h2&gt;

&lt;p&gt;Spent 5 minutes browsing their platform (onboarding.skyxintl.me). Took specific notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They list "structured learning pathways + spaced repetition" but no question bank size visible.&lt;/li&gt;
&lt;li&gt;They list "METAR decoder" as a feature.&lt;/li&gt;
&lt;li&gt;I didn't see a language switcher on the landing — looked English-only.&lt;/li&gt;
&lt;li&gt;They're pre-launch (countdown timer at 00:00:00:00).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are gaps where my API fills in. Not theoretical fit, actual fit, with specifics.&lt;/p&gt;

&lt;p&gt;Then I built &lt;a href="https://github.com/Perufitlife/rotatepilot-skyx-sandbox" rel="noopener noreferrer"&gt;&lt;code&gt;rotatepilot-skyx-sandbox&lt;/code&gt;&lt;/a&gt; — a single HTML file that hits four of my public REST endpoints live in the browser and renders the responses in a learning-platform style UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Question bank&lt;/strong&gt; — &lt;code&gt;GET /api/v1/question?subject=meteorology&amp;amp;count=2&lt;/code&gt; — returns FAA-labeled MCQs with explanations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;METAR decoder&lt;/strong&gt; — &lt;code&gt;GET /api/v1/metar?icao=KJFK&lt;/code&gt; — returns decoded flight category, wind, visibility, cloud layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airport lookup&lt;/strong&gt; — &lt;code&gt;GET /api/v1/airport/KSFO&lt;/code&gt; — returns name, ICAO, runways, training-airport flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quiz of the day&lt;/strong&gt; — &lt;code&gt;GET /api/v1/quiz-of-day&lt;/code&gt; — returns one shared question for daily-engagement features.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing is one file. No build step. No backend. Calls go straight from the browser to my edge-served endpoints (CORS enabled, no auth tier needed at this volume). Open DevTools and you see the real requests.&lt;/p&gt;

&lt;p&gt;Stack: ~300 lines of HTML/CSS/vanilla JS. Pushed to GitHub. Enabled GitHub Pages with &lt;code&gt;gh api -X POST repos/.../pages -f "source[branch]=master"&lt;/code&gt;. Live in 90 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://perufitlife.github.io/rotatepilot-skyx-sandbox/" rel="noopener noreferrer"&gt;Live demo&lt;/a&gt; — paste any ICAO, pick any subject, see real responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I sent back to the CEO
&lt;/h2&gt;

&lt;p&gt;Three messages, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Qualifying questions&lt;/strong&gt; — ranked top-2 pieces, format (API vs embed vs license), 6-month volume guess, commercial direction. Plus a 30-min call offer for later in the week if he preferred async.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Substantive feedback on their platform&lt;/strong&gt; — what looked strong, three peer-pushback notes (the countdown timer reads 00:00:00:00, no product screenshot above the fold, Product Hunt badge buried in footer), and a concrete integration proposal direction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The sandbox link&lt;/strong&gt; — "Instead of waiting on the call to show what the JSON contract looks like, I went ahead and built the sandbox. It's a single static page that hits 4 endpoints live. Take a look when you have 5 min — would help us skip a lot of 'what does it look like' on the call and jump straight to commercial shape."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total time: roughly 90 minutes of real work, including writing the messages and verifying the live endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this changes the conversation
&lt;/h2&gt;

&lt;p&gt;The default partnership conversation has three rounds of friction:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discovery&lt;/strong&gt; — what do you have, what do we want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mapping&lt;/strong&gt; — does the shape match, what would integration cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commercial&lt;/strong&gt; — how do we make money on this.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A sandbox collapses rounds 1 and 2 into a single artifact the other side can open and inspect on their own time. They don't need to wait for a meeting to understand the shape of what you have. You don't need to spend the meeting explaining endpoints. The meeting (if it happens) is just round 3, which is the only round that actually moves the deal.&lt;/p&gt;

&lt;p&gt;It also forces you to do the work &lt;em&gt;before&lt;/em&gt; you've gotten a "yes," which is exactly the work that makes the partnership real. Half the partnerships I've talked about never happened because the work to make them concrete never got done. Doing it on day zero filters out partners who weren't going to follow through anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general pattern
&lt;/h2&gt;

&lt;p&gt;You don't need someone's permission to demonstrate value. The thing that's expensive isn't the meeting, it's the &lt;em&gt;uncertainty&lt;/em&gt; between "we should partner" and "let's actually integrate." If you can collapse the uncertainty in 90 minutes of static-file work, do it.&lt;/p&gt;

&lt;p&gt;This works for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API-led partnerships&lt;/strong&gt; — show your endpoints rendering in their kind of UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed/widget integrations&lt;/strong&gt; — drop an iframe in a styled wrapper that resembles their site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data licensing&lt;/strong&gt; — a quick HTML view of a sample of your dataset with proposed fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold outbound to a specific company&lt;/strong&gt; — same idea, build a custom demo and lead with it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not work for vague "let's chat" inbounds where the asker doesn't know what they want. That's a different problem (qualifying, not delivering).&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools, for replicability
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One HTML file. No framework.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git init &amp;amp;&amp;amp; git add . &amp;amp;&amp;amp; git commit &amp;amp;&amp;amp; gh repo create --public --push&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gh api -X POST "repos/USER/REPO/pages" -f "source[branch]=master" -f "source[path]=/"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Live URL: &lt;code&gt;https://USER.github.io/REPO/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source for the sandbox I built: &lt;a href="https://github.com/Perufitlife/rotatepilot-skyx-sandbox" rel="noopener noreferrer"&gt;github.com/Perufitlife/rotatepilot-skyx-sandbox&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What I'm doing with this pattern next: applying it to every cold outbound I send for my BaaS security auditors. Instead of "want a free scan?" → "here's a sandbox of what a scan of your project might find, formatted as a real audit report." Same effort, 10x higher conversion.&lt;/p&gt;

&lt;p&gt;If you've shipped a sandbox-before-meeting and it worked (or didn't), I'd love to hear what you learned. Reply or DM.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building a portfolio of single-purpose dev tools at &lt;a href="https://github.com/Perufitlife" rel="noopener noreferrer"&gt;github.com/Perufitlife&lt;/a&gt;. Latest: five open-source BaaS security auditors (Supabase, Firebase, PocketBase, Appwrite, Nhost) shipped this week.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>partnership</category>
      <category>api</category>
      <category>buildinpublic</category>
      <category>startup</category>
    </item>
    <item>
      <title>How I Built and Launched a KYC Selfie Verification API in One Day (Free to Try)</title>
      <dc:creator>hajar balirh</dc:creator>
      <pubDate>Tue, 12 May 2026 12:07:08 +0000</pubDate>
      <link>https://dev.to/hajar_balirh_212fa1956ba2/how-i-built-and-launched-a-kyc-selfie-verification-api-in-one-day-free-to-try-a7d</link>
      <guid>https://dev.to/hajar_balirh_212fa1956ba2/how-i-built-and-launched-a-kyc-selfie-verification-api-in-one-day-free-to-try-a7d</guid>
      <description>&lt;p&gt;I've always wanted to build a product I could sell online. &lt;br&gt;
Last week I did it — a full AI-powered KYC API, live in production, &lt;br&gt;
listed on RapidAPI, with a registration system and usage dashboard. &lt;br&gt;
Here's exactly how I built it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is KYC?
&lt;/h2&gt;

&lt;p&gt;KYC (Know Your Customer) is the process of verifying a user's identity. &lt;br&gt;
Every fintech, bank, marketplace, and crypto platform needs it. &lt;br&gt;
Most solutions cost thousands of dollars per month. I built one for free.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the API does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Face detection and centering check&lt;/li&gt;
&lt;li&gt;✅ Liveness detection (real person vs photo of photo)&lt;/li&gt;
&lt;li&gt;✅ Age estimation (18+/21+ flags)&lt;/li&gt;
&lt;li&gt;✅ Glasses and mask detection&lt;/li&gt;
&lt;li&gt;✅ Emotion detection&lt;/li&gt;
&lt;li&gt;✅ Background quality check&lt;/li&gt;
&lt;li&gt;✅ ID document data extraction (name, DOB, document number)&lt;/li&gt;
&lt;li&gt;✅ Selfie vs ID document face matching&lt;/li&gt;
&lt;li&gt;✅ Confidence score 0-100&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; — Python REST API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini Vision&lt;/strong&gt; via OpenRouter — AI image analysis (free tier)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite + SQLAlchemy&lt;/strong&gt; — database for storing verifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Railway&lt;/strong&gt; — deployment (free tier)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The API in action
&lt;/h2&gt;

&lt;p&gt;Get a free API key at:&lt;br&gt;
&lt;a href="https://kyc-selfie-api-production.up.railway.app/register" rel="noopener noreferrer"&gt;https://kyc-selfie-api-production.up.railway.app/register&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then verify a selfie in Python:&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="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;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;https://kyc-selfie-api-production.up.railway.app/v1/verify/selfie&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;x-api-key&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;your-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;files&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;selfie&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;photo.jpg&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;rb&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;result&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;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"approved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"face_detected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"face_centered"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eyes_open"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"liveness_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"age_estimate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appears_18_plus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"glasses_detected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mask_detected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"emotion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"neutral"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"background_quality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"clean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lighting_quality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"good"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Extract data from an ID document
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&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;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;https://kyc-selfie-api-production.up.railway.app/v1/verify/document&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;x-api-key&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;your-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;files&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;document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;open&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_card.jpg&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;rb&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;doc&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;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;Name: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;full_name&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="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;DOB: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;date_of_birth&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="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;Number: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;document_number&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Full KYC — match selfie against ID
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;selfie.jpg&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;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;open&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.jpg&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;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;d&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;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;https://kyc-selfie-api-production.up.railway.app/v1/verify/kyc&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;x-api-key&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;your-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;files&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;selfie&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&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;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;Match approved: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;approved&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="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;Confidence: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Available endpoints
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /v1/verify/selfie&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Selfie analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /v1/verify/kyc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Selfie + ID matching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /v1/verify/document&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extract ID data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /v1/verify/age&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Age estimation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /v1/status/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check past result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /v1/usage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Usage stats&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Calls/month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;$49/mo&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultra&lt;/td&gt;
&lt;td&gt;$99/mo&lt;/td&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Building this in one day taught me a few things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. AI APIs make everything easier.&lt;/strong&gt; Instead of training a custom face &lt;br&gt;
detection model, I used Gemini Vision via a simple API call. The prompt &lt;br&gt;
engineering took 10 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. FastAPI is incredible.&lt;/strong&gt; Auto-generated docs at /docs, &lt;br&gt;
type validation, async support — everything just works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Distribution is harder than building.&lt;/strong&gt; The API took one day. &lt;br&gt;
Getting people to use it is the real challenge.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Arabic and Moroccan CIN support (huge untapped niche)&lt;/li&gt;
&lt;li&gt;Email notifications when verification completes&lt;/li&gt;
&lt;li&gt;React SDK for easy frontend integration&lt;/li&gt;
&lt;li&gt;Video liveness detection&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it free
&lt;/h2&gt;

&lt;p&gt;🔗 Register: &lt;a href="https://kyc-selfie-api-production.up.railway.app/register" rel="noopener noreferrer"&gt;https://kyc-selfie-api-production.up.railway.app/register&lt;/a&gt;&lt;br&gt;
🔗 Live demo: &lt;a href="https://kyc-selfie-api-production.up.railway.app" rel="noopener noreferrer"&gt;https://kyc-selfie-api-production.up.railway.app&lt;/a&gt;&lt;br&gt;
🔗 API docs: &lt;a href="https://kyc-selfie-api-production.up.railway.app/docs" rel="noopener noreferrer"&gt;https://kyc-selfie-api-production.up.railway.app/docs&lt;/a&gt;&lt;br&gt;
🔗 RapidAPI: &lt;a href="https://rapidapi.com/balirh12/api/kyc-selfie-verification" rel="noopener noreferrer"&gt;https://rapidapi.com/balirh12/api/kyc-selfie-verification&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Free plan — 100 verifications/month, no credit card required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with ❤️ using FastAPI, Gemini Vision, and Railway.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Questions? Drop them in the comments!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Code Migration Nobody Wants to Talk About (Until It Breaks in Prod)</title>
      <dc:creator>Nometria</dc:creator>
      <pubDate>Tue, 12 May 2026 11:52:18 +0000</pubDate>
      <link>https://dev.to/nometria_vibecoding/the-code-migration-nobody-wants-to-talk-about-until-it-breaks-in-prod-42bk</link>
      <guid>https://dev.to/nometria_vibecoding/the-code-migration-nobody-wants-to-talk-about-until-it-breaks-in-prod-42bk</guid>
      <description>&lt;h1&gt;
  
  
  Why Your AI-Built App Works in the Builder But Breaks in Production
&lt;/h1&gt;

&lt;p&gt;You shipped an app in Lovable or Bolt in three days. It's fast, it looks good, your early users love it. Then you try to scale it, and suddenly you're staring at database connection limits you didn't know existed, vendor lock-in you can't escape, and a codebase you don't actually own.&lt;/p&gt;

&lt;p&gt;This isn't a failure. This is what happens when you optimize for iteration instead of production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gap Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;AI builders are designed for speed. They hide infrastructure complexity so you can focus on features. That's the value prop. But that same abstraction becomes a cage when you need to own your data, control your deployments, or scale beyond the builder's infrastructure.&lt;/p&gt;

&lt;p&gt;Here's what actually happens:&lt;/p&gt;

&lt;p&gt;Your database lives on the builder's servers. Your code is locked into their export format. You have no rollback mechanism if something breaks. There's no CI/CD pipeline, no deployment history, no real version control. When you hit 1000 concurrent users, you hit their limits, not your own.&lt;/p&gt;

&lt;p&gt;Most founders don't realize this until they're already committed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Cost of Staying Locked In
&lt;/h2&gt;

&lt;p&gt;You can't integrate with your own systems. You can't implement custom authentication. You can't optimize your database queries because you don't control the schema. You can't migrate to cheaper infrastructure. And when the builder changes their pricing or deprecates a feature, you're at their mercy.&lt;/p&gt;

&lt;p&gt;A solo founder I know built a SaaS in Bolt. Six months in, they needed to add a custom integration their customers demanded. The builder didn't support it. They had two choices: rebuild from scratch or stay stuck.&lt;/p&gt;

&lt;p&gt;They rebuilt. Three months of engineering time, gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  There's a Better Path
&lt;/h2&gt;

&lt;p&gt;You don't need to choose between speed and ownership. You can build fast in an AI tool and deploy to real infrastructure without rebuilding anything.&lt;/p&gt;

&lt;p&gt;Export your app, point it at your own database on AWS or Vercel or Supabase, and deploy via CLI in three commands. Keep iterating in the builder while your production code stays on your infrastructure. When you need to customize something, you have the source code.&lt;/p&gt;

&lt;p&gt;A two-person team migrated an Emergent app to Vercel in a single sprint. SmartFixOS moved from Base44 to production and now manages real revenue for a repair business. Wright Choice Mentoring scaled to 10+ organizations after migrating their infrastructure.&lt;/p&gt;

&lt;p&gt;They all kept building fast. They just gained ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Technical Reality Check
&lt;/h2&gt;

&lt;p&gt;Here's what changes: your data moves from the builder's database to yours. Your code gets version control. You get a real deployment history so rollbacks take 30 seconds instead of hours. You can implement SOC2 compliance, GDPR data residency, custom domains, and SSL without waiting for the builder to support it.&lt;/p&gt;

&lt;p&gt;You also get responsibility. You need to think about backups, monitoring, and scaling. But you also get control, which is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Actually Do This
&lt;/h2&gt;

&lt;p&gt;Use something like Nometria (&lt;a href="https://nometria.com" rel="noopener noreferrer"&gt;https://nometria.com&lt;/a&gt;) that handles the export and deployment without requiring you to understand AWS internals. One-click from the browser, or three CLI commands if you prefer. Your app deploys to your infrastructure while you keep iterating in the builder.&lt;/p&gt;

&lt;p&gt;The math is clear: the cost of staying locked in grows exponentially. The cost of migrating early is linear. If you're planning to keep building this thing, own it from the start.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>api</category>
      <category>sdk</category>
    </item>
    <item>
      <title>How Telecom APIs Are Powering the Next Generation of MVNOs</title>
      <dc:creator>TelecomHub</dc:creator>
      <pubDate>Tue, 12 May 2026 11:23:15 +0000</pubDate>
      <link>https://dev.to/telecomhub/how-telecom-apis-are-powering-the-next-generation-of-mvnos-4obn</link>
      <guid>https://dev.to/telecomhub/how-telecom-apis-are-powering-the-next-generation-of-mvnos-4obn</guid>
      <description>&lt;p&gt;Launching an MVNO used to be a slow, expensive grind. You negotiated with MNOs, inherited someone's monolithic BSS/OSS stack, and then lived with it for years because switching cost too much. Every customization was a change order. Every new feature was a six-month conversation.&lt;br&gt;
That model is breaking down and the main reason is APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Stack Is Finally Composable&lt;/strong&gt;&lt;br&gt;
The fundamental shift happening right now is that telecom infrastructure is becoming composable. Instead of buying a monolith and hoping it covers your use case, operators are assembling stacks the same way a backend engineer assembles microservices: pick the best tool for each job, connect them via APIs, own the logic in between.&lt;/p&gt;

&lt;p&gt;This matters especially for new-wave MVNOs niche operators targeting specific communities, IoT-focused plays, B2B-only operators, or lean teams in emerging markets that can't afford the operational overhead of legacy systems. These aren't the old-school resellers. They need to move fast, differentiate at the product layer, and they can't do that if they're waiting on a vendor's roadmap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the API Layer Actually Covers&lt;/strong&gt;&lt;br&gt;
When people say "API-first telecom" it's worth being specific about what that means in practice. The core pieces are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subscriber lifecycle management activation&lt;/strong&gt;, deactivation, SIM swaps, plan changes. These should all be API calls. If your ops team is doing these manually through a portal, you have a scaling problem waiting to happen. Platforms like &lt;strong&gt;Telgoo5&lt;/strong&gt; have built their BSS specifically around exposing this layer programmatically, which means an MVNO can automate the entire subscriber journey without touching a dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time charging and rating&lt;/strong&gt; this is where prepaid gets interesting. Prepaid MVNOs live and die by accurate, low-latency balance management. When you can query and update balances in real-time through an API, you can build things that used to be hard: dynamic plan upsells mid-session, family data pooling, loyalty mechanics tied to usage behavior. Without that API access, you're stuck with whatever the billing vendor built as a UI feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Policy and network control&lt;/strong&gt; this one is underappreciated. QoS, throttling, plan enforcement at the network level most operators treat this as the carrier's problem. But if you want to build a "speed boost" add-on or enforce fair use policies intelligently, you need your business logic talking to your policy engine. &lt;strong&gt;Alepo&lt;/strong&gt; has been doing this kind of AAA and policy management work for a while, and their approach to exposing network behavior through APIs is what makes that kind of product feature actually buildable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhooks and event-driven integration&lt;/strong&gt; if your system is polling for balance thresholds, payment events, or usage alerts, that's a sign your platform isn't really API-first, it's just API-available. The distinction matters at scale. Real-time webhooks let your application react instead of check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Enterprise Side of This&lt;/strong&gt;&lt;br&gt;
Larger MVNOs and those in regulated markets face a different set of problems. Convergent billing running prepaid, postpaid, and hybrid plans under one roof gets messy fast. You end up with multiple systems that don't naturally agree on what a subscriber owes or what they've used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSG&lt;/strong&gt; has operated in this space for a long time, and their more recent work around integration-friendly billing infrastructure is relevant here. The value isn't the billing engine itself it's that the billing engine talks to the rest of your stack cleanly. Audit trails, compliance tooling, carrier-grade reliability: these things matter when you're operating at scale or in markets where regulators pay attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer Experience Is a Competitive Differentiator Now&lt;/strong&gt;&lt;br&gt;
Here's something that doesn't get said enough: the developer experience of your telecom platform is now a business differentiator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wavelo&lt;/strong&gt; spun out of Tucows was built specifically for operators who want to run their MVNO the way they'd run a SaaS product. Clean APIs, cloud-native architecture, fast iteration cycles. If your engineering team can ship a new plan structure the same day product decides to run an experiment, that's a real advantage over a competitor whose equivalent change takes three weeks and a vendor support ticket.&lt;/p&gt;

&lt;p&gt;This isn't just about convenience. It's about who can actually build differentiated products. The MVNO market is getting crowded; commoditized connectivity is a race to the bottom. The operators that win are the ones building product experiences on top of the network and that requires owning the logic layer, which requires good APIs underneath.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where This Is Heading: Edge and 5G&lt;/strong&gt;&lt;br&gt;
The next frontier for API-driven MVNOs is at the network edge. As 5G matures, there are real use cases around low-latency compute, private networks, and IoT at scale that require processing workloads closer to the radio. &lt;strong&gt;TelcoEdge Inc.&lt;/strong&gt; is working in this space their focus is on giving operators API access to edge infrastructure without needing deep network engineering in-house.&lt;/p&gt;

&lt;p&gt;This is forward-looking, but it's worth paying attention to now. MVNOs that are positioning for IoT or enterprise 5G use cases will need this infrastructure. Retrofitting it later is always more painful than building toward it early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Tradeoffs Nobody Talks About&lt;/strong&gt;&lt;br&gt;
This API-first model is genuinely better than what came before, but it's not without issues:&lt;/p&gt;

&lt;p&gt;Abstraction has limits. The cleaner the API, the less visibility you have into edge cases at the network layer. If you need to do something unusual, you will hit walls, and the vendor's support process becomes your bottleneck.&lt;/p&gt;

&lt;p&gt;Lock-in still exists it's just at a different layer. You're not locked into a monolith anymore, but you are locked into an API contract. If a platform deprecates an endpoint or changes behavior, you feel it. Version stability and migration paths should be part of your vendor evaluation.&lt;/p&gt;

&lt;p&gt;Developer experience varies wildly. "We have a REST API" covers everything from a beautifully documented, webhook-rich platform to a single endpoint and a 200-page PDF. The gap between those two things is enormous when you're actually building on it. Test the integration before you commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The actual takeaway&lt;/strong&gt;&lt;br&gt;
The MVNO stack is becoming an API stack. That's not a prediction anymore, it's what's happening. The operators that figure out how to compose the right pieces billing, policy, subscriber management, edge infrastructure and own the product logic on top are the ones that will build something worth using.&lt;/p&gt;

&lt;p&gt;The vendors and platforms in this space aren't interchangeable. They serve different segments, have different maturity levels, and come with different tradeoffs. But the direction they're all moving in is the same: give operators programmatic control, get out of the way, and let the product teams do their job.&lt;/p&gt;

</description>
      <category>telecom</category>
      <category>api</category>
      <category>mvno</category>
      <category>bss</category>
    </item>
  </channel>
</rss>
