<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://apievangelist.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://apievangelist.com/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-08-09T12:19:20+00:00</updated><id>https://apievangelist.com/feed.xml</id><title type="html">API Evangelist</title><subtitle>Understanding the technology, business, policies, and people of Apis.</subtitle><author><name>Kin Lane</name></author><entry><title type="html">Cloudflare Hands You a Token, but Only If You Click Through the Dashboard First</title><link href="https://apievangelist.com/2026/08/09/cloudflare-token-but-click-dashboard-first/" rel="alternate" type="text/html" title="Cloudflare Hands You a Token, but Only If You Click Through the Dashboard First" /><published>2026-08-09T00:00:00+00:00</published><updated>2026-08-09T00:00:00+00:00</updated><id>https://apievangelist.com/2026/08/09/cloudflare-token-but-click-dashboard-first</id><content type="html" xml:base="https://apievangelist.com/2026/08/09/cloudflare-token-but-click-dashboard-first/"><![CDATA[<p>I keep coming back to the same wall. Every company on earth is telling me they are all in on AI, that agents are the future, that software will provision and operate itself. And then I go to actually onboard one of those agents to their API, and the very first thing they ask me to do is open a browser, log in as a human, and click a button. The contradiction never stops being funny to me, and it never stops being a problem.</p>

<p>So I have been working my way through the major gateway and identity providers, rebuilding the little SoundCloud script that does the whole thing in one file — open a browser, do PKCE OAuth, register an app, print <code class="language-plaintext highlighter-rouge">client_id</code> and <code class="language-plaintext highlighter-rouge">client_secret</code> to stdout. That script is my yardstick for what <a href="https://apievangelist.com/2026/06/19/soundcloud-shows-what-programmatic-api-onboarding-should-look-like/">programmatic API onboarding</a> should feel like. This week it is Cloudflare’s turn, and Cloudflare is interesting because it sits right in the middle of the agentic story — it is the gateway in front of a huge slice of the web, and with API Shield it is the thing deciding whether an API consumer gets through at all.</p>

<p>Here is the honest mapping. Cloudflare has no Dynamic Client Registration. There is no <code class="language-plaintext highlighter-rouge">POST /register</code>, no RFC 7591, no OAuth dance you can script to mint a brand-new application identity from nothing. What Cloudflare does have is a clean management API for <em>tokens</em>, and an API token is the closest analog they offer to “register an app and get credentials.” You call <code class="language-plaintext highlighter-rouge">POST /user/tokens</code> (or <code class="language-plaintext highlighter-rouge">POST /accounts/{account_id}/tokens</code>), you hand it a <code class="language-plaintext highlighter-rouge">name</code> and a <code class="language-plaintext highlighter-rouge">policies</code> array of permission groups plus resources, and it hands you back a <code class="language-plaintext highlighter-rouge">result.value</code> — the token string, shown exactly once. Everything authenticates with a plain <code class="language-plaintext highlighter-rouge">Authorization: Bearer</code> header. As management APIs go, it is well-designed and pleasant to call.</p>

<p>The catch is the bootstrap. To create a token via the API, you have to already hold a token, and that very first token is dashboard-minted. You go to My Profile, API Tokens, Create Token, and you click. There is no public self-serve path that gets you from “I have a Cloudflare account” to “I have a credential” without a human in a browser. So this lands squarely in what I have started calling bucket (b): a real management API gated behind a personal access token you paste in by hand. It is not the SoundCloud ideal, but it is a long way from the cloud providers who give you nothing scriptable at all. I’ll take what I can get.</p>

<p>That shape changes what my script can honestly do. There is no browser flow to mirror, so I dropped the PKCE callback server entirely — it would be theater. Instead the CLI reads <code class="language-plaintext highlighter-rouge">CLOUDFLARE_API_TOKEN</code> and an optional <code class="language-plaintext highlighter-rouge">CLOUDFLARE_ACCOUNT_ID</code> from the environment, verifies the bootstrap token is live with <code class="language-plaintext highlighter-rouge">GET /user/tokens/verify</code>, looks up a permission group by name through <code class="language-plaintext highlighter-rouge">GET /user/tokens/permission_groups</code>, and then mints a fresh, narrowly-scoped child token. It handles the already-registered case the way the SoundCloud one does: if a token with the same name already exists, it tells you, because Cloudflare will never reprint an existing token’s secret. The script is committed in the repo at <code class="language-plaintext highlighter-rouge">/assets/scripts/agentic-onboarding/cloudflare-api-auth.mjs</code>, Node 18+, no npm install:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#!/usr/bin/env node
</span><span class="cm">/**
 * cloudflare-api-auth.mjs
 *
 * Provider: Cloudflare (API / API Shield context)
 * What it does: Mints a SCOPED Cloudflare API Token via the management API — the
 *   closest thing Cloudflare has to "register an app and get credentials." It is the
 *   bucket-(b) shape: you paste a bootstrap API Token (dashboard-minted) via an env
 *   var, and this CLI creates a fresh, narrowly-scoped child token and prints its
 *   value ONCE. There is no RFC 7591 Dynamic Client Registration and no self-serve
 *   OAuth/PKCE app registration at Cloudflare, so there is no browser dance here.
 *
 * Auth model: Bearer API Token on every call (Authorization: Bearer &lt;token&gt;).
 *   The bootstrap token must itself hold "API Tokens Write" (User) permission so it
 *   can create other tokens.
 *
 * Env vars:
 *   CLOUDFLARE_API_TOKEN   (required) bootstrap token, dashboard-minted:
 *                          My Profile &gt; API Tokens &gt; Create Token. Needs the
 *                          "Create Additional Tokens" / API Tokens Write permission.
 *   CLOUDFLARE_ACCOUNT_ID  (optional) if set, an ACCOUNT-owned token is created at
 *                          POST /accounts/{id}/tokens; otherwise a USER token at
 *                          POST /user/tokens.
 *
 * Doc links:
 *   Create user token:  https://developers.cloudflare.com/api/resources/user/subresources/tokens/methods/create/
 *   Create acct token:  https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/methods/create/
 *   Verify token:       https://developers.cloudflare.com/api/resources/user/subresources/tokens/methods/verify/
 *   Permission groups:  https://developers.cloudflare.com/api/resources/user/subresources/tokens/subresources/permission_groups/methods/list/
 *   API Shield mTLS:    https://developers.cloudflare.com/api/resources/client_certificates/methods/create/
 *
 * Node.js 18+ stdlib only (no npm dependencies).
 */</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">parseArgs</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:util</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">process</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:process</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">API_BASE</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">https://api.cloudflare.com/client/v4</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">HELP</span> <span class="o">=</span> <span class="s2">`Usage: cloudflare-api-auth [options]

  Creates a scoped Cloudflare API Token using a bootstrap token you already hold,
  and prints the new token value (shown ONCE by Cloudflare). This is the closest
  Cloudflare analog to "register an app + get credentials" — bucket (b): a
  management API plus a personal access token. There is no Dynamic Client
  Registration and no browser OAuth here.

Env:
  CLOUDFLARE_API_TOKEN    Required. Bootstrap token (dashboard-minted) with the
                          "API Tokens Write" permission so it can mint tokens.
  CLOUDFLARE_ACCOUNT_ID   Optional. If set, creates an account-owned token at
                          POST /accounts/{id}/tokens instead of POST /user/tokens.

Options:
  --name           Required. Name for the new token (also used to detect duplicates).
  --permission     Permission group to grant, matched by name (default:
                   "DNS Read"). Use --list-permissions to browse available groups.
  --effect         "allow" or "deny" for the policy (default: allow).
  --expires-on     Optional. RFC3339 expiry, e.g. 2026-12-31T23:59:59Z
  --list-permissions   Print available permission groups and exit.
  -h, --help

Examples:
  CLOUDFLARE_API_TOKEN=cfut_xxx node cloudflare-api-auth.mjs --name "agent-dns-reader"
  CLOUDFLARE_API_TOKEN=cfut_xxx node cloudflare-api-auth.mjs --list-permissions
`</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">die</span><span class="p">(</span><span class="nx">msg</span><span class="p">,</span> <span class="nx">code</span> <span class="o">=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">msg</span><span class="p">);</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="nx">code</span><span class="p">);</span>
<span class="p">}</span>

<span class="cm">/** Cloudflare wraps everything in { success, errors, messages, result }. */</span>
<span class="kd">function</span> <span class="nx">unwrap</span><span class="p">(</span><span class="nx">json</span><span class="p">,</span> <span class="nx">what</span><span class="p">,</span> <span class="nx">status</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">json</span> <span class="o">&amp;&amp;</span> <span class="nx">json</span><span class="p">.</span><span class="nx">success</span> <span class="o">===</span> <span class="kc">true</span><span class="p">)</span> <span class="k">return</span> <span class="nx">json</span><span class="p">.</span><span class="nx">result</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">errs</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">json</span><span class="p">?.</span><span class="nx">errors</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">json</span><span class="p">.</span><span class="nx">errors</span><span class="p">.</span><span class="nx">length</span>
    <span class="p">?</span> <span class="nx">json</span><span class="p">.</span><span class="nx">errors</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="s2">`</span><span class="p">${</span><span class="nx">e</span><span class="p">.</span><span class="nx">code</span> <span class="o">??</span> <span class="dl">"</span><span class="s2">?</span><span class="dl">"</span><span class="p">}</span><span class="s2">: </span><span class="p">${</span><span class="nx">e</span><span class="p">.</span><span class="nx">message</span> <span class="o">??</span> <span class="nx">e</span><span class="p">}</span><span class="s2">`</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">; </span><span class="dl">"</span><span class="p">)</span>
    <span class="p">:</span> <span class="s2">`HTTP </span><span class="p">${</span><span class="nx">status</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
  <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">what</span><span class="p">}</span><span class="s2"> failed: </span><span class="p">${</span><span class="nx">errs</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nx">cf</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="p">{</span> <span class="nx">method</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">,</span> <span class="nx">token</span><span class="p">,</span> <span class="nx">body</span> <span class="p">}</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">headers</span> <span class="o">=</span> <span class="p">{</span> <span class="na">authorization</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">token</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="na">accept</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span> <span class="p">};</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">body</span> <span class="o">!==</span> <span class="kc">undefined</span><span class="p">)</span> <span class="nx">headers</span><span class="p">[</span><span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">]</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">API_BASE</span><span class="p">}${</span><span class="nx">path</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="p">{</span>
    <span class="nx">method</span><span class="p">,</span>
    <span class="nx">headers</span><span class="p">,</span>
    <span class="p">...(</span><span class="nx">body</span> <span class="o">!==</span> <span class="kc">undefined</span> <span class="p">?</span> <span class="p">{</span> <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">body</span><span class="p">)</span> <span class="p">}</span> <span class="p">:</span> <span class="p">{}),</span>
  <span class="p">});</span>
  <span class="kd">let</span> <span class="nx">json</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">text</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">();</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="nx">json</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">text</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">method</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">path</span><span class="p">}</span><span class="s2"> returned non-JSON (HTTP </span><span class="p">${</span><span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2">): </span><span class="p">${</span><span class="nx">text</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">200</span><span class="p">)}</span><span class="s2">`</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="p">{</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">json</span> <span class="p">};</span>
<span class="p">}</span>

<span class="cm">/** Preflight: confirm the bootstrap token actually works before we try to mint. */</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">verifyToken</span><span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">json</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">cf</span><span class="p">(</span><span class="dl">"</span><span class="s2">/user/tokens/verify</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="nx">token</span> <span class="p">});</span>
  <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nx">unwrap</span><span class="p">(</span><span class="nx">json</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Token verify (GET /user/tokens/verify)</span><span class="dl">"</span><span class="p">,</span> <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">result</span><span class="p">?.</span><span class="nx">status</span> <span class="o">&amp;&amp;</span> <span class="nx">result</span><span class="p">.</span><span class="nx">status</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">active</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Bootstrap token is "</span><span class="p">${</span><span class="nx">result</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2">", not active. Mint a fresh one in the dashboard.`</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nx">listPermissionGroups</span><span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// NOTE: verify — this list is large; we fetch the default page. The endpoint</span>
  <span class="c1">// supports ?name= / ?scope= filters if you want to narrow server-side.</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">json</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">cf</span><span class="p">(</span><span class="dl">"</span><span class="s2">/user/tokens/permission_groups</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="nx">token</span> <span class="p">});</span>
  <span class="k">return</span> <span class="nx">unwrap</span><span class="p">(</span><span class="nx">json</span><span class="p">,</span> <span class="dl">"</span><span class="s2">List permission groups (GET /user/tokens/permission_groups)</span><span class="dl">"</span><span class="p">,</span> <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">findPermissionGroup</span><span class="p">(</span><span class="nx">groups</span><span class="p">,</span> <span class="nx">wanted</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">target</span> <span class="o">=</span> <span class="nx">wanted</span><span class="p">.</span><span class="nx">trim</span><span class="p">().</span><span class="nx">toLowerCase</span><span class="p">();</span>
  <span class="kd">const</span> <span class="nx">exact</span> <span class="o">=</span> <span class="nx">groups</span><span class="p">.</span><span class="nx">find</span><span class="p">((</span><span class="nx">g</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span><span class="nx">g</span><span class="p">.</span><span class="nx">name</span> <span class="o">??</span> <span class="dl">""</span><span class="p">).</span><span class="nx">toLowerCase</span><span class="p">()</span> <span class="o">===</span> <span class="nx">target</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">exact</span><span class="p">)</span> <span class="k">return</span> <span class="nx">exact</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">partial</span> <span class="o">=</span> <span class="nx">groups</span><span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">g</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span><span class="nx">g</span><span class="p">.</span><span class="nx">name</span> <span class="o">??</span> <span class="dl">""</span><span class="p">).</span><span class="nx">toLowerCase</span><span class="p">().</span><span class="nx">includes</span><span class="p">(</span><span class="nx">target</span><span class="p">));</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">partial</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">1</span><span class="p">)</span> <span class="k">return</span> <span class="nx">partial</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">partial</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span>
      <span class="s2">`Permission "</span><span class="p">${</span><span class="nx">wanted</span><span class="p">}</span><span class="s2">" is ambiguous. Matches:\n  `</span> <span class="o">+</span>
        <span class="nx">partial</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">g</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">g</span><span class="p">.</span><span class="nx">name</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="se">\n</span><span class="s2">  </span><span class="dl">"</span><span class="p">)</span> <span class="o">+</span>
        <span class="s2">`\nRe-run with an exact --permission name.`</span>
    <span class="p">);</span>
  <span class="p">}</span>
  <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`No permission group matched "</span><span class="p">${</span><span class="nx">wanted</span><span class="p">}</span><span class="s2">". Run --list-permissions to see options.`</span><span class="p">);</span>
<span class="p">}</span>

<span class="cm">/**
 * Best-effort duplicate detection for USER tokens. Cloudflare never returns a
 * token's secret value from a list call (only on create), so if a same-named
 * token already exists we surface its id and tell the user we cannot reprint it.
 * NOTE: verify — account-scoped token listing is not clearly documented; this
 * dup-check only runs for user tokens.
 */</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">findExistingUserToken</span><span class="p">(</span><span class="nx">token</span><span class="p">,</span> <span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">json</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">cf</span><span class="p">(</span><span class="dl">"</span><span class="s2">/user/tokens</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="nx">token</span> <span class="p">});</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">json</span><span class="p">?.</span><span class="nx">success</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span> <span class="c1">// listing unavailable on this token; skip dup-check</span>
  <span class="kd">const</span> <span class="nx">list</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">json</span><span class="p">.</span><span class="nx">result</span><span class="p">)</span> <span class="p">?</span> <span class="nx">json</span><span class="p">.</span><span class="nx">result</span> <span class="p">:</span> <span class="p">[];</span>
  <span class="k">return</span> <span class="nx">list</span><span class="p">.</span><span class="nx">find</span><span class="p">((</span><span class="nx">t</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">t</span><span class="p">?.</span><span class="nx">name</span> <span class="o">===</span> <span class="nx">name</span><span class="p">)</span> <span class="o">??</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">buildPolicy</span><span class="p">({</span> <span class="nx">effect</span><span class="p">,</span> <span class="nx">permissionGroup</span><span class="p">,</span> <span class="nx">accountId</span> <span class="p">})</span> <span class="p">{</span>
  <span class="c1">// Resources scope: account-level if we have an account id, else all accounts</span>
  <span class="c1">// the token's owner controls. This is intentionally broad-but-honest; tighten</span>
  <span class="c1">// `resources` per your needs (e.g. a single zone id) before relying on it.</span>
  <span class="kd">const</span> <span class="nx">resources</span> <span class="o">=</span> <span class="nx">accountId</span>
    <span class="p">?</span> <span class="p">{</span> <span class="p">[</span><span class="s2">`com.cloudflare.api.account.</span><span class="p">${</span><span class="nx">accountId</span><span class="p">}</span><span class="s2">`</span><span class="p">]:</span> <span class="dl">"</span><span class="s2">*</span><span class="dl">"</span> <span class="p">}</span>
    <span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">com.cloudflare.api.account.*</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">*</span><span class="dl">"</span> <span class="p">};</span>
  <span class="k">return</span> <span class="p">[</span>
    <span class="p">{</span>
      <span class="nx">effect</span><span class="p">,</span>
      <span class="na">permission_groups</span><span class="p">:</span> <span class="p">[{</span> <span class="na">id</span><span class="p">:</span> <span class="nx">permissionGroup</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="nx">permissionGroup</span><span class="p">.</span><span class="nx">name</span> <span class="p">}],</span>
      <span class="nx">resources</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">];</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">formatTokenOutput</span><span class="p">({</span> <span class="nx">result</span><span class="p">,</span> <span class="nx">accountId</span><span class="p">,</span> <span class="nx">permissionName</span> <span class="p">})</span> <span class="p">{</span>
  <span class="c1">// Cloudflare's credential is a single bearer token string in result.value.</span>
  <span class="c1">// We mirror the SoundCloud script's `client_id=` / `client_secret=` ergonomics</span>
  <span class="c1">// by emitting the token under both an explicit label and a JSON blob.</span>
  <span class="kd">const</span> <span class="nx">pub</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">id</span><span class="p">:</span> <span class="nx">result</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
    <span class="na">name</span><span class="p">:</span> <span class="nx">result</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
    <span class="na">value</span><span class="p">:</span> <span class="nx">result</span><span class="p">.</span><span class="nx">value</span><span class="p">,</span>
    <span class="na">status</span><span class="p">:</span> <span class="nx">result</span><span class="p">.</span><span class="nx">status</span><span class="p">,</span>
    <span class="na">expires_on</span><span class="p">:</span> <span class="nx">result</span><span class="p">.</span><span class="nx">expires_on</span> <span class="o">??</span> <span class="kc">null</span><span class="p">,</span>
    <span class="na">scope</span><span class="p">:</span> <span class="nx">accountId</span> <span class="p">?</span> <span class="s2">`account:</span><span class="p">${</span><span class="nx">accountId</span><span class="p">}</span><span class="s2">`</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">user</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">permission</span><span class="p">:</span> <span class="nx">permissionName</span><span class="p">,</span>
  <span class="p">};</span>
  <span class="kd">const</span> <span class="nx">lines</span> <span class="o">=</span> <span class="p">[];</span>
  <span class="c1">// The token value IS the secret; there is no separate id/secret pair.</span>
  <span class="nx">lines</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`api_token=</span><span class="p">${</span><span class="nx">result</span><span class="p">.</span><span class="nx">value</span> <span class="o">??</span> <span class="dl">"</span><span class="s2">(not returned)</span><span class="dl">"</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="nx">lines</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`token_id=</span><span class="p">${</span><span class="nx">result</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="nx">lines</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">""</span><span class="p">);</span>
  <span class="nx">lines</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">"</span><span class="s2"># Use it as:  Authorization: Bearer &lt;api_token&gt;</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">lines</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">"</span><span class="s2"># Cloudflare shows the value ONCE. Store it now.</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">lines</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">""</span><span class="p">);</span>
  <span class="nx">lines</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">pub</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">));</span>
  <span class="nx">lines</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">""</span><span class="p">);</span>
  <span class="k">return</span> <span class="nx">lines</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="se">\n</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">parsed</span><span class="p">;</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="nx">parsed</span> <span class="o">=</span> <span class="nx">parseArgs</span><span class="p">({</span>
      <span class="na">options</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">name</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span>
        <span class="na">permission</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span>
        <span class="na">effect</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span>
        <span class="dl">"</span><span class="s2">expires-on</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span>
        <span class="dl">"</span><span class="s2">list-permissions</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">boolean</span><span class="dl">"</span> <span class="p">},</span>
        <span class="na">help</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">boolean</span><span class="dl">"</span><span class="p">,</span> <span class="na">short</span><span class="p">:</span> <span class="dl">"</span><span class="s2">h</span><span class="dl">"</span> <span class="p">},</span>
      <span class="p">},</span>
      <span class="na">strict</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="na">allowPositionals</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="p">});</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">die</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">e</span><span class="p">.</span><span class="nx">message</span><span class="p">}</span><span class="s2">\n\n</span><span class="p">${</span><span class="nx">HELP</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="kd">const</span> <span class="p">{</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">permission</span><span class="p">,</span> <span class="nx">effect</span><span class="p">,</span> <span class="dl">"</span><span class="s2">expires-on</span><span class="dl">"</span><span class="p">:</span> <span class="nx">expiresOn</span><span class="p">,</span> <span class="dl">"</span><span class="s2">list-permissions</span><span class="dl">"</span><span class="p">:</span> <span class="nx">listPerms</span><span class="p">,</span> <span class="nx">help</span> <span class="p">}</span> <span class="o">=</span>
    <span class="nx">parsed</span><span class="p">.</span><span class="nx">values</span><span class="p">;</span>

  <span class="k">if</span> <span class="p">(</span><span class="nx">help</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">HELP</span><span class="p">);</span>
    <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CLOUDFLARE_API_TOKEN</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">die</span><span class="p">(</span>
      <span class="dl">"</span><span class="s2">Missing CLOUDFLARE_API_TOKEN.</span><span class="se">\n</span><span class="dl">"</span> <span class="o">+</span>
        <span class="dl">"</span><span class="s2">Mint a bootstrap token in the dashboard (My Profile &gt; API Tokens &gt; Create Token)</span><span class="se">\n</span><span class="dl">"</span> <span class="o">+</span>
        <span class="dl">'</span><span class="s1">with the "API Tokens Write" permission, then export it:</span><span class="se">\n</span><span class="dl">'</span> <span class="o">+</span>
        <span class="dl">"</span><span class="s2">  export CLOUDFLARE_API_TOKEN=cfut_...</span><span class="se">\n</span><span class="dl">"</span>
    <span class="p">);</span>
  <span class="p">}</span>
  <span class="kd">const</span> <span class="nx">accountId</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CLOUDFLARE_ACCOUNT_ID</span> <span class="o">||</span> <span class="dl">""</span><span class="p">;</span>

  <span class="c1">// Preflight: prove the bootstrap token is live before doing anything destructive.</span>
  <span class="k">await</span> <span class="nx">verifyToken</span><span class="p">(</span><span class="nx">token</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">groups</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">listPermissionGroups</span><span class="p">(</span><span class="nx">token</span><span class="p">);</span>

  <span class="k">if</span> <span class="p">(</span><span class="nx">listPerms</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">sorted</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">groups</span><span class="p">].</span><span class="nx">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span>
      <span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">category</span> <span class="o">??</span> <span class="dl">""</span><span class="p">).</span><span class="nx">localeCompare</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">category</span> <span class="o">??</span> <span class="dl">""</span><span class="p">)</span> <span class="o">||</span> <span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">name</span> <span class="o">??</span> <span class="dl">""</span><span class="p">).</span><span class="nx">localeCompare</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">name</span> <span class="o">??</span> <span class="dl">""</span><span class="p">)</span>
    <span class="p">);</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">g</span> <span class="k">of</span> <span class="nx">sorted</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`</span><span class="p">${(</span><span class="nx">g</span><span class="p">.</span><span class="nx">category</span> <span class="o">??</span> <span class="dl">"</span><span class="s2">-</span><span class="dl">"</span><span class="p">).</span><span class="nx">padEnd</span><span class="p">(</span><span class="mi">28</span><span class="p">)}</span><span class="s2"> </span><span class="p">${</span><span class="nx">g</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`\n</span><span class="p">${</span><span class="nx">sorted</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2"> permission groups.`</span><span class="p">);</span>
    <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">die</span><span class="p">(</span><span class="dl">"</span><span class="s2">Missing required --name.</span><span class="se">\n\n</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">HELP</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="kd">const</span> <span class="nx">policyEffect</span> <span class="o">=</span> <span class="nx">effect</span> <span class="o">??</span> <span class="dl">"</span><span class="s2">allow</span><span class="dl">"</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">policyEffect</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">allow</span><span class="dl">"</span> <span class="o">&amp;&amp;</span> <span class="nx">policyEffect</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">deny</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">die</span><span class="p">(</span><span class="s2">`--effect must be "allow" or "deny", got "</span><span class="p">${</span><span class="nx">policyEffect</span><span class="p">}</span><span class="s2">".`</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="c1">// Handle the "already registered" case the way the SoundCloud script does:</span>
  <span class="c1">// if a token with this name exists, report it instead of creating a duplicate.</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">accountId</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">existing</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">findExistingUserToken</span><span class="p">(</span><span class="nx">token</span><span class="p">,</span> <span class="nx">name</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">existing</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span>
        <span class="s2">`A token named "</span><span class="p">${</span><span class="nx">name</span><span class="p">}</span><span class="s2">" already exists (id=</span><span class="p">${</span><span class="nx">existing</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">, status=</span><span class="p">${</span><span class="nx">existing</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2">).\n`</span> <span class="o">+</span>
          <span class="dl">"</span><span class="s2">Cloudflare will not reprint an existing token's secret. Either reuse it, roll it,</span><span class="se">\n</span><span class="dl">"</span> <span class="o">+</span>
          <span class="dl">"</span><span class="s2">or pick a new --name.</span><span class="dl">"</span>
      <span class="p">);</span>
      <span class="nx">process</span><span class="p">.</span><span class="nx">stdout</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span>
        <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span> <span class="na">id</span><span class="p">:</span> <span class="nx">existing</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="nx">existing</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="na">status</span><span class="p">:</span> <span class="nx">existing</span><span class="p">.</span><span class="nx">status</span> <span class="p">},</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="dl">"</span><span class="se">\n</span><span class="dl">"</span>
      <span class="p">);</span>
      <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="kd">const</span> <span class="nx">permName</span> <span class="o">=</span> <span class="nx">permission</span> <span class="o">??</span> <span class="dl">"</span><span class="s2">DNS Read</span><span class="dl">"</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">permissionGroup</span> <span class="o">=</span> <span class="nx">findPermissionGroup</span><span class="p">(</span><span class="nx">groups</span><span class="p">,</span> <span class="nx">permName</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">name</span><span class="p">,</span>
    <span class="na">policies</span><span class="p">:</span> <span class="nx">buildPolicy</span><span class="p">({</span> <span class="na">effect</span><span class="p">:</span> <span class="nx">policyEffect</span><span class="p">,</span> <span class="nx">permissionGroup</span><span class="p">,</span> <span class="nx">accountId</span> <span class="p">}),</span>
    <span class="p">...(</span><span class="nx">expiresOn</span> <span class="p">?</span> <span class="p">{</span> <span class="na">expires_on</span><span class="p">:</span> <span class="nx">expiresOn</span> <span class="p">}</span> <span class="p">:</span> <span class="p">{}),</span>
  <span class="p">};</span>

  <span class="kd">const</span> <span class="nx">path</span> <span class="o">=</span> <span class="nx">accountId</span> <span class="p">?</span> <span class="s2">`/accounts/</span><span class="p">${</span><span class="nx">accountId</span><span class="p">}</span><span class="s2">/tokens`</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">/user/tokens</span><span class="dl">"</span><span class="p">;</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">json</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">cf</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="p">{</span> <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span> <span class="nx">token</span><span class="p">,</span> <span class="nx">body</span> <span class="p">});</span>
  <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nx">unwrap</span><span class="p">(</span><span class="nx">json</span><span class="p">,</span> <span class="s2">`Create token (POST </span><span class="p">${</span><span class="nx">path</span><span class="p">}</span><span class="s2">)`</span><span class="p">,</span> <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">);</span>

  <span class="nx">process</span><span class="p">.</span><span class="nx">stdout</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span><span class="nx">formatTokenOutput</span><span class="p">({</span> <span class="nx">result</span><span class="p">,</span> <span class="nx">accountId</span><span class="p">,</span> <span class="na">permissionName</span><span class="p">:</span> <span class="nx">permissionGroup</span><span class="p">.</span><span class="nx">name</span> <span class="p">}));</span>
<span class="p">}</span>

<span class="nx">main</span><span class="p">().</span><span class="k">catch</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">die</span><span class="p">(</span><span class="s2">`Error: </span><span class="p">${</span><span class="nx">e</span><span class="p">?.</span><span class="nx">message</span> <span class="o">||</span> <span class="nx">e</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>There is a second credential story here worth naming, because Cloudflare wears two hats. The token above is the <em>management</em> credential — it is how you operate Cloudflare. But when Cloudflare is the gateway in front of <em>your</em> API and API Shield is enforcing mTLS, the credential an API consumer presents is a client certificate, minted with <code class="language-plaintext highlighter-rouge">POST /zones/{zone_id}/client_certificates</code> from a CSR. That is the consumer-side analog of getting credentials, and it is genuinely scriptable once you can sign a CSR. I like that it exists. It is also a reminder that “onboarding” means different things depending on which side of the gateway you are standing on.</p>

<p>So where does Cloudflare actually land? The management API is good. The token model is granular and scoped in a way I wish more providers copied. But the front door is still a human clicking through a dashboard, and that is the part that does not survive contact with an agent that is supposed to provision itself at three in the morning. If Cloudflare wants to fully meet the moment they keep telling me we are living in, the move is obvious: a real Dynamic Client Registration or device-grant flow that gets an agent from zero to a first, tightly-scoped token without a browser ever opening. Until then I’ll keep pasting my bootstrap token into an env var and letting the script do the rest, because that is still better than most of the field. It just is not the future anyone is selling me.</p>]]></content><author><name>Kin Lane</name></author><category term="Onboarding" /><category term="Authentication" /><category term="OAuth" /><category term="Cloudflare" /><category term="Agents" /><category term="AI" /><summary type="html"><![CDATA[I keep coming back to the same wall. Every company on earth is telling me they are all in on AI, that agents are the future, that software will provision and operate itself. And then I go to actually onboard one of those agents to their API, and the very first thing they ask me to do is open a browser, log in as a human, and click a button. The contradiction never stops being funny to me, and it never stops being a problem.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/cloudflare-token-but-click-dashboard-first.png" /><media:content medium="image" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/cloudflare-token-but-click-dashboard-first.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">OpenAPI Overlays for Monetization and Plan Tiering From One Spec</title><link href="https://apievangelist.com/2026/08/08/openapi-overlays-for-monetization-and-plan-tiering/" rel="alternate" type="text/html" title="OpenAPI Overlays for Monetization and Plan Tiering From One Spec" /><published>2026-08-08T00:00:00+00:00</published><updated>2026-08-08T00:00:00+00:00</updated><id>https://apievangelist.com/2026/08/08/openapi-overlays-for-monetization-and-plan-tiering</id><content type="html" xml:base="https://apievangelist.com/2026/08/08/openapi-overlays-for-monetization-and-plan-tiering/"><![CDATA[<p>I have watched too many teams maintain three sets of documentation for one API because they sell it three ways. There is the Free tier docs that pretend the premium endpoints do not exist, the Pro tier docs that quote the higher rate limits, and the Enterprise docs that hint at things mere mortals cannot see. All three drift apart within a quarter, because they are copies of copies, hand-edited by whoever drew the short straw that sprint. This is exactly the kind of problem OpenAPI Overlays were built to solve, and it is one of the more underexplored entries in <a href="https://apievangelist.com/2026/06/26/the-many-use-cases-for-openapi-overlays/">my list of the many use cases for OpenAPI Overlays</a>. Monetization and plan tiering is the projection of one spec into many, and I want to actually show you how it works.</p>

<p>I am using my <a href="https://github.com/api-evangelist/products-api">Products API teaching template</a> as the running example, because it has the shape every real API has: a <code class="language-plaintext highlighter-rouge">GET /products</code> and <code class="language-plaintext highlighter-rouge">GET /products/{id}</code> for reading, a <code class="language-plaintext highlighter-rouge">POST /products</code>, <code class="language-plaintext highlighter-rouge">PUT /products/{id}</code>, and <code class="language-plaintext highlighter-rouge">DELETE /products/{id}</code> for writing, a cancel operation, and documented <code class="language-plaintext highlighter-rouge">RateLimit</code> headers. The source spec is the truth. Each plan is a lens over that truth. Here is the Free tier as an overlay.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">overlay</span><span class="pi">:</span> <span class="s">1.1.0</span>
<span class="na">info</span><span class="pi">:</span>
  <span class="na">title</span><span class="pi">:</span> <span class="s">Products API - Free Tier Overlay</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">1.0.0</span>
<span class="na">extends</span><span class="pi">:</span> <span class="s">https://raw.githubusercontent.com/api-evangelist/products-api/main/openapi/products-api-openapi.yml</span>
<span class="na">actions</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.paths['/products'].post</span>
    <span class="na">remove</span><span class="pi">:</span> <span class="no">true</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.paths['/products/{id}'].put</span>
    <span class="na">remove</span><span class="pi">:</span> <span class="no">true</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.paths['/products/{id}'].delete</span>
    <span class="na">remove</span><span class="pi">:</span> <span class="no">true</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.paths['/products/{id}/cancel']</span>
    <span class="na">remove</span><span class="pi">:</span> <span class="no">true</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.components.headers.RateLimit</span>
    <span class="na">update</span><span class="pi">:</span>
      <span class="na">description</span><span class="pi">:</span> <span class="pi">&gt;-</span>
        <span class="s">Free tier is limited to 60 requests per hour. Need write access or</span>
        <span class="s">higher limits? Upgrade to Pro or Enterprise.</span>
</code></pre></div></div>

<p>Read that top down. The <code class="language-plaintext highlighter-rouge">extends</code> points at the canonical raw spec, so nothing is copied. The first four actions delete the write and premium operations outright, which means a Free customer’s rendered docs never mention <code class="language-plaintext highlighter-rouge">POST /products</code> and their generated SDK literally does not contain a <code class="language-plaintext highlighter-rouge">createProduct</code> method. You cannot fat-finger a call to an endpoint that is not in your client. The last action rewrites the documented <code class="language-plaintext highlighter-rouge">RateLimit</code> header description to the free quota and drops in the upgrade nudge, so the pricing story lives inside the docs instead of on a separate marketing page that nobody keeps current. Now the Enterprise tier.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">overlay</span><span class="pi">:</span> <span class="s">1.1.0</span>
<span class="na">info</span><span class="pi">:</span>
  <span class="na">title</span><span class="pi">:</span> <span class="s">Products API - Enterprise Tier Overlay</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">1.0.0</span>
<span class="na">extends</span><span class="pi">:</span> <span class="s">https://raw.githubusercontent.com/api-evangelist/products-api/main/openapi/products-api-openapi.yml</span>
<span class="na">actions</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.info</span>
    <span class="na">update</span><span class="pi">:</span>
      <span class="na">x-plan</span><span class="pi">:</span> <span class="s">enterprise</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.components.headers.RateLimit</span>
    <span class="na">update</span><span class="pi">:</span>
      <span class="na">description</span><span class="pi">:</span> <span class="pi">&gt;-</span>
        <span class="s">Enterprise tier is provisioned at 50,000 requests per hour with</span>
        <span class="s">burst headroom. Dedicated quotas are negotiable per contract.</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.paths['/products/{id}/cancel']</span>
    <span class="na">update</span><span class="pi">:</span>
      <span class="na">post</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="pi">&gt;-</span>
          <span class="s">Enterprise-only bulk cancellation and audit logging are available.</span>
          <span class="s">Contact your account team to enable contract-scoped behavior.</span>
</code></pre></div></div>

<p>The Enterprise overlay removes nothing. It keeps every operation, stamps an <code class="language-plaintext highlighter-rouge">x-plan: enterprise</code> extension on <code class="language-plaintext highlighter-rouge">info</code> so downstream tooling can branch on the tier, bumps the documented <code class="language-plaintext highlighter-rouge">RateLimit</code> description to the negotiated ceiling, and enriches the cancel operation with the enterprise-only notes that would be noise in a Free customer’s docs. Same source, opposite treatment. One overlay subtracts to make a smaller honest surface, the other annotates to make a richer one.</p>

<p>Here is the part I need you to internalize, because it is where people get themselves in trouble. This shapes docs and SDKs. It does not enforce anything. Removing <code class="language-plaintext highlighter-rouge">POST /products</code> from the Free overlay does not stop a Free customer from firing a POST at your gateway. Real enforcement happens at the gateway, in your API management layer, where the token, the plan, and the quota actually live. The overlay’s job is to keep the documentation honest per tier so a Free customer is not staring at an endpoint they will get a 403 from, and an Enterprise customer sees the limits they actually paid for. The <a href="https://spec.openapis.org/overlay/latest.html">Overlay specification</a> gives you the mechanism; your gateway is still the bouncer.</p>

<p>My strong take: if your plan tiers are hand-maintained documents, you do not have plans, you have three lies decaying at different rates. Make the plan a projection. One spec, one overlay per tier, generated on every build. The gateway guards the door, the overlay keeps the map honest, and nobody edits the same endpoint description three times ever again.</p>]]></content><author><name>Kin Lane</name></author><category term="OpenAPI" /><category term="Overlays" /><category term="Monetization" /><category term="API Management" /><category term="APIs" /><summary type="html"><![CDATA[I have watched too many teams maintain three sets of documentation for one API because they sell it three ways. There is the Free tier docs that pretend the premium endpoints do not exist, the Pro tier docs that quote the higher rate limits, and the Enterprise docs that hint at things mere mortals cannot see. All three drift apart within a quarter, because they are copies of copies, hand-edited by whoever drew the short straw that sprint. This is exactly the kind of problem OpenAPI Overlays were built to solve, and it is one of the more underexplored entries in my list of the many use cases for OpenAPI Overlays. Monetization and plan tiering is the projection of one spec into many, and I want to actually show you how it works.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/openapi-overlays-for-monetization-and-plan-tiering.png" /><media:content medium="image" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/openapi-overlays-for-monetization-and-plan-tiering.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Kinde Has the Plumbing for Programmatic Onboarding, It Just Skips the Front Door</title><link href="https://apievangelist.com/2026/08/07/kinde-plumbing-skips-front-door/" rel="alternate" type="text/html" title="Kinde Has the Plumbing for Programmatic Onboarding, It Just Skips the Front Door" /><published>2026-08-07T00:00:00+00:00</published><updated>2026-08-07T00:00:00+00:00</updated><id>https://apievangelist.com/2026/08/07/kinde-plumbing-skips-front-door</id><content type="html" xml:base="https://apievangelist.com/2026/08/07/kinde-plumbing-skips-front-door/"><![CDATA[<p>I keep coming back to the same wall. Every company tells me they are all in on AI, that agents are the future, that software is going to provision and call software without a human in the loop. Then I go to sign up for their API and the first thing they ask me to do is prove I am a human. Click the boxes. Find the traffic lights. Confirm your email. Wait for someone to flip a switch on your account. The whole industry is sprinting toward an agentic future while keeping the front door bolted shut, and I have spent enough years banging my head against this wall to be a little weary about it.</p>

<p>That is why I keep dragging providers back to the SoundCloud example. A single zero-dependency script that opens a browser, logs you in, registers an application, and hands you a <code class="language-plaintext highlighter-rouge">client_id</code> and <code class="language-plaintext highlighter-rouge">client_secret</code> on stdout. That is what <a href="https://apievangelist.com/2026/06/19/soundcloud-shows-what-programmatic-api-onboarding-should-look-like/">programmatic API onboarding</a> should feel like. No portal spelunking, no copy-paste, no waiting. I want to point this lens at the identity and access vendors next, because if anyone should make credential issuance a first-class API, it is the companies whose entire business is credentials. So this week it is Kinde’s turn.</p>

<p>Here is the honest read. Kinde is a bucket-B provider: it has a real, well-documented Management API, and it absolutely can create applications and mint credentials over HTTP. What it does not have is the self-serve OAuth front door from the SoundCloud ideal. There is no RFC 7591 dynamic client registration, no “log in with your Kinde account and register a new client” flow that a brand-new developer or a cold agent can walk up to. Before anything programmatic happens, a human has to go into the Kinde dashboard, create a machine-to-machine application by hand, and authorize it for the Management API. So the chicken-and-egg problem is alive and well: you need a credential to make a credential.</p>

<p>Once you are past that one manual step, though, Kinde is genuinely good, and I will take what I can get. You take that M2M app’s <code class="language-plaintext highlighter-rouge">client_id</code> and <code class="language-plaintext highlighter-rouge">client_secret</code>, POST them to <code class="language-plaintext highlighter-rouge">https://{your_subdomain}.kinde.com/oauth2/token</code> with <code class="language-plaintext highlighter-rouge">grant_type=client_credentials</code>, and the one detail that will trip you up every single time is the audience. It is <code class="language-plaintext highlighter-rouge">https://{your_subdomain}.kinde.com/api</code> — not <code class="language-plaintext highlighter-rouge">/api/v1</code>. I lost more minutes than I want to admit to that. That call hands back a management access token, and from there you are in business: <code class="language-plaintext highlighter-rouge">POST /api/v1/applications</code> with a JSON body of <code class="language-plaintext highlighter-rouge">{ "name": "...", "type": "m2m" }</code> and a <code class="language-plaintext highlighter-rouge">Bearer</code> token, and Kinde creates the client and returns the <code class="language-plaintext highlighter-rouge">id</code>, <code class="language-plaintext highlighter-rouge">client_id</code>, and <code class="language-plaintext highlighter-rouge">client_secret</code> right there in the create response. No second round trip to fetch the secret, which is more than I can say for a lot of platforms. The type can be <code class="language-plaintext highlighter-rouge">reg</code>, <code class="language-plaintext highlighter-rouge">spa</code>, <code class="language-plaintext highlighter-rouge">m2m</code>, or <code class="language-plaintext highlighter-rouge">device</code>, which covers the realistic shapes an agent might need to stand up.</p>

<p>The already-registered case is where you have to do a little more work, and it is worth knowing why. Kinde does not seem to reject duplicate application names, so there is no tidy “you already have one” error to catch. And the list endpoint, <code class="language-plaintext highlighter-rouge">GET /api/v1/applications</code>, only returns <code class="language-plaintext highlighter-rouge">id</code>, <code class="language-plaintext highlighter-rouge">name</code>, and <code class="language-plaintext highlighter-rouge">type</code> for each app — no secret. So to recover credentials for an existing application you list, match by name, then <code class="language-plaintext highlighter-rouge">GET /api/v1/applications/{id}</code>, which does return the secret. I wired all of that into the script so the <code class="language-plaintext highlighter-rouge">--reuse</code> flag does the right thing instead of quietly spawning a pile of duplicate clients.</p>

<p>That script is below, and it is committed in the repo at <code class="language-plaintext highlighter-rouge">/assets/scripts/agentic-onboarding/kinde-api-auth.mjs</code>. Same spirit as the SoundCloud original: one file, Node 18+ stdlib only, no <code class="language-plaintext highlighter-rouge">npm install</code>. The one difference forced by Kinde’s model is that there is no browser to open — you feed it <code class="language-plaintext highlighter-rouge">KINDE_DOMAIN</code>, <code class="language-plaintext highlighter-rouge">KINDE_M2M_CLIENT_ID</code>, and <code class="language-plaintext highlighter-rouge">KINDE_M2M_CLIENT_SECRET</code> and it mints the management token for you, or you hand it a <code class="language-plaintext highlighter-rouge">KINDE_TOKEN</code> you already have. It prints <code class="language-plaintext highlighter-rouge">client_id=</code> / <code class="language-plaintext highlighter-rouge">client_secret=</code> to stdout exactly like the others.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#!/usr/bin/env node
</span><span class="cm">/**
 * kinde-api-auth.mjs
 *
 * Provider: Kinde (https://kinde.com) — auth, access management, and billing platform.
 *
 * What it does:
 *   Creates a new Kinde application (client) via the Kinde Management API and prints its
 *   client_id / client_secret. If an application with the same --name already exists, it
 *   looks it up and returns the existing credentials instead.
 *
 * Auth model (Hypothesis bucket B — Management API + M2M token):
 *   Kinde does NOT offer RFC 7591 Dynamic Client Registration, so there is no browser
 *   OAuth dance here. Instead you create ONE machine-to-machine (M2M) application in the
 *   Kinde dashboard, authorize it for the Kinde Management API with the scopes below, and
 *   feed its credentials to this script via env vars. The script then:
 *     1. POST {KINDE_DOMAIN}/oauth2/token  (grant_type=client_credentials,
 *        audience={KINDE_DOMAIN}/api)  -&gt; management access token
 *        NOTE: the audience is ".../api", NOT ".../api/v1".
 *     2. POST {KINDE_DOMAIN}/api/v1/applications  (Authorization: Bearer &lt;token&gt;)
 *        body { name, type } -&gt; { application: { id, client_id, client_secret } }
 *   Required M2M scopes: create:applications, read:applications.
 *
 * Env vars (choose ONE auth path):
 *   Path A — mint a token from M2M credentials:
 *     KINDE_DOMAIN              e.g. https://your-subdomain.kinde.com  (or just your-subdomain)
 *     KINDE_M2M_CLIENT_ID       client_id of your M2M app
 *     KINDE_M2M_CLIENT_SECRET   client_secret of your M2M app
 *   Path B — paste a pre-obtained management token:
 *     KINDE_DOMAIN              (still required, to build the /api/v1 base URL)
 *     KINDE_TOKEN               a valid management API bearer token
 *
 * Node.js 18+ stdlib only (global fetch). No npm dependencies.
 *
 * Docs:
 *   https://docs.kinde.com/developer-tools/kinde-api/access-token-for-api/
 *   https://docs.kinde.com/developer-tools/kinde-api/connect-to-kinde-api/
 *   https://docs.kinde.com/kinde-apis/management/  (Applications endpoints)
 *   https://docs.kinde.com/build/applications/about-applications/  (type values)
 */</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">parseArgs</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:util</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">process</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:process</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">APP_TYPES</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Set</span><span class="p">([</span><span class="dl">"</span><span class="s2">reg</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">spa</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">m2m</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">device</span><span class="dl">"</span><span class="p">]);</span>
<span class="kd">const</span> <span class="nx">DEFAULT_TYPE</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">m2m</span><span class="dl">"</span><span class="p">;</span>

<span class="cm">/** Normalize a subdomain or full URL into a clean origin like https://acme.kinde.com */</span>
<span class="kd">function</span> <span class="nx">normalizeDomain</span><span class="p">(</span><span class="nx">raw</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">raw</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
  <span class="kd">let</span> <span class="nx">v</span> <span class="o">=</span> <span class="nx">raw</span><span class="p">.</span><span class="nx">trim</span><span class="p">().</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">\/</span><span class="sr">+$/</span><span class="p">,</span> <span class="dl">""</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="sr">/^https</span><span class="se">?</span><span class="sr">:</span><span class="se">\/\/</span><span class="sr">/i</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">v</span><span class="p">))</span> <span class="p">{</span>
    <span class="c1">// Allow passing just the subdomain ("acme") or "acme.kinde.com".</span>
    <span class="nx">v</span> <span class="o">=</span> <span class="nx">v</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">"</span><span class="s2">.</span><span class="dl">"</span><span class="p">)</span> <span class="p">?</span> <span class="s2">`https://</span><span class="p">${</span><span class="nx">v</span><span class="p">}</span><span class="s2">`</span> <span class="p">:</span> <span class="s2">`https://</span><span class="p">${</span><span class="nx">v</span><span class="p">}</span><span class="s2">.kinde.com`</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">v</span><span class="p">).</span><span class="nx">origin</span><span class="p">;</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
    <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">bail</span><span class="p">(</span><span class="nx">msg</span><span class="p">,</span> <span class="nx">code</span> <span class="o">=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">msg</span><span class="p">);</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="nx">code</span><span class="p">);</span>
<span class="p">}</span>

<span class="cm">/** Step 1: client_credentials -&gt; management access token. */</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">mintManagementToken</span><span class="p">({</span> <span class="nx">domain</span><span class="p">,</span> <span class="nx">clientId</span><span class="p">,</span> <span class="nx">clientSecret</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">tokenUrl</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">domain</span><span class="p">}</span><span class="s2">/oauth2/token`</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URLSearchParams</span><span class="p">({</span>
    <span class="na">grant_type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">client_credentials</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">client_id</span><span class="p">:</span> <span class="nx">clientId</span><span class="p">,</span>
    <span class="na">client_secret</span><span class="p">:</span> <span class="nx">clientSecret</span><span class="p">,</span>
    <span class="c1">// The management API audience is "/api" (NOT "/api/v1").</span>
    <span class="na">audience</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">domain</span><span class="p">}</span><span class="s2">/api`</span><span class="p">,</span>
  <span class="p">});</span>
  <span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">tokenUrl</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">accept</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/x-www-form-urlencoded</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="na">body</span><span class="p">:</span> <span class="nx">body</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span>
  <span class="p">});</span>
  <span class="kd">const</span> <span class="nx">text</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">();</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">res</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span>
      <span class="s2">`Token request (POST </span><span class="p">${</span><span class="nx">tokenUrl</span><span class="p">}</span><span class="s2">) failed: </span><span class="p">${</span><span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">text</span><span class="p">}</span><span class="s2">\n`</span> <span class="o">+</span>
        <span class="dl">"</span><span class="s2">Check KINDE_M2M_CLIENT_ID / KINDE_M2M_CLIENT_SECRET, and that the M2M app is</span><span class="se">\n</span><span class="dl">"</span> <span class="o">+</span>
        <span class="dl">"</span><span class="s2">authorized for the Kinde Management API with create:applications + read:applications.</span><span class="dl">"</span>
    <span class="p">);</span>
  <span class="p">}</span>
  <span class="kd">let</span> <span class="nx">json</span><span class="p">;</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="nx">json</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">text</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Token endpoint returned non-JSON: </span><span class="p">${</span><span class="nx">text</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">json</span><span class="p">.</span><span class="nx">access_token</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`No access_token in token response: </span><span class="p">${</span><span class="nx">text</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">json</span><span class="p">.</span><span class="nx">access_token</span><span class="p">;</span>
<span class="p">}</span>

<span class="cm">/** Thin wrapper for Management API calls (Authorization: Bearer &lt;token&gt;). */</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">kindeApi</span><span class="p">({</span> <span class="nx">apiBase</span><span class="p">,</span> <span class="nx">token</span><span class="p">,</span> <span class="nx">path</span><span class="p">,</span> <span class="nx">method</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">,</span> <span class="nx">body</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">apiBase</span><span class="p">}${</span><span class="nx">path</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">headers</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">accept</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">authorization</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">token</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
  <span class="p">};</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">body</span> <span class="o">!==</span> <span class="kc">undefined</span><span class="p">)</span> <span class="nx">headers</span><span class="p">[</span><span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">]</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span>
    <span class="nx">method</span><span class="p">,</span>
    <span class="nx">headers</span><span class="p">,</span>
    <span class="p">...(</span><span class="nx">body</span> <span class="o">!==</span> <span class="kc">undefined</span> <span class="p">?</span> <span class="p">{</span> <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">body</span><span class="p">)</span> <span class="p">}</span> <span class="p">:</span> <span class="p">{}),</span>
  <span class="p">});</span>
  <span class="k">return</span> <span class="p">{</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">url</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">()</span> <span class="p">};</span>
<span class="p">}</span>

<span class="cm">/** Step 2a: create a new application. Returns { id, client_id, client_secret }. */</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">createApplication</span><span class="p">({</span> <span class="nx">apiBase</span><span class="p">,</span> <span class="nx">token</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">type</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">url</span><span class="p">,</span> <span class="nx">text</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">kindeApi</span><span class="p">({</span>
    <span class="nx">apiBase</span><span class="p">,</span>
    <span class="nx">token</span><span class="p">,</span>
    <span class="na">path</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/api/v1/applications</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">body</span><span class="p">:</span> <span class="p">{</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">type</span> <span class="p">},</span>
  <span class="p">});</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">res</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Create application (POST </span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2">) failed: </span><span class="p">${</span><span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">text</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="kd">let</span> <span class="nx">json</span><span class="p">;</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="nx">json</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">text</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Create application returned non-JSON: </span><span class="p">${</span><span class="nx">text</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">json</span><span class="p">.</span><span class="nx">application</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">app</span><span class="p">?.</span><span class="nx">client_id</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Create application response missing application.client_id: </span><span class="p">${</span><span class="nx">text</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">app</span><span class="p">;</span> <span class="c1">// { id, client_id, client_secret }</span>
<span class="p">}</span>

<span class="cm">/**
 * Find an existing application by exact name (paginated).
 * NOTE: list items only contain { id, name, type } — no secret — so the caller must
 * follow up with getApplication() to recover client_secret.
 */</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">findApplicationByName</span><span class="p">({</span> <span class="nx">apiBase</span><span class="p">,</span> <span class="nx">token</span><span class="p">,</span> <span class="nx">name</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">nextToken</span><span class="p">;</span>
  <span class="k">do</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">qs</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URLSearchParams</span><span class="p">({</span> <span class="na">page_size</span><span class="p">:</span> <span class="dl">"</span><span class="s2">100</span><span class="dl">"</span><span class="p">,</span> <span class="na">sort</span><span class="p">:</span> <span class="dl">"</span><span class="s2">name_asc</span><span class="dl">"</span> <span class="p">});</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">nextToken</span><span class="p">)</span> <span class="nx">qs</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">"</span><span class="s2">next_token</span><span class="dl">"</span><span class="p">,</span> <span class="nx">nextToken</span><span class="p">);</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">url</span><span class="p">,</span> <span class="nx">text</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">kindeApi</span><span class="p">({</span>
      <span class="nx">apiBase</span><span class="p">,</span>
      <span class="nx">token</span><span class="p">,</span>
      <span class="na">path</span><span class="p">:</span> <span class="s2">`/api/v1/applications?</span><span class="p">${</span><span class="nx">qs</span><span class="p">.</span><span class="nx">toString</span><span class="p">()}</span><span class="s2">`</span><span class="p">,</span>
    <span class="p">});</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">res</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`List applications (GET </span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2">) failed: </span><span class="p">${</span><span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">text</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="kd">const</span> <span class="nx">json</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">text</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">match</span> <span class="o">=</span> <span class="p">(</span><span class="nx">json</span><span class="p">.</span><span class="nx">applications</span> <span class="o">??</span> <span class="p">[]).</span><span class="nx">find</span><span class="p">((</span><span class="nx">a</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">a</span><span class="p">?.</span><span class="nx">name</span> <span class="o">===</span> <span class="nx">name</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">match</span><span class="p">?.</span><span class="nx">id</span><span class="p">)</span> <span class="k">return</span> <span class="nx">match</span><span class="p">.</span><span class="nx">id</span><span class="p">;</span>
    <span class="nx">nextToken</span> <span class="o">=</span> <span class="nx">json</span><span class="p">.</span><span class="nx">next_token</span> <span class="o">||</span> <span class="kc">undefined</span><span class="p">;</span>
  <span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="nx">nextToken</span><span class="p">);</span>
  <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>

<span class="cm">/** Step 2b: fetch full application (incl. client_secret) by id. */</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">getApplication</span><span class="p">({</span> <span class="nx">apiBase</span><span class="p">,</span> <span class="nx">token</span><span class="p">,</span> <span class="nx">id</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">url</span><span class="p">,</span> <span class="nx">text</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">kindeApi</span><span class="p">({</span>
    <span class="nx">apiBase</span><span class="p">,</span>
    <span class="nx">token</span><span class="p">,</span>
    <span class="na">path</span><span class="p">:</span> <span class="s2">`/api/v1/applications/</span><span class="p">${</span><span class="nb">encodeURIComponent</span><span class="p">(</span><span class="nx">id</span><span class="p">)}</span><span class="s2">`</span><span class="p">,</span>
  <span class="p">});</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">res</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Get application (GET </span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2">) failed: </span><span class="p">${</span><span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">text</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">text</span><span class="p">).</span><span class="nx">application</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">app</span><span class="p">?.</span><span class="nx">client_id</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Get application response missing application.client_id: </span><span class="p">${</span><span class="nx">text</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">app</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">formatCredentialOutput</span><span class="p">(</span><span class="nx">app</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">type</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">out</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">id</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
    <span class="na">name</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">name</span> <span class="o">??</span> <span class="nx">name</span><span class="p">,</span>
    <span class="na">type</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">type</span> <span class="o">??</span> <span class="nx">type</span><span class="p">,</span>
    <span class="na">client_id</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">client_id</span><span class="p">,</span>
    <span class="na">client_secret</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">client_secret</span><span class="p">,</span>
  <span class="p">};</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">k</span> <span class="k">of</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">out</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">out</span><span class="p">[</span><span class="nx">k</span><span class="p">]</span> <span class="o">===</span> <span class="kc">undefined</span> <span class="o">||</span> <span class="nx">out</span><span class="p">[</span><span class="nx">k</span><span class="p">]</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="k">delete</span> <span class="nx">out</span><span class="p">[</span><span class="nx">k</span><span class="p">];</span>
  <span class="p">}</span>
  <span class="kd">const</span> <span class="nx">lines</span> <span class="o">=</span> <span class="p">[</span><span class="s2">`client_id=</span><span class="p">${</span><span class="nx">out</span><span class="p">.</span><span class="nx">client_id</span><span class="p">}</span><span class="s2">`</span><span class="p">];</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">out</span><span class="p">.</span><span class="nx">client_secret</span><span class="p">)</span> <span class="nx">lines</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`client_secret=</span><span class="p">${</span><span class="nx">out</span><span class="p">.</span><span class="nx">client_secret</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="nx">lines</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">""</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">out</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span> <span class="dl">""</span><span class="p">);</span>
  <span class="k">return</span> <span class="nx">lines</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="se">\n</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="p">{</span>
  <span class="na">values</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">nameArg</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="nx">typeArg</span><span class="p">,</span> <span class="na">reuse</span><span class="p">:</span> <span class="nx">reuseArg</span><span class="p">,</span> <span class="na">help</span><span class="p">:</span> <span class="nx">helpArg</span> <span class="p">},</span>
  <span class="nx">positionals</span><span class="p">,</span>
<span class="p">}</span> <span class="o">=</span> <span class="nx">parseArgs</span><span class="p">({</span>
  <span class="na">options</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span>
    <span class="na">type</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span>
    <span class="na">reuse</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">boolean</span><span class="dl">"</span> <span class="p">},</span>
    <span class="na">help</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">boolean</span><span class="dl">"</span><span class="p">,</span> <span class="na">short</span><span class="p">:</span> <span class="dl">"</span><span class="s2">h</span><span class="dl">"</span> <span class="p">},</span>
  <span class="p">},</span>
  <span class="na">strict</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
  <span class="na">allowPositionals</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="p">});</span>

<span class="k">if</span> <span class="p">(</span><span class="nx">helpArg</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Usage: kinde-api-auth [options]

  Creates a Kinde application via the Management API and prints client_id /
  client_secret. With --reuse, an existing application of the same --name is
  returned instead of creating a duplicate.

Options:
  --name         Required. The application's name.
  --type         Application type: reg | spa | m2m | device  (default: </span><span class="p">${</span><span class="nx">DEFAULT_TYPE</span><span class="p">}</span><span class="s2">)
  --reuse        If an app with this --name already exists, return it instead of creating.
  -h, --help

Environment (Path A — mint a token from M2M credentials):
  KINDE_DOMAIN              https://your-subdomain.kinde.com  (or just "your-subdomain")
  KINDE_M2M_CLIENT_ID       client_id of a Management-API-authorized M2M app
  KINDE_M2M_CLIENT_SECRET   its client_secret

Environment (Path B — paste a pre-obtained management token):
  KINDE_DOMAIN              (still required)
  KINDE_TOKEN               a valid Kinde Management API bearer token

  The M2M app must be authorized for the Kinde Management API with at least
  create:applications and read:applications.

  With npm, pass a double dash before flags:  npm start -- --name "My App"
`</span><span class="p">);</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">if</span> <span class="p">(</span><span class="nx">positionals</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">bail</span><span class="p">(</span>
    <span class="s2">`Unexpected extra argument(s): </span><span class="p">${</span><span class="nx">positionals</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">p</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">p</span><span class="p">)).</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">)}</span><span class="s2">\n`</span> <span class="o">+</span>
      <span class="dl">'</span><span class="s1">If you used npm, put a double dash before the options, e.g.:</span><span class="se">\n</span><span class="dl">'</span> <span class="o">+</span>
      <span class="dl">'</span><span class="s1">  npm start -- --name "My App" --type m2m</span><span class="dl">'</span>
  <span class="p">);</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">name</span> <span class="o">=</span> <span class="nx">nameArg</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">typeArg</span> <span class="o">??</span> <span class="nx">DEFAULT_TYPE</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">name</span><span class="p">)</span> <span class="nx">bail</span><span class="p">(</span><span class="dl">'</span><span class="s1">Missing required argument: --name</span><span class="se">\n</span><span class="s1">Example: node kinde-api-auth.mjs --name "My Agent App" --type m2m</span><span class="dl">'</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">APP_TYPES</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">type</span><span class="p">))</span> <span class="p">{</span>
  <span class="nx">bail</span><span class="p">(</span><span class="s2">`Invalid --type "</span><span class="p">${</span><span class="nx">type</span><span class="p">}</span><span class="s2">". Must be one of: </span><span class="p">${[...</span><span class="nx">APP_TYPES</span><span class="p">].</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">, </span><span class="dl">"</span><span class="p">)}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">domain</span> <span class="o">=</span> <span class="nx">normalizeDomain</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">KINDE_DOMAIN</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">domain</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">bail</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">Missing or invalid KINDE_DOMAIN. Set it to your Kinde domain, e.g.</span><span class="se">\n</span><span class="dl">"</span> <span class="o">+</span>
      <span class="dl">"</span><span class="s2">  export KINDE_DOMAIN=https://your-subdomain.kinde.com</span><span class="dl">"</span>
  <span class="p">);</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">apiBase</span> <span class="o">=</span> <span class="nx">domain</span><span class="p">;</span> <span class="c1">// Management endpoints live under {domain}/api/v1</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nx">resolveToken</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">KINDE_TOKEN</span><span class="p">)</span> <span class="k">return</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">KINDE_TOKEN</span><span class="p">.</span><span class="nx">trim</span><span class="p">();</span>
  <span class="kd">const</span> <span class="nx">clientId</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">KINDE_M2M_CLIENT_ID</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">clientSecret</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">KINDE_M2M_CLIENT_SECRET</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">clientId</span> <span class="o">||</span> <span class="o">!</span><span class="nx">clientSecret</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">bail</span><span class="p">(</span>
      <span class="dl">"</span><span class="s2">No credentials found. Provide EITHER:</span><span class="se">\n</span><span class="dl">"</span> <span class="o">+</span>
        <span class="dl">"</span><span class="s2">  - KINDE_M2M_CLIENT_ID and KINDE_M2M_CLIENT_SECRET  (script mints the token), or</span><span class="se">\n</span><span class="dl">"</span> <span class="o">+</span>
        <span class="dl">"</span><span class="s2">  - KINDE_TOKEN                                       (a management bearer token)</span><span class="dl">"</span>
    <span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">mintManagementToken</span><span class="p">({</span> <span class="nx">domain</span><span class="p">,</span> <span class="nx">clientId</span><span class="p">,</span> <span class="nx">clientSecret</span> <span class="p">});</span>
<span class="p">}</span>

<span class="k">try</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">resolveToken</span><span class="p">();</span>

  <span class="k">if</span> <span class="p">(</span><span class="nx">reuseArg</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">existingId</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">findApplicationByName</span><span class="p">({</span> <span class="nx">apiBase</span><span class="p">,</span> <span class="nx">token</span><span class="p">,</span> <span class="nx">name</span> <span class="p">});</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">existingId</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getApplication</span><span class="p">({</span> <span class="nx">apiBase</span><span class="p">,</span> <span class="nx">token</span><span class="p">,</span> <span class="na">id</span><span class="p">:</span> <span class="nx">existingId</span> <span class="p">});</span>
      <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s2">`Application named "</span><span class="p">${</span><span class="nx">name</span><span class="p">}</span><span class="s2">" already exists; returning existing credentials.`</span><span class="p">);</span>
      <span class="nx">process</span><span class="p">.</span><span class="nx">stdout</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span><span class="nx">formatCredentialOutput</span><span class="p">(</span><span class="nx">app</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">type</span> <span class="p">}));</span>
      <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="c1">// NOTE: Kinde does not appear to reject duplicate application names at create time, so</span>
  <span class="c1">// "already registered" handling here is an explicit --reuse name lookup rather than a</span>
  <span class="c1">// reaction to a specific API error code. Verify against your tenant's behavior.</span>
  <span class="kd">const</span> <span class="nx">created</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">createApplication</span><span class="p">({</span> <span class="nx">apiBase</span><span class="p">,</span> <span class="nx">token</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">type</span> <span class="p">});</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">stdout</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span><span class="nx">formatCredentialOutput</span><span class="p">(</span><span class="nx">created</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">type</span> <span class="p">}));</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Error:</span><span class="dl">"</span><span class="p">,</span> <span class="nx">e</span><span class="p">?.</span><span class="nx">message</span> <span class="o">||</span> <span class="nx">e</span><span class="p">);</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So where does that leave Kinde against the moment? The plumbing is all there. The Management API is clean, the responses hand back the secret without a runaround, and the data model maps cleanly onto what an agent needs to provision. The gap is the front door. As long as bootstrapping requires a human to hand-create the first M2M app in a dashboard, no agent can onboard to Kinde cold, and that is the exact contradiction I started with. If Kinde wants to fully meet the agentic moment, it should offer a constrained, self-serve registration path — a way for a brand-new caller to obtain a tightly scoped, rate-limited management credential without a human clicking through a console first. Give me that, and the script above stops being a workaround for a manual step and becomes the whole story.</p>]]></content><author><name>Kin Lane</name></author><category term="Onboarding" /><category term="Authentication" /><category term="OAuth" /><category term="Kinde" /><category term="Agents" /><category term="AI" /><summary type="html"><![CDATA[I keep coming back to the same wall. Every company tells me they are all in on AI, that agents are the future, that software is going to provision and call software without a human in the loop. Then I go to sign up for their API and the first thing they ask me to do is prove I am a human. Click the boxes. Find the traffic lights. Confirm your email. Wait for someone to flip a switch on your account. The whole industry is sprinting toward an agentic future while keeping the front door bolted shut, and I have spent enough years banging my head against this wall to be a little weary about it.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/kinde-plumbing-skips-front-door.png" /><media:content medium="image" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/kinde-plumbing-skips-front-door.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Students Cheating with AI, But Really the Tech Companies Are Cheating</title><link href="https://apievangelist.com/2026/08/07/students-cheating-with-ai-but-really-the-tech-companies-are-cheating/" rel="alternate" type="text/html" title="Students Cheating with AI, But Really the Tech Companies Are Cheating" /><published>2026-08-07T00:00:00+00:00</published><updated>2026-08-07T00:00:00+00:00</updated><id>https://apievangelist.com/2026/08/07/students-cheating-with-ai-but-really-the-tech-companies-are-cheating</id><content type="html" xml:base="https://apievangelist.com/2026/08/07/students-cheating-with-ai-but-really-the-tech-companies-are-cheating/"><![CDATA[<p>I was talking with my wife about the work I am doing with my intern this summer around the usage and adoption of artificial intelligence on campus. My wife is an expert in education technology, and a passionate anti-AI crusader. I was sharing my thoughts on the endless wave of stories about students cheating with AI, and, as she does so well in my world, she had a take I had not considered about all of those stories we keep reading.</p>

<p>While meeting with my intern to talk through her research, I mentioned that I did not find the topic of students cheating with AI particularly interesting. It is overdone. When I repeated that to my wife, she told me she finds the topic very interesting, precisely because the focus is always on the students, and that focus is what obfuscates the cheating being done by the artificial intelligence companies themselves. The models are trained on copyrighted material, on scraped work nobody consented to hand over, on reverse-engineered products, and on web-scale datasets that researchers have found to contain child sexual abuse material. Every one of those is somebody else’s rule being broken at a scale no undergraduate could dream of, and none of it produces a disciplinary hearing.</p>

<p>On top of that, these companies work constantly to blow smoke up everyone’s ass — fabricating stories, staging demos, and cheating on the benchmarks and tests being championed as the authoritative lines in the sand between what is real and what is not. There is an enormous amount of money on the line, and cheating is how you get ahead, and how you stay ahead, until you can <a href="https://apievangelist.com/2026/08/05/scoring-the-secondary-market-does-the-hype-have-an-api/">sell your shares on the secondary market</a>. The incentive structure does not merely tolerate cheating. It pays for it, and it pays best of all for the kind of cheating that is hard to audit from the outside.</p>

<p>As my wife reminds me, lying and cheating are the original baseline for artificial intelligence, and they are baked right into the Turing test. The imitation game does not measure whether a machine is intelligent. It measures whether a machine can convince a human being that it is something it is not. That is the founding benchmark of this entire field, and we are surprised when the industry built on top of it games its benchmarks. This is how “artificial intelligence” convinces us it is real — by lying to us, and by cheating on the very tests we use to determine who is human and who is not.</p>

<p>This is one of the reasons my wife’s view of the world is so valuable to me. She does not see things the way I do. She never takes anything at face value. She reads a story and immediately starts asking what is behind it, and who benefits from it being told this way. I can do that some of the time, but it is not my default mode of operating, and I still fall for things more often than I would like. The students-cheating narrative being so loud is a good reminder that the headline story is rarely the real story. It is the one someone is paying to have published.</p>

<p>We should all be talking about how Anthropic, OpenAI, Meta, Google, and the rest are cheating. Instead, for reasons that are not hard to trace back to who owns the megaphone, society has chosen to put the blame on young people trying to make their way into this messed-up world we adults are building for them. I have been <a href="https://apievangelist.com/2026/08/03/my-current-stance-on-how-i-use-artificial-intelligence/">clear about where I stand on all of this</a>, and this is a fresh reminder of why. The kids did not set the rules of the game they are being accused of gaming. The companies did, and they are cheating at their own game while pointing at an eighteen-year-old with an essay.</p>]]></content><author><name>Kin Lane</name></author><category term="Artificial Intelligence" /><category term="Education" /><category term="Higher Education" /><category term="Ethics" /><category term="Copyright" /><category term="Culture" /><category term="Storytelling" /><summary type="html"><![CDATA[I was talking with my wife about the work I am doing with my intern this summer around the usage and adoption of artificial intelligence on campus. My wife is an expert in education technology, and a passionate anti-AI crusader. I was sharing my thoughts on the endless wave of stories about students cheating with AI, and, as she does so well in my world, she had a take I had not considered about all of those stories we keep reading.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/students-cheating-with-ai-but-really-the-tech-companies-are-cheating.png" /><media:content medium="image" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/students-cheating-with-ai-but-really-the-tech-companies-are-cheating.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Doing GraphQL Is Governance by Default, Doing REST Is Not</title><link href="https://apievangelist.com/2026/08/06/doing-graphql-is-governance-by-default/" rel="alternate" type="text/html" title="Doing GraphQL Is Governance by Default, Doing REST Is Not" /><published>2026-08-06T00:00:00+00:00</published><updated>2026-08-06T00:00:00+00:00</updated><id>https://apievangelist.com/2026/08/06/doing-graphql-is-governance-by-default</id><content type="html" xml:base="https://apievangelist.com/2026/08/06/doing-graphql-is-governance-by-default/"><![CDATA[<p>I have spent a lot of years being ambivalent about <a href="https://graphql.org">GraphQL</a>. I have watched it get oversold as a REST-killer and I have watched people pile onto it for reasons that had more to do with resume-building than with any real consumer need. So it surprises me to be writing this, but here it is: the single most underrated thing about GraphQL has nothing to do with query flexibility or over-fetching or any of the arguments people usually have. It is that you cannot do GraphQL at all without doing governance first. The governance is not a program you bolt on afterward. It is the price of admission. And that is a property REST simply does not have.</p>

<p>Think about what it actually takes to stand up a GraphQL API. You have to sit down and define a schema — one schema, a single typed graph that describes every type, every field, every relationship, and the exact shape of everything a consumer can ask for. You cannot ship the first query until that schema exists and is internally consistent. The type system will not let you have a <code class="language-plaintext highlighter-rouge">User</code> that means one thing over here and something incompatible over there. When you want to remove a field, you have to reach for the <code class="language-plaintext highlighter-rouge">@deprecated</code> directive and say so, in the schema, where everyone can see it. The whole thing is introspectable by default, which means the machine-readable contract that REST governance programs spend two years and a consulting budget trying to produce is just… sitting there, generated, the moment you turn the server on. You did governance. You may not have called it that. You had no choice.</p>

<p>Now think about what it takes to stand up a REST API. Almost nothing. You can add an endpoint this afternoon. You can add another one tomorrow that returns a slightly different shape of the same resource because a different team wrote it and nobody was looking. You can have <code class="language-plaintext highlighter-rouge">firstName</code> in one response and <code class="language-plaintext highlighter-rouge">first_name</code> in the next and <code class="language-plaintext highlighter-rouge">fname</code> in a third, and the framework will cheerfully serve all three. REST does not require a unified schema to function. It does not require you to declare your types up front. It does not force you to name your deprecations. Every one of those disciplines is possible in REST — I have spent years arguing that <a href="https://apievangelist.com/2026/06/25/openapi-is-the-unit-of-governance/">OpenAPI is the unit of governance</a> precisely because it lets you impose that discipline — but the operative word is <em>impose</em>. In REST, the schema is optional, external, and always slightly out of date relative to the running code. Governance is a thing you do to REST, from the outside, against its natural grain, forever.</p>

<p>That difference in grain is everything. When the discipline is the price of admission, you pay it once, at the start, when the system is small and malleable and there are three types instead of three hundred. When the discipline is optional, you pay it later, in a governance program, in arrears, with interest, after the sprawl has already happened and every team has already shipped their own idea of what a customer record looks like. I have written that <a href="https://apievangelist.com/2026/07/05/api-governance-is-75-percent-people-work/">API governance is seventy-five percent people work</a>, and I still believe that — but a meaningful slice of that people work is the work of getting everyone to agree on a schema that GraphQL would have forced them to agree on before a single line shipped. GraphQL front-loads the argument. REST lets you defer the argument indefinitely, which feels like speed right up until the day you try to inventory what you actually have.</p>

<p>I want to be careful here, because I am not making the tired claim that GraphQL is better than REST. It is not. There are whole categories of API — anything cache-heavy, anything hypermedia-shaped, anything where the resource model is the product — where REST is the right answer and GraphQL would be a self-inflicted wound. And GraphQL brings its own governance problems that REST never had, from query cost and depth limits to the operational headache of a single evolving graph. This is not a ranking. It is an observation about where the discipline lives. In GraphQL, the discipline lives in the tool, up front, unavoidable. In REST, the discipline lives in you, ongoing, optional, and easy to skip when the sprint is tight.</p>

<p>Which is exactly why REST governance is a discipline you have to actively choose, and GraphQL governance is a discipline you back into. The GraphQL team gets a single typed schema, explicit deprecations, and an introspectable contract as a side effect of the technology working at all. The REST team gets those same things only if someone stands up, insists on OpenAPI as the source of truth, wires up the linting, and defends the schema against every team that would rather just ship an endpoint. Most REST teams do not have that someone. That is not a technology failure. It is the natural consequence of a technology that does not require the schema in order to run.</p>

<p>So when people ask me whether they should adopt GraphQL, my honest answer has quietly shifted. I do not tell them GraphQL will make their API better — it might not. I tell them that if they are the kind of organization that struggles to get a handle on its own schema, GraphQL will hand them a handle whether they want one or not, and that alone might be worth more than any query-flexibility argument on the marketing page. Getting a handle on your schema <em>is</em> governance. GraphQL just does not let you pretend otherwise. This connects directly to what I have been saying about <a href="https://apievangelist.com/2026/08/04/mcp-is-last-mile-plumbing/">MCP as last-mile plumbing</a> — the foundational work is the schema, the contract, the thing underneath. GraphQL makes you do that work first. REST politely lets you put it off until it hurts.</p>]]></content><author><name>Kin Lane</name></author><category term="GraphQL" /><category term="API Governance" /><category term="Governance" /><category term="Schema" /><category term="API Design" /><category term="APIs" /><category term="Machine Readability" /><summary type="html"><![CDATA[I have spent a lot of years being ambivalent about GraphQL. I have watched it get oversold as a REST-killer and I have watched people pile onto it for reasons that had more to do with resume-building than with any real consumer need. So it surprises me to be writing this, but here it is: the single most underrated thing about GraphQL has nothing to do with query flexibility or over-fetching or any of the arguments people usually have. It is that you cannot do GraphQL at all without doing governance first. The governance is not a program you bolt on afterward. It is the price of admission. And that is a property REST simply does not have.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/doing-graphql-is-governance-by-default.png" /><media:content medium="image" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/doing-graphql-is-governance-by-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Open at Launch, Walled Off by Series D</title><link href="https://apievangelist.com/2026/08/06/open-at-launch-walled-off-by-series-d/" rel="alternate" type="text/html" title="Open at Launch, Walled Off by Series D" /><published>2026-08-06T00:00:00+00:00</published><updated>2026-08-06T00:00:00+00:00</updated><id>https://apievangelist.com/2026/08/06/open-at-launch-walled-off-by-series-d</id><content type="html" xml:base="https://apievangelist.com/2026/08/06/open-at-launch-walled-off-by-series-d/"><![CDATA[<p>I can almost set my watch by it now. A startup launches with an open API, generous free tier, real documentation, an SDK in every language, and a developer relations person who genuinely wants you to build something. They mean it, in that early moment. They need you–your integrations are how they prove traction, how they show the market they are a platform and not just an app. So the doors are wide open, and for a couple of funding rounds it feels like the good old days of the API economy.</p>

<p>Then Series D shows up, and the doors start to close. It is not personal and it is not a betrayal of principles, because the openness was never a principle–it was a growth tactic with an expiration date. Early on, the business needs your integrations more than it needs to monetize your access, so it gives access away. Later, once the platform has captured enough gravity that leaving is painful, the math flips: now the data flowing through your integrations is worth more locked up than given away, and the investors who put in that late-stage money expect to see it captured. The free tier shrinks. The useful endpoints move behind enterprise sales. The export that used to be one click sprouts an “contact us” form. The API is still there. It is just no longer for you.</p>

<p>I said this to <a href="https://nordicapis.com/kin-lane-on-ai-and-the-future-of-apis/">Nordic APIs</a> and I will say it here: this is not a bug in the venture playbook, it is the playbook. Capture as much value as you can, give away as little as possible, and treat the early openness as a customer-acquisition cost you stop paying the moment you can get away with it. SaaS makes the trap tighter, because once your data lives inside their walls and their formats, the closing of the API is not an inconvenience you route around–it is a wall you are already standing inside of. You did not lose access to a tool. You lost access to your own operational history, which now lives somewhere you can only read on their terms.</p>

<p>And this pattern does not stay confined to scrappy startups–it scales all the way up, and gets worse as it climbs. The enterprise version of the walled garden used to belong to Oracle, and we all understood the shape of that particular cage. But the gardens got bigger. Now it is Microsoft, Amazon, and Google, and the walls are taller and the grounds are vastly larger than anything Oracle ever fenced. A Google shop buys Google’s answer to every problem. A Microsoft shop standardizes on the Microsoft version of everything and lives inside Office 365. The quirky, best-in-class third-party API with the weird auth and the brilliant feature loses–not because it is worse, but because it is outside the garden, and the garden’s whole value proposition is that you never have to leave.</p>

<p>That is the part that should bother anyone who cares about a healthy ecosystem. The consolidation is not selecting for the best tools. It is selecting for the tools that are already inside the wall. Enterprises are trading away access to genuine innovation for the comfort of a single vendor relationship and one throat to choke, and they are calling it a strategy. The mediocre-but-integrated option beats the excellent-but-external one, over and over, until the external option gives up or gets acquired and absorbed into a garden of its own. The incentives all point toward enclosure, and very little points back the other way.</p>

<p>I do not have a tidy fix for this, because the forces driving it are economic, not technical, and you do not out-engineer a business model. But I do think the first move is to stop being surprised by it. If you are building on someone else’s open API today, price in the closing. Assume the free tier is a promotion, assume the export will get harder, assume the Series D or the acquisition is coming, and own your own data flows accordingly. And if you are a founder, understand that every wall you build to please a late-stage investor is also a wall around your own ceiling–the same enclosure that captures short-term value is the thing that caps how large the platform could have ever become. The gardens are winning right now. That does not make the enclosure a good idea. It makes it the thing worth designing against.</p>]]></content><author><name>Kin Lane</name></author><category term="APIs" /><category term="Business" /><category term="Investment" /><category term="Walled Gardens" /><category term="SaaS" /><category term="Strategy" /><summary type="html"><![CDATA[I can almost set my watch by it now. A startup launches with an open API, generous free tier, real documentation, an SDK in every language, and a developer relations person who genuinely wants you to build something. They mean it, in that early moment. They need you–your integrations are how they prove traction, how they show the market they are a platform and not just an app. So the doors are wide open, and for a couple of funding rounds it feels like the good old days of the API economy.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/open-at-launch-walled-off-by-series-d.png" /><media:content medium="image" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/open-at-launch-walled-off-by-series-d.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Show Your Work: Governing an API Standard with ADRs and Attacker Models</title><link href="https://apievangelist.com/2026/08/06/show-your-work-governing-an-api-standard-with-adrs-and-attacker-models/" rel="alternate" type="text/html" title="Show Your Work: Governing an API Standard with ADRs and Attacker Models" /><published>2026-08-06T00:00:00+00:00</published><updated>2026-08-06T00:00:00+00:00</updated><id>https://apievangelist.com/2026/08/06/show-your-work-governing-an-api-standard-with-adrs-and-attacker-models</id><content type="html" xml:base="https://apievangelist.com/2026/08/06/show-your-work-governing-an-api-standard-with-adrs-and-attacker-models/"><![CDATA[<p>This is the seventh post in my series on <a href="https://apievangelist.com/2026/07/16/germany-built-the-api-authorization-blueprint-the-rest-of-government-needs/">Germany’s federal API authorization blueprint</a>. The last six posts walked the technology. This one is about the thing I think will outlast every specific technology choice in the project, the part that is genuinely the hardest to copy and the most valuable to try: <em>how they made the decisions, and how they wrote them down.</em> The technology is maybe forty percent of the work. The governance is the rest, and it is where most government standards efforts quietly die.</p>

<p>Here is the discipline in one sentence: every consequential choice in this architecture exists as an <a href="https://gitlab.opencode.de/sachsen-anhalt/mid/foederale-api-autorisierungsinfrastruktur">architecture decision record</a> — twenty-three of them — and each one states the problem, the decision drivers, the options considered, the good and bad of each option, and then the chosen option with the reasoning. Not “we use DPoP.” Instead: here is the problem sender-constraining solves, here are the three approaches, here is what is good and bad about no constraint, about mTLS binding, and about DPoP, and here is why DPoP won <em>for us</em> and what it costs us. The rejected options are documented as carefully as the winner. I cannot overstate how unusual and how valuable that is. When you inherit this system in five years, or when a new state joins the federation and asks “why didn’t you just use mutual TLS everywhere,” the answer is not locked in someone’s memory or lost with a departed contractor — it is written down, with the trade-off attached. They even chose the ADR <em>format</em> in an ADR, and cited the old Parnas essay about faking a rational design process, which tells you they take the practice seriously as a practice.</p>

<p>The second governance move is the one I have been beating a drum about for my whole career, so watching a government do it properly was a small thrill: <strong>the security requirements are derived from explicit attacker models, not asserted.</strong> Most security documents are a list of controls handed down as if from a mountain. Germany’s two protection tiers each come with a companion attacker-model document — one built on the OAuth security best-current-practice for the normal tier, one built on the FAPI attacker model for the high tier — that defines the adversary and the security goals <em>first</em>, and then derives the required mechanisms from them. For the high-assurance profile they went all the way to formal analysis, proving the profile satisfies the attacker model rather than claiming it does. When a control has a threat model behind it, a reviewer can evaluate whether the control actually addresses the threat, and an implementer understands <em>why</em> the requirement exists instead of treating it as ceremony. That is the difference between security theater and security engineering, and it is all in the open repository.</p>

<p>Third, they did it in public, and they consulted. The entire thing — principles, requirements, ADRs, target architecture, glossary — lives on Open CoDE, Germany’s public code platform, under an open license. They ran a formal public consultation, took feedback as tracked issues, closed it, and worked the responses in. This matters for a reason that goes beyond transparency as a virtue: a standard that independent agencies are eventually expected to <em>follow</em> has to earn legitimacy, and you earn legitimacy by letting the people who will be bound by the decision see the reasoning and push back on it before it hardens. Publishing the reasoning is not a nicety; it is how you get buy-in across organizations that do not report to you.</p>

<p>And fourth — this is the subtle one that I think US agencies especially need to hear — they separated <em>approving the specifications</em> from <em>making them mandatory</em>, and treated the second as its own hard problem. When the <a href="https://www.it-planungsrat.de/beschluss/b-2026-16-it">IT Planning Council approved the work in June</a>, it did not wave a wand and declare everything binding. It commissioned a project group, including the relevant federal security agencies, to work out <em>how</em> to make the requirements binding — implementation timelines, exemptions, transition periods, and support measures like guidelines and training — with a proposal due at a later session, alongside a pilot of the core components. That is a realistic two-step: get the specification right and consulted, <em>then</em> do the genuinely difficult organizational work of adoption, with runway and support, instead of mandating by memo and hoping. Anyone who has watched a mandate-by-memo bounce off an agency that had no budget, no timeline, and no help knows exactly why this sequencing matters.</p>

<p>I have spent the last year arguing that governance needs to be a first-class, kept, versioned artifact rather than a moment that happens and evaporates — that reviews should have provenance, that rules should be executable, that the record should be something you can diff over time. Germany’s project is the most complete real-world example of that philosophy applied to a national standard that I have seen. The ADRs <em>are</em> the memory. The attacker models <em>are</em> the provenance for every control. The public repository <em>is</em> the diffable record. It is, in a sense, the same thing I have been doing when I commit governance receipts next to my own APIs, just scaled to a federation and executed with real institutional weight behind it.</p>

<p>So if you are a US or European agency and you take only one thing from this entire series, I would not want it to be DPoP or FAPI or the transparency log, as good as those choices are. I would want it to be the <em>method</em>: write the decisions down with the options you rejected, derive your requirements from a named adversary, do it in the open where the people who will follow it can see and challenge it, and treat “make it binding” as a funded, phased program rather than an announcement. Get the method right and the technology choices become improvable over time. Get the method wrong and even the best technology choices rot in a PDF nobody can question. Next in the series I finally turn the lens onto us: a concrete playbook for how US agencies could emulate this, swapping German building blocks for login.gov, SAM.gov, and the tools we already have.</p>]]></content><author><name>Kin Lane</name></author><category term="API Governance" /><category term="Architecture Decision Records" /><category term="Digital Government" /><category term="Germany" /><category term="API Security" /><category term="Standards" /><category term="Transparency" /><summary type="html"><![CDATA[This is the seventh post in my series on Germany’s federal API authorization blueprint. The last six posts walked the technology. This one is about the thing I think will outlast every specific technology choice in the project, the part that is genuinely the hardest to copy and the most valuable to try: how they made the decisions, and how they wrote them down. The technology is maybe forty percent of the work. The governance is the rest, and it is where most government standards efforts quietly die.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/show-your-work-governing-an-api-standard-with-adrs-and-attacker-models.png" /><media:content medium="image" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/show-your-work-governing-an-api-standard-with-adrs-and-attacker-models.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">OpenAPI Overlays for Environment Promotion Across Dev, Staging, and Production</title><link href="https://apievangelist.com/2026/08/05/openapi-overlays-for-environment-promotion/" rel="alternate" type="text/html" title="OpenAPI Overlays for Environment Promotion Across Dev, Staging, and Production" /><published>2026-08-05T00:00:00+00:00</published><updated>2026-08-05T00:00:00+00:00</updated><id>https://apievangelist.com/2026/08/05/openapi-overlays-for-environment-promotion</id><content type="html" xml:base="https://apievangelist.com/2026/08/05/openapi-overlays-for-environment-promotion/"><![CDATA[<p>I keep finding these forgotten corners of my <a href="https://apievangelist.com/2026/06/26/the-many-use-cases-for-openapi-overlays/">use cases for OpenAPI Overlays</a> roundup that nobody talks about, and environment promotion is the one I get the most quietly frustrated about. Because the way most teams handle it today is with three copies of the same OpenAPI file. There’s a dev version, a staging version, and a production version, and they’re identical except for the server URLs, the token endpoints, and a rate-limit note or two. Then they drift. Somebody adds an operation to production’s spec and forgets to backport it to staging, and now your three “sources of truth” are lying to each other. I’ve watched this happen at enough shops that I’ve stopped calling it a mistake and started calling it a structural inevitability. If you maintain three files, you will eventually have three different APIs on paper.</p>

<p>The fix is to stop treating environments as different specs and start treating them as different projections of the same spec. You keep one canonical, environment-agnostic OpenAPI definition, and you keep a thin <a href="https://spec.openapis.org/overlay/latest.html">Overlay</a> per stage that carries only the per-environment truth. My running example through this series is the <a href="https://github.com/api-evangelist/products-api">Products API teaching template</a>, and its base spec deliberately doesn’t commit to a hostname. The servers block is a placeholder, the OAuth flow points at a generic identity host, and there’s nothing in it that ties it to a deployment. That’s on purpose. The base spec describes what the API <em>is</em> — <code class="language-plaintext highlighter-rouge">GET</code>/<code class="language-plaintext highlighter-rouge">POST /products</code>, <code class="language-plaintext highlighter-rouge">GET</code>/<code class="language-plaintext highlighter-rouge">PUT</code>/<code class="language-plaintext highlighter-rouge">DELETE /products/{id}</code>, the cancel operation, the <code class="language-plaintext highlighter-rouge">Product</code> schema, the <code class="language-plaintext highlighter-rouge">NotFound</code> and <code class="language-plaintext highlighter-rouge">TooManyRequests</code> responses. Where it runs is not the spec’s job. That’s the overlay’s job.</p>

<p>Here’s the dev and staging overlay. It rewrites the servers to the staging host, repoints the OAuth token and authorization URLs at the staging identity provider, and stamps a loud warning into the description so nobody mistakes it for the real thing.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">overlay</span><span class="pi">:</span> <span class="s">1.1.0</span>
<span class="na">info</span><span class="pi">:</span>
  <span class="na">title</span><span class="pi">:</span> <span class="s">Products API - Staging Environment Overlay</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">1.0.0</span>
<span class="na">extends</span><span class="pi">:</span> <span class="s">https://raw.githubusercontent.com/api-evangelist/products-api/main/openapi/products-api-openapi.yml</span>
<span class="na">actions</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.servers</span>
    <span class="na">update</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">url</span><span class="pi">:</span> <span class="s">https://staging.api.example.com/v1</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s">Staging environment - non-production</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.components.securitySchemes.OAuth2.flows.authorizationCode</span>
    <span class="na">update</span><span class="pi">:</span>
      <span class="na">authorizationUrl</span><span class="pi">:</span> <span class="s">https://identity.staging.example.com/oauth/authorize</span>
      <span class="na">tokenUrl</span><span class="pi">:</span> <span class="s">https://identity.staging.example.com/oauth/token</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.info</span>
    <span class="na">update</span><span class="pi">:</span>
      <span class="na">description</span><span class="pi">:</span> <span class="pi">&gt;-</span>
        <span class="s">STAGING ENVIRONMENT - This is a non-production deployment. Data resets</span>
        <span class="s">nightly at 00:00 UTC and should be treated as disposable. Do not store</span>
        <span class="s">anything here you expect to keep. Rate limits are relaxed for testing.</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">extends</code> field is doing the heavy lifting — it points at the canonical raw URL, so this overlay is meaningless on its own and can only ever describe the one true Products API. The three actions are surgical. They target the servers array, the OAuth flow inside <code class="language-plaintext highlighter-rouge">components.securitySchemes</code>, and the <code class="language-plaintext highlighter-rouge">info</code> object, and they touch nothing else. Every operation, every schema, every problem+json example comes straight from the base spec, unchanged.</p>

<p>The production overlay is the same shape with the real values, plus it hardens the description around rate limits instead of relaxing them.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">overlay</span><span class="pi">:</span> <span class="s">1.1.0</span>
<span class="na">info</span><span class="pi">:</span>
  <span class="na">title</span><span class="pi">:</span> <span class="s">Products API - Production Environment Overlay</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">1.0.0</span>
<span class="na">extends</span><span class="pi">:</span> <span class="s">https://raw.githubusercontent.com/api-evangelist/products-api/main/openapi/products-api-openapi.yml</span>
<span class="na">actions</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.servers</span>
    <span class="na">update</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">url</span><span class="pi">:</span> <span class="s">https://api.example.com/v1</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s">Production environment</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.components.securitySchemes.OAuth2.flows.authorizationCode</span>
    <span class="na">update</span><span class="pi">:</span>
      <span class="na">authorizationUrl</span><span class="pi">:</span> <span class="s">https://identity.example.com/oauth/authorize</span>
      <span class="na">tokenUrl</span><span class="pi">:</span> <span class="s">https://identity.example.com/oauth/token</span>
  <span class="pi">-</span> <span class="na">target</span><span class="pi">:</span> <span class="s">$.info</span>
    <span class="na">update</span><span class="pi">:</span>
      <span class="na">description</span><span class="pi">:</span> <span class="pi">&gt;-</span>
        <span class="s">PRODUCTION - Live customer data. Rate limits are strictly enforced at</span>
        <span class="s">1000 requests per minute per token; exceeding them returns 429 with a</span>
        <span class="s">problem+json body and RateLimit headers indicating your reset window.</span>
        <span class="s">Cancel operations are irreversible in this environment.</span>
</code></pre></div></div>

<p>Now put it in the pipeline, which is where the whole idea earns its keep. You promote a single build artifact — the canonical spec — from dev to staging to production, and at each stage your CI job applies the matching overlay to generate the environment-specific description that ships to that stage’s portal, mock server, or gateway. Same artifact, different overlay, swapped by stage name. There is exactly one place to add a new operation, and when you do, all three environments inherit it the next time they’re built. Drift becomes impossible because there’s nothing to drift <em>from</em>.</p>

<p>That’s the take I want to leave you with. Environment configuration is not three contracts you keep in sync by discipline and prayer. It is one contract with three projections, and the overlay is how you make that projection explicit, versioned, and reproducible. Stop copying the spec. Project it.</p>]]></content><author><name>Kin Lane</name></author><category term="OpenAPI" /><category term="Overlays" /><category term="DevOps" /><category term="CI/CD" /><category term="APIs" /><summary type="html"><![CDATA[I keep finding these forgotten corners of my use cases for OpenAPI Overlays roundup that nobody talks about, and environment promotion is the one I get the most quietly frustrated about. Because the way most teams handle it today is with three copies of the same OpenAPI file. There’s a dev version, a staging version, and a production version, and they’re identical except for the server URLs, the token endpoints, and a rate-limit note or two. Then they drift. Somebody adds an operation to production’s spec and forgets to backport it to staging, and now your three “sources of truth” are lying to each other. I’ve watched this happen at enough shops that I’ve stopped calling it a mistake and started calling it a structural inevitability. If you maintain three files, you will eventually have three different APIs on paper.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/openapi-overlays-for-environment-promotion.png" /><media:content medium="image" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/openapi-overlays-for-environment-promotion.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Scoring The Secondary Market: Does The Hype Have An API?</title><link href="https://apievangelist.com/2026/08/05/scoring-the-secondary-market-does-the-hype-have-an-api/" rel="alternate" type="text/html" title="Scoring The Secondary Market: Does The Hype Have An API?" /><published>2026-08-05T00:00:00+00:00</published><updated>2026-08-05T00:00:00+00:00</updated><id>https://apievangelist.com/2026/08/05/scoring-the-secondary-market-does-the-hype-have-an-api</id><content type="html" xml:base="https://apievangelist.com/2026/08/05/scoring-the-secondary-market-does-the-hype-have-an-api/"><![CDATA[<p>There is a particular kind of company I have been curious about for a while now: the one whose shares trade on the private secondary markets. Forge Global, Hiive, EquityZen, Nasdaq Private Market, Augment — these venues let people buy and sell stock in companies that have not gone public yet. If you want to know which private companies the market currently believes in, that is a reasonable place to look. Someone has done the work of deciding these names are worth transacting on. The valuations are real money. The conviction is priced.</p>

<p>So I started profiling them. Not their cap tables — their APIs. I have been running the secondary-market listings through the same pipeline I run everything else through: find the company, find its developer surface if it has one, harvest whatever contracts and artifacts are actually published, and then rate the result with the Kin Score. The question I am chasing is simple and a little rude. These companies are valued like technology companies. Are they operable like technology companies?</p>

<p>I now have 278 of them scored, and the answer is more interesting than either “yes” or “no.” Everything below is drawn from the <a href="https://providers.apievangelist.com/secondary-market/">secondary market section of the providers site</a>, which is where these profiles live as I work through them.</p>

<h2 id="what-the-numbers-say">What the numbers say</h2>

<p>The average composite Kin Score across the cohort is <strong>27.5</strong>, with a median of <strong>24.7</strong>. For context, that is the bottom half of the scale. Broken into bands:</p>

<ul>
  <li><strong>exemplar or strong: 25 companies (9%)</strong></li>
  <li>developing: 35 companies (13%)</li>
  <li><strong>emerging, thin, or minimal: 218 companies (78%)</strong></li>
</ul>

<p>Agent readiness — measured separately, because being usable by a human developer and being usable by an agent are not the same problem — comes out lower still. Mean <strong>19.8</strong>, median <strong>12.6</strong>. Nearly half the cohort, <strong>129 companies, score as human-only</strong>: no machine-readable contract, no agent-facing affordances, nothing an autonomous consumer could act against without a person in the loop. Eleven companies out of 278 rate agent-native.</p>

<p>The facet breakdown is where it gets genuinely useful, because it explains the shape of the failure rather than just its size. Averaged across the cohort:</p>

<table>
  <thead>
    <tr>
      <th>Facet</th>
      <th>Mean</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Discoverability</td>
      <td>76.8</td>
    </tr>
    <tr>
      <td>Commercial clarity</td>
      <td>31.6</td>
    </tr>
    <tr>
      <td>Developer ergonomics</td>
      <td>26.2</td>
    </tr>
    <tr>
      <td>Contract quality</td>
      <td>18.2</td>
    </tr>
    <tr>
      <td>Operational transparency</td>
      <td>15.5</td>
    </tr>
    <tr>
      <td>Governance</td>
      <td>8.8</td>
    </tr>
  </tbody>
</table>

<p>Read that top-to-bottom and you have the whole story. These companies are <strong>extremely easy to find and almost impossible to operate against</strong>. They have websites, positioning, press, a careers page, a blog. Discoverability at 76.8 says the marketing works. Governance at 8.8 and operational transparency at 15.5 say that once you get past the marketing there is very little contract, very little published operational reality, and almost no evidence of anyone governing the surface. The gap between 76.8 and 8.8 is the gap between being known and being usable.</p>

<h2 id="who-is-actually-good">Who is actually good</h2>

<p>The top of the table is not who I expected, and that is the most useful finding in the whole exercise:</p>

<table>
  <thead>
    <tr>
      <th>Company</th>
      <th>Kin Score</th>
      <th>Agent Readiness</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Hubble Network</td>
      <td>72.9</td>
      <td>50.5</td>
    </tr>
    <tr>
      <td>ControlUp</td>
      <td>68.2</td>
      <td>59.7</td>
    </tr>
    <tr>
      <td>Niural</td>
      <td>67.1</td>
      <td>68.0</td>
    </tr>
    <tr>
      <td>Method Financial</td>
      <td>65.7</td>
      <td>70.7</td>
    </tr>
    <tr>
      <td>GoFundMe</td>
      <td>64.0</td>
      <td>56.5</td>
    </tr>
    <tr>
      <td>OpenGov</td>
      <td>63.0</td>
      <td>77.5</td>
    </tr>
    <tr>
      <td>Lukka</td>
      <td>62.5</td>
      <td>60.8</td>
    </tr>
    <tr>
      <td>Read AI</td>
      <td>61.5</td>
      <td>56.3</td>
    </tr>
    <tr>
      <td>ModMed</td>
      <td>60.7</td>
      <td>58.8</td>
    </tr>
  </tbody>
</table>

<p>Satellite Bluetooth. Digital employee experience monitoring. Global payroll. Bank data connectivity. Government budgeting software. Crypto accounting data. These are infrastructure companies. They are, with a couple of exceptions, not the names that come up when people talk about hot private companies. The consumer brands with the recognizable logos are mostly not in this table — they are down in the 78%.</p>

<p>That inversion is the point. <strong>Private-market conviction and API maturity are not the same signal, and in this cohort they are barely correlated.</strong> The market is pricing brand, growth, and category. The Kin Score is measuring whether anyone can build on you. Those turn out to be close to independent variables.</p>

<h2 id="where-i-have-to-be-careful">Where I have to be careful</h2>

<p>I want to be honest about what this evidence does and does not support, because it would be easy to turn this into a cheap dunk and the cheap version would be wrong.</p>

<p><strong>A low score is not automatically a failure.</strong> A large share of this cohort is clinical-stage biotech, medical devices, consumer packaged goods, and deep-tech hardware. A company developing an mRNA therapeutic has no obligation to ship an OpenAPI, and scoring it against one tells you about its category, not its competence. The Kin Score answers “can a developer or an agent do anything with this company’s public surface,” and for a lot of these firms the honest answer is “no, and that is fine.” What the number is genuinely good for is separating the companies that <em>present</em> as technology companies from the ones that <em>behave</em> like them.</p>

<p><strong>The sample is small and biased.</strong> 278 companies is 1.6% of the 17,311 in my backlog. I am working through the highest-conviction slice first — companies listed on three or more venues — and within that, alphabetically. That is a real bias and I am not going to pretend the average holds for the other 98%. Ask me again at a few thousand.</p>

<p><strong>I am scoring the public surface only.</strong> Plenty of these companies have real, well-built APIs sitting behind a sales conversation, a partner agreement, or a login. That work is invisible to me and it does not count here. This is a measure of what a company publishes to the open web, not of what its engineers have built. A company with a great private API and no public evidence of it scores badly, and by the rubric’s own terms that is a correct result — but it is a narrower claim than “this company has no API.”</p>

<h2 id="why-i-keep-doing-it-anyway">Why I keep doing it anyway</h2>

<p>Because the interesting number is not the average, it is the distribution. A market where 9% of highly-valued private companies are meaningfully operable and 46% are entirely human-only is telling you something specific about where the next few years of integration work is going to come from — and about how much of the “everything is an API company now” story is positioning rather than architecture.</p>

<p>It also tells you something about agents specifically. Every one of these companies is going to be asked, fairly soon, whether an AI agent can transact with them. Right now, for most of this cohort, the answer is structurally no. Not “no, we chose not to” — no in the sense that there is nothing there to act against. The eleven agent-native companies in this set have a head start that is measured in years of accumulated contract, documentation, and operational discipline, not in a quarter of roadmap.</p>

<p>I will keep grinding through the backlog and publishing what the numbers say. The profiles, the artifacts, and the score history for every company are open on the network at <a href="https://providers.apievangelist.com/secondary-market/">providers.apievangelist.com/secondary-market</a>, so you do not have to take my averages on faith — you can go read the evidence for any single company and disagree with me about it.</p>]]></content><author><name>Kin Lane</name></author><category term="Secondary Market" /><category term="Kin Score" /><category term="Ratings" /><category term="Discovery" /><category term="Agent Readiness" /><category term="Research" /><category term="Strategy" /><summary type="html"><![CDATA[There is a particular kind of company I have been curious about for a while now: the one whose shares trade on the private secondary markets. Forge Global, Hiive, EquityZen, Nasdaq Private Market, Augment — these venues let people buy and sell stock in companies that have not gone public yet. If you want to know which private companies the market currently believes in, that is a reasonable place to look. Someone has done the work of deciding these names are worth transacting on. The valuations are real money. The conviction is priced.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/scoring-the-secondary-market-does-the-hype-have-an-api.png" /><media:content medium="image" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/scoring-the-secondary-market-does-the-hype-have-an-api.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Stytch Already Built the Self-Serve Onboarding the Agentic Web Needs</title><link href="https://apievangelist.com/2026/08/05/stytch-built-self-serve-onboarding/" rel="alternate" type="text/html" title="Stytch Already Built the Self-Serve Onboarding the Agentic Web Needs" /><published>2026-08-05T00:00:00+00:00</published><updated>2026-08-05T00:00:00+00:00</updated><id>https://apievangelist.com/2026/08/05/stytch-built-self-serve-onboarding</id><content type="html" xml:base="https://apievangelist.com/2026/08/05/stytch-built-self-serve-onboarding/"><![CDATA[<p>I keep coming back to the same contradiction. Every company tells me they are all in on AI, that agents are the future, that software is about to start talking to software at a scale we have never seen. And then they hand me an onboarding flow built for a human with a mouse, a corporate email address, and an afternoon to kill clicking through a dashboard. You cannot have it both ways. If an agent is going to use your API, an agent has to be able to get credentials for your API. That means a machine has to be able to register a client and walk away with a <code class="language-plaintext highlighter-rouge">client_id</code> and a <code class="language-plaintext highlighter-rouge">client_secret</code>. Most vendors still cannot do this, and I have spent enough time banging my head against that wall to notice when somebody actually got it right.</p>

<p>Stytch got it right. That is not a sentence I write often, so let me be precise about what I mean.</p>

<p>When I wrote about what <a href="https://apievangelist.com/2026/06/19/soundcloud-shows-what-programmatic-api-onboarding-should-look-like/">programmatic API onboarding</a> should look like, the ideal was a single script that opens a browser, lets you sign in, registers an application, and prints the credentials to your terminal. No support ticket. No sales call. No “contact us for API access.” Stytch’s Connected Apps feature clears that bar two different ways, and the second one is the one that matters for agents.</p>

<p>The first way is the honest, boring, fully supported path. You hold your Stytch project credentials, a <code class="language-plaintext highlighter-rouge">project_id</code> and a <code class="language-plaintext highlighter-rouge">secret</code>, and you authenticate with plain HTTP Basic against <code class="language-plaintext highlighter-rouge">https://test.stytch.com</code> or <code class="language-plaintext highlighter-rouge">https://api.stytch.com</code>. You <code class="language-plaintext highlighter-rouge">POST /v1/connected_apps/clients</code> with a <code class="language-plaintext highlighter-rouge">client_type</code> and a name, and you get back a <code class="language-plaintext highlighter-rouge">connected_app</code> object carrying a <code class="language-plaintext highlighter-rouge">client_id</code> and, for confidential clients, a <code class="language-plaintext highlighter-rouge">client_secret</code>. That secret is shown exactly once. Stytch stores a hash and cannot recover it, which is the correct behavior even if it means you have to actually read the response instead of going back to fish for it later. This is bucket (b) in my mental model: a management API and a token you paste in from an environment variable. It works, it is documented, and it does not require me to file anything with a human.</p>

<p>The second way is the one that made me sit up. Stytch implements RFC 7591, OAuth 2.0 Dynamic Client Registration, at <code class="language-plaintext highlighter-rouge">POST /v1/oauth2/register</code>. No authorization needed. A client, or an agent, can show up at runtime with no project credentials at all, post its <code class="language-plaintext highlighter-rouge">redirect_uris</code> and a name, and register itself. Public clients using <code class="language-plaintext highlighter-rouge">token_endpoint_auth_method</code> of <code class="language-plaintext highlighter-rouge">none</code> get deduplicated by their metadata, so an agent that registers twice with the same shape gets the same <code class="language-plaintext highlighter-rouge">client_id</code> back instead of littering your project with junk. This is not an accident. Stytch built this specifically for MCP, the protocol that lets agents discover and call tools, and they have written openly about why dynamic client registration is the missing piece for agentic auth. That is the whole point. An agent cannot stop and ask a human to go create an OAuth app in a dashboard. It has to register itself, and Stytch lets it.</p>

<p>So I wrote the script. It does both. Run it in the default mode and it uses your <code class="language-plaintext highlighter-rouge">STYTCH_PROJECT_ID</code> and <code class="language-plaintext highlighter-rouge">STYTCH_SECRET</code> to create a Connected App through the management endpoint. Pass <code class="language-plaintext highlighter-rouge">--dcr</code> and it skips the credentials entirely and hits the public dynamic registration endpoint, the way an agent would. Either way it prints <code class="language-plaintext highlighter-rouge">client_id</code> and, when there is one, <code class="language-plaintext highlighter-rouge">client_secret</code> to stdout. Node 18, standard library only, no npm install.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#!/usr/bin/env node
</span><span class="c1">// stytch-api-auth.mjs — Register a Stytch Connected App (Management or RFC 7591 DCR).</span>
<span class="c1">// Env: STYTCH_PROJECT_ID, STYTCH_SECRET, STYTCH_ENV=test|live</span>
<span class="c1">// Default: HTTP Basic POST /v1/connected_apps/clients</span>
<span class="c1">// --dcr:   public POST /v1/public/&lt;project_id&gt;/oauth2/register</span>
<span class="c1">// Docs: https://stytch.com/docs/b2b/api/connected-apps-create</span>
<span class="c1">//       https://stytch.com/docs/b2b/api/connected-app-dynamic-client-registration</span>
<span class="c1">// See the full, committed source at:</span>
<span class="c1">//   /assets/scripts/agentic-onboarding/stytch-api-auth.mjs</span>
<span class="c1">// (Full implementation is checked into the repo; the management + DCR paths,</span>
<span class="c1">//  HTTP Basic auth, env handling, --help, and credential printing are all there.)</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">parseArgs</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:util</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">process</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:process</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">ENV_BASE</span> <span class="o">=</span> <span class="p">{</span> <span class="na">test</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://test.stytch.com</span><span class="dl">"</span><span class="p">,</span> <span class="na">live</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://api.stytch.com</span><span class="dl">"</span> <span class="p">};</span>
<span class="kd">const</span> <span class="nx">CREATE_PATH</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">/v1/connected_apps/clients</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">dcrPath</span> <span class="o">=</span> <span class="p">(</span><span class="nx">id</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="s2">`/v1/public/</span><span class="p">${</span><span class="nx">id</span><span class="p">}</span><span class="s2">/oauth2/register`</span><span class="p">;</span> <span class="c1">// NOTE: canonical form is https://${projectDomain}/v1/oauth2/register</span>

<span class="kd">function</span> <span class="nx">basicAuth</span><span class="p">(</span><span class="nx">id</span><span class="p">,</span> <span class="nx">secret</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="dl">"</span><span class="s2">Basic </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">id</span><span class="p">}</span><span class="s2">:</span><span class="p">${</span><span class="nx">secret</span><span class="p">}</span><span class="s2">`</span><span class="p">).</span><span class="nx">toString</span><span class="p">(</span><span class="dl">"</span><span class="s2">base64</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">requireEnv</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">v</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">[</span><span class="nx">name</span><span class="p">];</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">v</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s2">`Missing required environment variable: </span><span class="p">${</span><span class="nx">name</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span> <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span> <span class="p">}</span>
  <span class="k">return</span> <span class="nx">v</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">req</span><span class="p">({</span> <span class="nx">url</span><span class="p">,</span> <span class="nx">method</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span> <span class="nx">headers</span> <span class="o">=</span> <span class="p">{},</span> <span class="nx">body</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span>
    <span class="nx">method</span><span class="p">,</span>
    <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="na">accept</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span> <span class="p">...</span><span class="nx">headers</span> <span class="p">},</span>
    <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">body</span><span class="p">),</span>
  <span class="p">});</span>
  <span class="k">return</span> <span class="p">{</span> <span class="nx">res</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">()</span> <span class="p">};</span>
<span class="p">}</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">createConnectedApp</span><span class="p">({</span> <span class="nx">base</span><span class="p">,</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">secret</span><span class="p">,</span> <span class="nx">clientType</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">website</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">CREATE_PATH</span><span class="p">,</span> <span class="nx">base</span><span class="p">).</span><span class="nx">toString</span><span class="p">();</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">text</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">req</span><span class="p">({</span>
    <span class="nx">url</span><span class="p">,</span>
    <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="na">authorization</span><span class="p">:</span> <span class="nx">basicAuth</span><span class="p">(</span><span class="nx">id</span><span class="p">,</span> <span class="nx">secret</span><span class="p">)</span> <span class="p">},</span>
    <span class="na">body</span><span class="p">:</span> <span class="p">{</span> <span class="na">client_type</span><span class="p">:</span> <span class="nx">clientType</span><span class="p">,</span> <span class="p">...(</span><span class="nx">name</span> <span class="o">&amp;&amp;</span> <span class="p">{</span> <span class="na">client_name</span><span class="p">:</span> <span class="nx">name</span> <span class="p">}),</span> <span class="p">...(</span><span class="nx">website</span> <span class="o">&amp;&amp;</span> <span class="p">{</span> <span class="na">redirect_urls</span><span class="p">:</span> <span class="p">[</span><span class="nx">website</span><span class="p">]</span> <span class="p">})</span> <span class="p">},</span>
  <span class="p">});</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">j</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">text</span><span class="p">);</span> <span class="k">return</span> <span class="p">(</span><span class="nx">j</span><span class="p">.</span><span class="nx">connected_app</span> <span class="o">??</span> <span class="nx">j</span><span class="p">);</span> <span class="p">}</span>
  <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Create (POST </span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2">) failed: </span><span class="p">${</span><span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">text</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">registerViaDcr</span><span class="p">({</span> <span class="nx">base</span><span class="p">,</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">website</span><span class="p">,</span> <span class="nx">authMethod</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">dcrPath</span><span class="p">(</span><span class="nx">id</span><span class="p">),</span> <span class="nx">base</span><span class="p">).</span><span class="nx">toString</span><span class="p">();</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">text</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">req</span><span class="p">({</span>
    <span class="nx">url</span><span class="p">,</span>
    <span class="na">body</span><span class="p">:</span> <span class="p">{</span> <span class="na">redirect_uris</span><span class="p">:</span> <span class="nx">website</span> <span class="p">?</span> <span class="p">[</span><span class="nx">website</span><span class="p">]</span> <span class="p">:</span> <span class="p">[],</span> <span class="p">...(</span><span class="nx">name</span> <span class="o">&amp;&amp;</span> <span class="p">{</span> <span class="na">client_name</span><span class="p">:</span> <span class="nx">name</span> <span class="p">}),</span> <span class="na">token_endpoint_auth_method</span><span class="p">:</span> <span class="nx">authMethod</span> <span class="p">},</span>
  <span class="p">});</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="k">return</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">text</span><span class="p">);</span>
  <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`DCR register (POST </span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2">) failed: </span><span class="p">${</span><span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">text</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">printCreds</span><span class="p">(</span><span class="nx">app</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">out</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">client_id</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">client_id</span><span class="p">,</span>
    <span class="na">client_secret</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">client_secret</span><span class="p">,</span>
    <span class="na">name</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">client_name</span><span class="p">,</span>
    <span class="na">client_type</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">client_type</span><span class="p">,</span>
    <span class="na">token_endpoint_auth_method</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">token_endpoint_auth_method</span><span class="p">,</span>
  <span class="p">};</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">k</span> <span class="k">of</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">out</span><span class="p">))</span> <span class="k">if</span> <span class="p">(</span><span class="nx">out</span><span class="p">[</span><span class="nx">k</span><span class="p">]</span> <span class="o">==</span> <span class="kc">null</span><span class="p">)</span> <span class="k">delete</span> <span class="nx">out</span><span class="p">[</span><span class="nx">k</span><span class="p">];</span>
  <span class="kd">const</span> <span class="nx">lines</span> <span class="o">=</span> <span class="p">[</span><span class="s2">`client_id=</span><span class="p">${</span><span class="nx">out</span><span class="p">.</span><span class="nx">client_id</span> <span class="o">??</span> <span class="dl">""</span><span class="p">}</span><span class="s2">`</span><span class="p">];</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">out</span><span class="p">.</span><span class="nx">client_secret</span><span class="p">)</span> <span class="nx">lines</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`client_secret=</span><span class="p">${</span><span class="nx">out</span><span class="p">.</span><span class="nx">client_secret</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="nx">lines</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">""</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">out</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span> <span class="dl">""</span><span class="p">);</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">stdout</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span><span class="nx">lines</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="se">\n</span><span class="dl">"</span><span class="p">));</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="p">{</span> <span class="na">values</span><span class="p">:</span> <span class="p">{</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">website</span><span class="p">,</span> <span class="nx">type</span><span class="p">,</span> <span class="nx">dcr</span><span class="p">,</span> <span class="nx">help</span> <span class="p">}</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">parseArgs</span><span class="p">({</span>
  <span class="na">options</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span> <span class="na">website</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span> <span class="na">type</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span>
    <span class="na">dcr</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">boolean</span><span class="dl">"</span> <span class="p">},</span> <span class="na">help</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">boolean</span><span class="dl">"</span><span class="p">,</span> <span class="na">short</span><span class="p">:</span> <span class="dl">"</span><span class="s2">h</span><span class="dl">"</span> <span class="p">},</span>
  <span class="p">},</span>
  <span class="na">strict</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">allowPositionals</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="p">});</span>

<span class="k">if</span> <span class="p">(</span><span class="nx">help</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Usage: stytch-api-auth [--dcr] [--name N] [--website URL] [--type T]
  default: POST /v1/connected_apps/clients (HTTP Basic project_id:secret)
  --dcr:   public RFC 7591 POST /v1/public/&lt;project_id&gt;/oauth2/register
  --type:  management: first_party|first_party_public|third_party|third_party_public (default third_party)
           dcr: public|confidential (default confidential)
  Env: STYTCH_PROJECT_ID, STYTCH_SECRET, STYTCH_ENV=test|live`</span><span class="p">);</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">base</span> <span class="o">=</span> <span class="nx">ENV_BASE</span><span class="p">[(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">STYTCH_ENV</span> <span class="o">||</span> <span class="dl">"</span><span class="s2">test</span><span class="dl">"</span><span class="p">).</span><span class="nx">toLowerCase</span><span class="p">()];</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">base</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">STYTCH_ENV must be "test" or "live".</span><span class="dl">'</span><span class="p">);</span> <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span> <span class="p">}</span>
<span class="kd">const</span> <span class="nx">projectId</span> <span class="o">=</span> <span class="nx">requireEnv</span><span class="p">(</span><span class="dl">"</span><span class="s2">STYTCH_PROJECT_ID</span><span class="dl">"</span><span class="p">);</span>

<span class="p">(</span><span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="kd">let</span> <span class="nx">app</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">dcr</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">app</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">registerViaDcr</span><span class="p">({</span> <span class="nx">base</span><span class="p">,</span> <span class="na">id</span><span class="p">:</span> <span class="nx">projectId</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">website</span><span class="p">,</span> <span class="na">authMethod</span><span class="p">:</span> <span class="nx">type</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">public</span><span class="dl">"</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">none</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">client_secret_basic</span><span class="dl">"</span> <span class="p">});</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">secret</span> <span class="o">=</span> <span class="nx">requireEnv</span><span class="p">(</span><span class="dl">"</span><span class="s2">STYTCH_SECRET</span><span class="dl">"</span><span class="p">);</span>
      <span class="nx">app</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">createConnectedApp</span><span class="p">({</span> <span class="nx">base</span><span class="p">,</span> <span class="na">id</span><span class="p">:</span> <span class="nx">projectId</span><span class="p">,</span> <span class="nx">secret</span><span class="p">,</span> <span class="na">clientType</span><span class="p">:</span> <span class="nx">type</span> <span class="o">||</span> <span class="dl">"</span><span class="s2">third_party</span><span class="dl">"</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">website</span> <span class="p">});</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">app</span><span class="p">?.</span><span class="nx">client_id</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">No client_id returned by Stytch.</span><span class="dl">"</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">app</span><span class="p">.</span><span class="nx">client_secret</span><span class="p">)</span> <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Note: no client_secret (public client). Use PKCE.</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">printCreds</span><span class="p">(</span><span class="nx">app</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Error:</span><span class="dl">"</span><span class="p">,</span> <span class="nx">e</span><span class="p">?.</span><span class="nx">message</span> <span class="o">||</span> <span class="nx">e</span><span class="p">);</span>
    <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">})();</span>
</code></pre></div></div>

<p>I will be honest about the one seam. The dynamic registration endpoint canonically lives on your project’s custom authentication domain, and the project-id-in-the-path form I use in the script is the public fallback you want to verify against your own project before you lean on it. That is a small annoyance, not a wall. Everything else here is exactly what I have been asking the rest of the industry for. The script is committed in the repo at <code class="language-plaintext highlighter-rouge">/assets/scripts/agentic-onboarding/stytch-api-auth.mjs</code>.</p>

<p>Here is the thing I want every other vendor to take from this. Stytch did not have to invent some proprietary onboarding API to meet the agentic moment. They implemented an existing standard, RFC 7591, and pointed it at the obvious use case. That is the whole move. The agents are not waiting for your roadmap. They are waiting for an endpoint they can call without a human in the loop, and Stytch shipped it. I will take what I can get, and this is one of the few times I get to point at a company and say: do this.</p>]]></content><author><name>Kin Lane</name></author><category term="Onboarding" /><category term="Authentication" /><category term="OAuth" /><category term="Stytch" /><category term="Agents" /><category term="AI" /><summary type="html"><![CDATA[I keep coming back to the same contradiction. Every company tells me they are all in on AI, that agents are the future, that software is about to start talking to software at a scale we have never seen. And then they hand me an onboarding flow built for a human with a mouse, a corporate email address, and an afternoon to kill clicking through a dashboard. You cannot have it both ways. If an agent is going to use your API, an agent has to be able to get credentials for your API. That means a machine has to be able to register a client and walk away with a client_id and a client_secret. Most vendors still cannot do this, and I have spent enough time banging my head against that wall to notice when somebody actually got it right.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/stytch-built-self-serve-onboarding.png" /><media:content medium="image" url="https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/stytch-built-self-serve-onboarding.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>