<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: caderaven6851</title>
    <description>The latest articles on DEV Community by caderaven6851 (@caderaven6851).</description>
    <link>https://dev.to/caderaven6851</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4065325%2F5fb3809a-8378-4fda-ac64-ba5d865a95ce.png</url>
      <title>DEV Community: caderaven6851</title>
      <link>https://dev.to/caderaven6851</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/caderaven6851"/>
    <language>en</language>
    <item>
      <title>Realtime message tracing at fan-out — security controls that survive a reconnect</title>
      <dc:creator>caderaven6851</dc:creator>
      <pubDate>Mon, 31 Aug 2026 03:06:54 +0000</pubDate>
      <link>https://dev.to/caderaven6851/realtime-message-tracing-at-fan-out-security-controls-that-survive-a-reconnect-ep8</link>
      <guid>https://dev.to/caderaven6851/realtime-message-tracing-at-fan-out-security-controls-that-survive-a-reconnect-ep8</guid>
      <description>&lt;p&gt;If you just want a live poll to land for every attendee in a session — and to be able to prove afterwards that it did — the least complex shape that works is a sequence-first realtime channel. The server stamps every message with a monotonic id before fan-out, clients replay from the last id they hold after a reconnect, and message tracing then costs one integer per message instead of a whole logging pipeline. Security controls hang off the same seam: short-lived, channel-scoped tokens you can revoke mid-session.&lt;/p&gt;

&lt;p&gt;The rest of this is about when that shape is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a live-poll fan-out actually costs you
&lt;/h2&gt;

&lt;p&gt;Two meters, and they are nowhere near the same size.&lt;/p&gt;

&lt;p&gt;Connection minutes are the boring one: peak concurrent clients multiplied by session length. Delivered messages are the other, and delivered messages are where the bill lives, because fan-out multiplies. One publish into a channel with N subscribers is N deliveries, and providers bill the N, not the 1.&lt;/p&gt;

&lt;p&gt;Size it for a B2B SaaS customer-success platform running a 40-minute session for 900 attendees. Connection minutes come to 900 × 40 = 36,000, which is a rounding error on any plan. Now the poll: six questions, most people vote, so call it 5,400 inbound votes. If you re-broadcast the running tally on every vote — the obvious implementation, and the one you will write first — that is 5,400 × 900 ≈ 4.9 million deliveries for a single session. The connection meter is noise next to that.&lt;/p&gt;

&lt;p&gt;Coalesce the tally and the dominant term collapses. Publish at most one tally frame per second per question while voting is open, plus one final frame when the question closes: roughly 250 frames, 225,000 deliveries, same product behaviour. &lt;strong&gt;At fan-out the only lever that matters is how many frames you publish, not how big they are.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two shapes for the same session, and the invariant each one holds
&lt;/h2&gt;

&lt;p&gt;Shape one is broadcast-first. The broker is transport and nothing more: your server publishes the rendered tally, connected clients paint it, and a client that missed a frame is simply behind until the next one arrives. The invariant is thin — a connected subscriber receives frames published while it was connected, in order — and that is the whole contract. Recovery is a plain HTTP snapshot fetch on reconnect. It is genuinely cheaper to operate, and for a poll whose result is decoration on a slide it is the correct choice.&lt;/p&gt;

&lt;p&gt;Shape two is sequence-first. Your API writes the vote to your own store, assigns a per-channel monotonic sequence number, and only then publishes a thin envelope carrying no payload at all — just enough for the client to know it is stale.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"session.8f21.poll"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"seq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;184&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"tally.updated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"01K3QF7X9M2E4V8Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"published_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-08-31T09:14:02Z"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That shape asks exactly two things of whatever occupies the fan-out slot: a token you can scope to one channel and expire, and a publish call cheap enough to make per frame. Infrai's realtime surface is one of the candidates worth shortlisting there, mostly because it is self-describing — &lt;code&gt;GET /v1/discovery&lt;/code&gt; returns the request schema, the response schema and a runnable example for each capability, so wiring token issuance is a read of one capability page rather than the adoption of another SDK. The integration cost of a realtime provider is mostly reading, not typing.&lt;/p&gt;

&lt;p&gt;The invariant is stronger than in shape one, and more expensive. The store is the source of truth, the channel is a hint, and the client's question after a reconnect is not "what did I miss" but "give me everything after 184". Duplicate delivery stops mattering, because the client dedupes on &lt;code&gt;message_id&lt;/code&gt;. Out-of-order delivery stops mattering, because &lt;code&gt;seq&lt;/code&gt; is total per channel. You pay for that with a write path and a replay endpoint you now own and have to test — including the ugly case where the write commits and the publish does not, which your client must survive by polling the replay endpoint on a slow timer.&lt;/p&gt;

&lt;p&gt;Neither shape gives you exactly-once at the edge. Nothing does. Treat every fan-out as at-least-once and make the client idempotent on the message id, and most of the "the poll showed 41%, then 39%" class of report disappears before anyone files it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should message tracing and security controls work in a customer support chat?
&lt;/h2&gt;

&lt;p&gt;The same session usually carries a support chat beside the poll: attendees asking questions, one or two agents answering. Same plumbing, different risk profile. On the poll channel the worst case is a wrong number on a slide; on the customer support chat the worst case is an attendee subscribing to a channel that carries another customer's ticket context.&lt;/p&gt;

&lt;p&gt;Three controls do most of the work, and all three are decided when the token is issued rather than enforced in the client:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a channel scope that names exactly the channels this participant may read and write, with no wildcard suffix that a curious front-end can widen;&lt;/li&gt;
&lt;li&gt;a TTL short enough that a leaked token expires before the session does — 15 minutes with a refresh beats one token for the whole 40;&lt;/li&gt;
&lt;li&gt;a revoke path you actually call, on agent handoff and on any auth event that changes the subject.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tracing is the fourth thing, and it is the one teams bolt on late. If the token carries the auth subject and the publish path stamps &lt;code&gt;message_id&lt;/code&gt; and &lt;code&gt;seq&lt;/code&gt;, then "who was allowed to see what, and when" is a join over data you already hold. If it doesn't, you are reconstructing intent from connection logs at 2am during an escalation.&lt;/p&gt;

&lt;p&gt;Here is the token-issue leg — a single POST to &lt;code&gt;/v1/realtime/token/issue&lt;/code&gt;, with the retry behaviour worth insisting on in review:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;issue_poll_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attendee_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Mint a short-lived token scoped to one session&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s poll channel.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.poll&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attendee_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttl_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;# A retried mint returns the same grant instead of a second live token.
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;poll-token-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;attendee_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/realtime/token/issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token issue refused: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token issue rate limited after 4 attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;grant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;issue_poll_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8f21&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attendee-4471&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idempotency key is the part people skip. A client that retries after a network blip gets its existing grant back rather than a second live token, which matters because a revoke list is only operationally useful while it stays short.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the platforms differ on delivery guarantees
&lt;/h2&gt;

&lt;p&gt;Everyone advertises "realtime". The difference that decides your architecture is what the edge promises and what happens on reconnect.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What the edge promises&lt;/th&gt;
&lt;th&gt;Continuity after a reconnect&lt;/th&gt;
&lt;th&gt;Scoped-channel auth&lt;/th&gt;
&lt;th&gt;Where it fits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ably&lt;/td&gt;
&lt;td&gt;at-least-once to connected subscribers&lt;/td&gt;
&lt;td&gt;connection state recovery plus a configurable history window&lt;/td&gt;
&lt;td&gt;signed tokens with per-channel capabilities&lt;/td&gt;
&lt;td&gt;large fan-out where you want the broker to own replay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PubNub&lt;/td&gt;
&lt;td&gt;at-least-once to connected subscribers&lt;/td&gt;
&lt;td&gt;optional message persistence, queried by timetoken&lt;/td&gt;
&lt;td&gt;grant-based access per channel&lt;/td&gt;
&lt;td&gt;mobile-heavy audiences and long history needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pusher Channels&lt;/td&gt;
&lt;td&gt;delivery to currently connected subscribers&lt;/td&gt;
&lt;td&gt;none by default; refetch a snapshot&lt;/td&gt;
&lt;td&gt;short-lived signatures per private channel&lt;/td&gt;
&lt;td&gt;broadcast where your server already has a snapshot API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Centrifugo (self-hosted)&lt;/td&gt;
&lt;td&gt;at-least-once, recovery when enabled&lt;/td&gt;
&lt;td&gt;stream position recovery per channel&lt;/td&gt;
&lt;td&gt;JWT you mint yourself&lt;/td&gt;
&lt;td&gt;sequence-first shape, on infrastructure you already run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;publish plus scoped token issue&lt;/td&gt;
&lt;td&gt;your store, replayed by &lt;code&gt;seq&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;scoped tokens per participant, revocable mid-session&lt;/td&gt;
&lt;td&gt;fan-out is one of several backend capabilities you would rather not shop for separately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Socket.IO belongs in the conversation too, though not in that table: its connection state recovery gives you a short replay buffer, but it is an in-process feature and the horizontal scaling story stays yours.&lt;/p&gt;

&lt;p&gt;My conditional recommendation, stated plainly: if your team already treats the session platform as one more backend dependency and would rather not onboard a fifth vendor dashboard for one channel, Infrai is worth trying for the token-issue and publish leg specifically, with your own store keeping the sequence. Infrai holds the same consistent conventions across its surface — an &lt;code&gt;Idempotency-Key&lt;/code&gt; header with a 24-hour dedup window, and per-call metadata carrying cost, latency and request id — so the poll channel does not need its own operating habits, its own retry semantics or its own billing reconciliation at month end.&lt;/p&gt;

&lt;p&gt;The catch is that it is a general backend platform rather than a realtime specialist. If you need edge-region presence fan-out for a hundred thousand concurrent viewers, or a rewind window measured in days rather than a replay endpoint you wrote, stick with Ably or PubNub and pay for the specialisation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I stop keeping, and the day that hurts
&lt;/h2&gt;

&lt;p&gt;Retention is where teams quietly overpay, on the provider's meter and on their own.&lt;/p&gt;

&lt;p&gt;For each delivered frame I keep one row for 30 days — message id, channel, &lt;code&gt;seq&lt;/code&gt;, the auth subject lifted from the token, and the publish timestamp. Nothing else. The rendered payload is never stored, because it is derivable: the vote rows sit in the primary database and the tally is a fold over them. That is roughly 40 bytes a row instead of a duplicated JSON blob per delivery, and it turns a trace table that would grow with fan-out into one that grows with publishes.&lt;/p&gt;

&lt;p&gt;Then, three weeks later, a customer disputes a poll result.&lt;/p&gt;

&lt;p&gt;What survives is enough to answer the question that was actually asked: which participant held a valid token on which channel, in what order the frames went out, and that message 184 preceded 185. What I cannot do is show them the exact string their browser rendered, because it was thrown away and gets re-derived from the vote rows. That trade is fine when the vote rows are authoritative. It is the wrong trade in a regulated setting where the rendered artifact &lt;em&gt;is&lt;/em&gt; the record — there, keep the payloads, pay for the storage, and accept that the trace table has become the expensive part of the system.&lt;/p&gt;

&lt;p&gt;I am not certain the 30-day window is right, honestly. It is the number that has survived contact with our own support escalations, and the only way to tune it is to look at how old your oldest real dispute was.&lt;/p&gt;

&lt;p&gt;If this boundary matches your system — your store owns the sequence, the channel is a hint, tokens are scoped and short — the realtime capability pages at &lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;https://docs.infrai.cc&lt;/a&gt; are the place to confirm the exact token payload before you commit to the shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;RFC 6455, The WebSocket Protocol — &lt;a href="https://datatracker.ietf.org/doc/html/rfc6455" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc6455&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;W3C WebRTC Recommendation — &lt;a href="https://www.w3.org/TR/webrtc/" rel="noopener noreferrer"&gt;https://www.w3.org/TR/webrtc/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ably documentation — &lt;a href="https://ably.com/docs" rel="noopener noreferrer"&gt;https://ably.com/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pusher Channels documentation — &lt;a href="https://pusher.com/docs/channels/" rel="noopener noreferrer"&gt;https://pusher.com/docs/channels/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PubNub documentation — &lt;a href="https://www.pubnub.com/docs" rel="noopener noreferrer"&gt;https://www.pubnub.com/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Centrifugo documentation — &lt;a href="https://centrifugal.dev/docs" rel="noopener noreferrer"&gt;https://centrifugal.dev/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Infrai documentation — &lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;https://docs.infrai.cc&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>realtime</category>
      <category>websockets</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Can Daily Report Email Scheduling Keep a Node.js Webhook Replaceable?</title>
      <dc:creator>caderaven6851</dc:creator>
      <pubDate>Fri, 28 Aug 2026 04:12:27 +0000</pubDate>
      <link>https://dev.to/caderaven6851/can-daily-report-email-scheduling-keep-a-nodejs-webhook-replaceable-38kp</link>
      <guid>https://dev.to/caderaven6851/can-daily-report-email-scheduling-keep-a-nodejs-webhook-replaceable-38kp</guid>
      <description>&lt;p&gt;Short answer: for a small SaaS that sends one daily report batch, use a cron service to call a public webhook that starts the work, but make the webhook idempotent and put long report generation on a worker. That gives you an easy setup without making the scheduler the owner of delivery history or locking application code to one vendor.&lt;/p&gt;

&lt;p&gt;The important distinction is easy to miss: a scheduler can decide when to ask for a report; it cannot, by itself, prove that every email was delivered exactly once. If the request times out after the mail provider accepted the message, retrying the request can create a duplicate. The application needs a delivery key, a database record, and a worker boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  A daily report is an audit record before it is a timer
&lt;/h2&gt;

&lt;p&gt;Treat the public Express route as a command to create or resume a report job, not as the place where the report is rendered and sent. The route should authenticate the caller, derive a stable run key such as &lt;code&gt;daily-report:2026-08-11&lt;/code&gt;, insert that key under a unique constraint, and return success for a repeated request that refers to the same job. The exact Express syntax is a framework detail; the contract is the part worth preserving during migration.&lt;/p&gt;

&lt;p&gt;That contract has four invariants:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A scheduler trigger is allowed to arrive more than once.&lt;/li&gt;
&lt;li&gt;A report job has one application-owned idempotency key.&lt;/li&gt;
&lt;li&gt;The worker records report and email status in the database, because the cron run output keeps only the first 4 KB.&lt;/li&gt;
&lt;li&gt;A retry never assumes that a timeout means “nothing happened.” It checks the stored state and the email provider's own idempotency or message status facilities.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is also where a public endpoint becomes a security boundary. Cron tasks support a public &lt;code&gt;http_url&lt;/code&gt;; an internal-only Express address will not receive the request. Put authentication and replay protection at the route, and keep the scheduler key out of the URL.&lt;/p&gt;

&lt;p&gt;Infrai is a reasonable fit for the scheduling part when the team wants one REST API and one key across backend services, rather than another SDK, credential, and dashboard. Its plain HTTP surface also keeps the scheduler adapter small: replacing it later means changing the trigger adapter, not the report job contract. That is a concrete portability benefit, not a promise that every scheduler has identical semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the scheduler behind one replaceable adapter
&lt;/h2&gt;

&lt;p&gt;The first artifact should be a scheduler adapter whose only job is to issue a trigger. Keep the report job name, idempotency key, status transitions, and worker interface in application code. That arrangement makes the uncomfortable question testable: if the scheduler disappears next quarter, can another service call the same endpoint and create the same job without changing the mail path?&lt;/p&gt;

&lt;p&gt;One sentence is enough for the rule.&lt;/p&gt;

&lt;p&gt;Replace the timer, not the business contract.&lt;/p&gt;

&lt;p&gt;For this workflow, the choice is reversible only if the public endpoint accepts a stable command and the database owns the result. An Express route can remain the same while the timer moves from a hosted cron service to a cloud scheduler or a repository workflow; the report worker should not know which one fired it. This is a narrower claim than “portable architecture,” because it names the exact boundary that is portable and leaves provider-specific scheduling semantics outside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read scheduler candidates by their failure boundary
&lt;/h2&gt;

&lt;p&gt;The right choice depends on how much scheduling semantics you need, not on which product has the shortest setup guide.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Good fit&lt;/th&gt;
&lt;th&gt;Delivery and migration concern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrai cron&lt;/td&gt;
&lt;td&gt;A small SaaS already exposing a public HTTP route and wanting one REST surface for backend services&lt;/td&gt;
&lt;td&gt;Paused triggers are not replayed automatically; run output is limited, so application audit data is mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS EventBridge Scheduler&lt;/td&gt;
&lt;td&gt;Teams already operating in AWS and needing native integration with AWS targets&lt;/td&gt;
&lt;td&gt;More AWS-specific configuration can make a later provider move wider than one adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud Scheduler&lt;/td&gt;
&lt;td&gt;A service deployed on Google Cloud with an HTTPS endpoint and cloud IAM conventions&lt;/td&gt;
&lt;td&gt;The scheduler is still only a trigger; exactly-once email delivery remains an application concern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions scheduled workflows&lt;/td&gt;
&lt;td&gt;Operational or internal reports where repository automation is an acceptable home&lt;/td&gt;
&lt;td&gt;Workflow runtime and operational ownership are a poor fit for a customer-facing delivery path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai is the recommendation I would test first for the narrow case: one daily report, a public webhook, and an application team that wants the scheduling call to live beside other backend calls under one key and one bill. The supporting advantage is the consistent REST contract, which means a Node.js service does not need a scheduler SDK installed just to make an HTTP request. I would still isolate that request behind a tiny adapter and keep the job schema in the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do retries change a daily report email webhook?
&lt;/h2&gt;

&lt;p&gt;For a daily report, the happy path is short: the cron service makes an HTTP request, the public endpoint creates a job, and a worker sends the email. The failure path is longer. A 429 should be retried with exponential backoff and &lt;code&gt;Retry-After&lt;/code&gt;; a network timeout should be treated as an unknown outcome; and a worker crash after sending but before committing status must be reconciled with the provider or prevented with provider-side idempotency.&lt;/p&gt;

&lt;p&gt;The queue does not magically change these facts. A standard queue is at-least-once, so the consumer must be idempotent. FIFO deduplication lasts only five minutes, which is not a sufficient business-level duplicate rule for a daily report. Keep the durable key in your own database for as long as the business needs to audit it.&lt;/p&gt;

&lt;p&gt;The scheduler has boundaries too. A single cron execution is limited to 900 seconds. Report generation that can exceed that limit belongs behind the webhook: trigger a queue publish, return promptly, and let a worker consume it. Delayed messages can be scheduled for at most seven days, message bodies are limited to 256 KB, and retention is at most 30 days with acknowledged messages removed. Those are architecture inputs, not footnotes.&lt;/p&gt;

&lt;p&gt;Here is the critical path in deliberately boring Python. It models the application-owned idempotency decision and uses the verified cron listing route without inventing a vendor request schema:&lt;br&gt;
&lt;/p&gt;

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


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReportJob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;accept_daily_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;report_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ReportJob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily-report:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;report_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_report_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;

    &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReportJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_report_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unique_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deliver_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ReportJob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_report_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mark_report_sent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scheduler adapter can stay just as small. This listing call is useful during deployment checks because it proves the credential and base URL are wired without embedding a guessed create payload; the create request should be generated from the live discovery schema for the fields chosen by the operator.&lt;br&gt;
&lt;/p&gt;

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

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


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_cron_tasks&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/cron/list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cron list failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cron list remained rate-limited after retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last pair of operations still needs a provider-specific reconciliation strategy if the provider cannot make &lt;code&gt;send_report&lt;/code&gt; idempotent. I’m not claiming the database and mail API can form one transaction. They cannot. The useful design is to make the ambiguity visible and give it an owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this design stops being the right tool
&lt;/h2&gt;

&lt;p&gt;Do not use a cron trigger as a substitute for a workflow engine. There is no DAG orchestration or fan-out/join primitive in this capability set, so a report that coordinates many dependent stages, waits for branches, and must catch up after a pause belongs with Airflow or Temporal. Those systems cost more operational attention, but their model matches the problem.&lt;/p&gt;

&lt;p&gt;Stick with a direct cloud scheduler when the service already has a strong cloud boundary and the extra platform surface is less important than native IAM and observability. Choose GitHub Actions for an internal report where repository ownership is the real operational boundary. Choose a queue plus workers when rendering or sending can exceed 900 seconds, when recipient fan-out is large, or when retries need a durable state machine.&lt;/p&gt;

&lt;p&gt;The catch is that this recommendation is intentionally narrow. It is not suitable when missed runs must be replayed automatically, when the endpoint cannot be public HTTPS, or when auditability depends on complete scheduler output rather than your database. Your mileage may vary if the email provider's idempotency guarantees are weaker than the report's business requirements; validate that contract before production.&lt;/p&gt;

&lt;p&gt;For the scheduler adapter, use the documented &lt;code&gt;POST /v1/cron/create&lt;/code&gt; route discovered for this capability. The API base is &lt;code&gt;https://api.infrai.cc/v1&lt;/code&gt;, and authentication uses &lt;code&gt;Authorization: Bearer $INFRAI_API_KEY&lt;/code&gt;; keep the exact request fields aligned with the live discovery schema. A minimal adapter should also set an explicit HTTP method, handle non-2xx responses, and back off on 429 responses.&lt;/p&gt;

&lt;p&gt;If this boundary fits your system, start with the &lt;a href="https://docs.infrai.cc/en/guides/cron/answers/why-daily-scheduled-email-should-enqueue-jobs-instead-o/" rel="noopener noreferrer"&gt;cron capability guide&lt;/a&gt; and verify the current request schema before wiring the adapter. The migration test is simple: replace the scheduler call in one module while the report job, idempotency key, database records, and worker remain unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-run-lambda-schedule.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-run-lambda-schedule.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/scheduler/docs/overview" rel="noopener noreferrer"&gt;https://cloud.google.com/scheduler/docs/overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule" rel="noopener noreferrer"&gt;https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>scheduling</category>
      <category>webhooks</category>
      <category>architecture</category>
      <category>node</category>
    </item>
    <item>
      <title>Password-Reset Event Notifications: Idempotency Keys for Email and SMS Retry Audits</title>
      <dc:creator>caderaven6851</dc:creator>
      <pubDate>Wed, 26 Aug 2026 04:43:01 +0000</pubDate>
      <link>https://dev.to/caderaven6851/password-reset-event-notifications-idempotency-keys-for-email-and-sms-retry-audits-20dm</link>
      <guid>https://dev.to/caderaven6851/password-reset-event-notifications-idempotency-keys-for-email-and-sms-retry-audits-20dm</guid>
      <description>&lt;p&gt;Use a durable delivery-claim ledger keyed by the logical password-reset challenge, channel, and message purpose; commit that claim before dispatch, reuse the same idempotency key on every retry, and record an ambiguous outcome as unknown rather than pretending the notification was sent exactly once. For a gaming account reset with a short expiry, compliance evidence is the deciding constraint: the system must explain why an email or SMS was attempted, which logical message it represented, and what the transport acknowledged without storing the reset secret itself.&lt;/p&gt;

&lt;p&gt;Exactly-once is the wrong external promise. A worker can crash after a transport accepts a message but before the acknowledgement reaches the backend. No local flag can distinguish that case from a request the transport never accepted. The defensible goal is narrower: one durable intent, one stable deduplication identity, bounded retry behavior, and an audit trail that preserves uncertainty.&lt;/p&gt;

&lt;p&gt;It sounds fussy. It isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimize reset data before deduplicating it
&lt;/h2&gt;

&lt;p&gt;The ledger needs an opaque challenge identity, not the secret that authorizes the password change. That privacy boundary comes first; deduplication cannot justify copying credentials into a longer-lived evidence store.&lt;/p&gt;

&lt;p&gt;Start with the unit of intent, not the queue delivery. A queue message ID identifies one delivery attempt by the broker; a password-reset challenge identifies the user-visible action. If a handler derives its idempotency key from the queue message ID, a redelivery with a new envelope can pass the guard and send the same reset twice. Derive the stable key from immutable business inputs such as &lt;code&gt;challenge_id&lt;/code&gt;, &lt;code&gt;channel&lt;/code&gt;, and &lt;code&gt;message_purpose&lt;/code&gt;. Keep &lt;code&gt;template_version&lt;/code&gt; in the recorded payload hash when content changes matter to the evidence, but don't casually put a mutable template version into the deduplication key: doing so may turn a rendering update into permission to notify again.&lt;/p&gt;

&lt;p&gt;For a workflow that deliberately sends both channels, email and SMS need separate claims because they are separate intended effects. For a fallback workflow, the workflow state must authorize only one active channel at a time; two independent consumers racing over the same account event can each be internally idempotent and still produce two messages. Deduplication is local to the identity you define — a badly scoped identity gives a perfectly consistent wrong answer.&lt;/p&gt;

&lt;p&gt;A Node.js backend should put the unique constraint and state transition in the database, even though the example below is Python. The language isn't the concurrency boundary. The database is. An in-process map, a request-scoped flag, or a cache entry created after dispatch leaves a gap in which two workers can both decide they are first.&lt;/p&gt;

&lt;p&gt;The minimum invariants are deliberately small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One row exists for each logical message and channel.&lt;/li&gt;
&lt;li&gt;The unique claim is committed before any external side effect begins.&lt;/li&gt;
&lt;li&gt;Every transport retry reuses the same idempotency key and content hash.&lt;/li&gt;
&lt;li&gt;Expired challenges are suppressed before dispatch and recorded with a reason.&lt;/li&gt;
&lt;li&gt;An acknowledgement records the transport request identifier; a timeout records an unknown outcome, not a fabricated success or failure.&lt;/li&gt;
&lt;li&gt;Logs contain an opaque challenge identifier and payload hash, never the reset token or raw message body.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last distinction matters. &lt;code&gt;accepted&lt;/code&gt;, &lt;code&gt;rejected&lt;/code&gt;, and &lt;code&gt;unknown&lt;/code&gt; are three different facts. Collapsing &lt;code&gt;unknown&lt;/code&gt; into &lt;code&gt;rejected&lt;/code&gt; makes an automatic retry look safe when it may duplicate an already accepted SMS. Collapsing it into &lt;code&gt;accepted&lt;/code&gt; makes the audit record claim evidence the system never received. I'm not sure every transport exposes enough lookup data to resolve an unknown result; that capability has to be verified for the chosen integration before the retry policy is approved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should compliance evidence prove about Node.js email and SMS retries?
&lt;/h2&gt;

&lt;p&gt;The ledger should model a state machine, not a Boolean &lt;code&gt;sent&lt;/code&gt; column. A practical sequence is &lt;code&gt;claimed&lt;/code&gt; to &lt;code&gt;dispatching&lt;/code&gt; to &lt;code&gt;accepted&lt;/code&gt;, with terminal &lt;code&gt;expired&lt;/code&gt; and &lt;code&gt;rejected&lt;/code&gt; states plus an &lt;code&gt;unknown&lt;/code&gt; state for an interrupted acknowledgement. Each transition should be append-only in the audit history even if a current-state column exists for fast reads.&lt;/p&gt;

&lt;p&gt;Consider a ten-minute reset challenge. The API creates challenge &lt;code&gt;rst_7f31&lt;/code&gt;, records its expiry, and publishes a notification intent. Worker A claims the email message and begins dispatch. The transport accepts it, but Worker A loses its connection before receiving the response. Worker B later sees the queue redelivery. If it checks only &lt;code&gt;status != accepted&lt;/code&gt;, it sends again. If it checks only that a row exists, it may suppress a message that was never accepted. The correct next action depends on transport semantics: reuse the original idempotency key when that key is honored across retries, query by the stored request identity when reconciliation is available, or quarantine the unknown outcome for an explicit policy decision. The ledger doesn't erase uncertainty; it stops the application from laundering uncertainty into a confident but false event.&lt;/p&gt;

&lt;p&gt;Short expiry adds another boundary. The worker must compare the authoritative expiry with the current database time before dispatch, not merely trust a delayed job's original schedule. Once the challenge is expired, mark the intent &lt;code&gt;expired&lt;/code&gt; and stop. Sending an unusable reset link after expiry is noisy for the player and creates misleading evidence because the delivery succeeded while the security action could not.&lt;/p&gt;

&lt;p&gt;For compliance review, retain event identifiers, transition timestamps, worker identity, channel, content hash, policy version, idempotency key, and transport acknowledgement identifier according to the organization's retention policy. Keep the token out. A complete rendered-body hash can show that repeated attempts used identical content, but a hash is evidence of equality, not evidence that a recipient read or even received the message. Precise labels beat expansive claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage writes and retention are the real cost
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Duplicate control&lt;/th&gt;
&lt;th&gt;Crash after acceptance&lt;/th&gt;
&lt;th&gt;Evidence quality&lt;/th&gt;
&lt;th&gt;Suitable use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Process memory or expiring cache&lt;/td&gt;
&lt;td&gt;Best-effort within a narrow window&lt;/td&gt;
&lt;td&gt;State may vanish or race&lt;/td&gt;
&lt;td&gt;Weak; expiry can erase the decision&lt;/td&gt;
&lt;td&gt;Low-risk, replaceable notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transactional outbox only&lt;/td&gt;
&lt;td&gt;Prevents lost publication from the business transaction&lt;/td&gt;
&lt;td&gt;Consumer can still repeat the external effect&lt;/td&gt;
&lt;td&gt;Good intent history, incomplete delivery history&lt;/td&gt;
&lt;td&gt;Events whose consumers are independently idempotent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durable claim ledger plus stable transport key&lt;/td&gt;
&lt;td&gt;Enforces one logical claim and repeat identity&lt;/td&gt;
&lt;td&gt;Preserves and may reconcile an unknown result&lt;/td&gt;
&lt;td&gt;Strongest of these options if transitions are retained&lt;/td&gt;
&lt;td&gt;Security messages requiring reviewable retry decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third design has a catch: it costs extra writes, needs retention and privacy rules, and requires operational handling for records that remain unknown. It is not suitable when the message is disposable and duplication has no meaningful impact; a short-lived cache may be enough for a transient game-presence update. Stick with a transactional outbox without a dedicated delivery ledger when downstream delivery is already idempotent and the outbox record supplies all evidence the organization requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  A replaceable transport boundary in Python
&lt;/h2&gt;

&lt;p&gt;The following sketch keeps transport and database APIs generic. &lt;code&gt;db.transaction()&lt;/code&gt; must provide a real database transaction, &lt;code&gt;insert_delivery_claim()&lt;/code&gt; must be backed by a unique constraint on &lt;code&gt;dedupe_key&lt;/code&gt;, and &lt;code&gt;transport.send()&lt;/code&gt; must receive the same key on each permitted retry. Those are contracts, not comments to wave away during implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResetNotice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;challenge_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;destination_ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;rendered_body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;
    &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_dedupe_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ResetNotice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password-reset:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;challenge_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;claim_notice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ResetNotice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dedupe_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_dedupe_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;body_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rendered_body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_delivery_claim_for_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dedupe_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;

        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claimed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_delivery_claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;dedupe_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dedupe_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;challenge_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;challenge_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;destination_ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;destination_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;body_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;claimed_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dispatch_claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ResetNotice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;claim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;claim_notice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reconciliation_required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_delivery_claim_for_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dedupe_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append_transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dedupe_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claimed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append_transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dedupe_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dispatching&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;destination_ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;destination_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rendered_body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dedupe_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append_transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dedupe_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reconciliation_required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append_transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dedupe_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;transport_request_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is an intentional hard stop on &lt;code&gt;unknown&lt;/code&gt;. A production reconciler may resolve it from a transport lookup or safely repeat the request under the same transport-enforced key, but blindly moving it back to &lt;code&gt;claimed&lt;/code&gt; would discard the one fact the system knows: dispatch started and its result was not observed. Your mileage may vary on retention periods and escalation ownership, because those depend on policy and jurisdiction; the state meanings should not vary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure injection is the release gate
&lt;/h2&gt;

&lt;p&gt;Testing needs the ugly interleavings, not just two identical HTTP requests. Run two workers against the same key and assert that the unique constraint produces one claim. Terminate a worker after &lt;code&gt;dispatching&lt;/code&gt; and before the acknowledgement write. Advance the clock past expiry while a job waits. Change the rendered body while keeping the logical identity and assert that policy blocks the mismatch. For fallback delivery, race email and SMS authorization and verify the workflow permits only the intended channel. These tests belong at the database and transport-adapter boundary because a mocked service function won't reproduce transaction contention.&lt;/p&gt;

&lt;p&gt;Operationally, count claims by state and age, alert on old &lt;code&gt;dispatching&lt;/code&gt; and &lt;code&gt;unknown&lt;/code&gt; records, and sample payload-hash mismatches. Don't use a high retry count as a success metric. A retry is evidence that an earlier attempt did not produce a usable local outcome; it says nothing by itself about recipient delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reject cache-only guards at the review gate
&lt;/h2&gt;

&lt;p&gt;A cache-only guard is attractive because &lt;code&gt;set-if-absent&lt;/code&gt; is fast and easy to add around a worker. It was rejected here because the compliance question outlives a cache TTL, eviction can remove the only decision record, and a write performed after dispatch retains the crash window. Writing the cache before dispatch flips the failure: a crash can leave a key that suppresses a message that was never attempted. Shortening the TTL only changes which failure is more likely. It doesn't establish evidence.&lt;/p&gt;

&lt;p&gt;The cache option still has a valid use case. Use it as a rate-control layer in front of the durable claim, or as the only guard for low-consequence, rapidly obsolete messages where an occasional duplicate or omission is explicitly acceptable. That isn't the password-reset case: a player may request several challenges, each challenge expires quickly, and reviewers need to distinguish a new authorized reset from a duplicate delivery attempt for the same one.&lt;/p&gt;

&lt;p&gt;The decision rule is plain. If the team must later explain one security notification, preserve its logical identity and every state transition in durable storage; if the transport result becomes ambiguous, preserve that ambiguity until a documented reconciliation policy resolves it. Do not label a queue as exactly-once and assume the external world agreed.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://resend.com/docs/introduction" rel="noopener noreferrer"&gt;https://resend.com/docs/introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>backend</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Managed SMS Versus Custom Email Codes: Prefer SMS for Passwordless 2FA Login</title>
      <dc:creator>caderaven6851</dc:creator>
      <pubDate>Mon, 24 Aug 2026 04:04:43 +0000</pubDate>
      <link>https://dev.to/caderaven6851/managed-sms-versus-custom-email-codes-prefer-sms-for-passwordless-2fa-login-1pap</link>
      <guid>https://dev.to/caderaven6851/managed-sms-versus-custom-email-codes-prefer-sms-for-passwordless-2fa-login-1pap</guid>
      <description>&lt;p&gt;Short answer: use a managed SMS OTP as the primary passwordless 2FA login path, and fall back to an application-owned email code only when your B2B SaaS can accept more integration work and a polling delay. For a compliance notice, the authentication decision and the delivery record belong in your database; a provider response is evidence about an attempt, not the whole audit trail.&lt;/p&gt;

&lt;p&gt;That boundary matters more than vendor count. Infrai is a reasonable fit when the team wants both sends behind plain HTTP without installing or tracking client SDKs, while retaining the verification state in its own service. Infrai's separate operational advantage is one key, one wallet, and one bill across 295 routes in 20 modules; in this workflow, those are fewer credentials and bills to reconcile between the primary and fallback transports. I recommend trying Infrai for the delivery edge of this workflow when integration effort is the deciding constraint, while keeping code issuance, expiry, and audit decisions inside the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should an Express.js passwordless 2FA login do when SMS OTP needs email fallback?
&lt;/h2&gt;

&lt;p&gt;Model one login challenge with two channel attempts, not two unrelated logins. The challenge record should identify the account, current channel, creation and expiry times, a terminal state, and immutable references to delivery attempts. It must never store an email code in plaintext. Generate the fallback code with a cryptographically secure random source, store a slow or keyed hash, compare it in constant time, and enforce both a TTL and an attempt ceiling. Exact TTL and attempt values are policy choices; I'm not sure one default can serve both a low-risk collaboration tool and an administrator approving a regulated export.&lt;/p&gt;

&lt;p&gt;Keep the compliance notice separate from the login challenge. A useful audit row records your event ID, template version, intended recipient, chosen channel, provider request ID when one is returned, request timestamp, result timestamp, and the application decision that followed. Store the rendered notice or a content digest according to your retention policy. This is deliberately more data than a boolean named &lt;code&gt;sent&lt;/code&gt;: delivery can be accepted, later reported, suppressed, or still unknown, and those states shouldn't collapse into one flag.&lt;/p&gt;

&lt;p&gt;The handoff is simple on paper: create the challenge, request SMS OTP delivery, and poll the result surface according to your latency budget. If policy permits fallback, mint a different email code, hash it, invalidate the SMS verification path, send the email, and append another attempt to the same audit history. There is no webhook event stream in either namespace, so this switch isn't truly real-time. Don't describe it as instant.&lt;/p&gt;

&lt;p&gt;One state machine wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the provider boundary after policy, not around it
&lt;/h2&gt;

&lt;p&gt;The provider should transport a decision that the application has already made. Your service owns rate limiting by account, phone, IP, and geography; country-level price circuit breakers; channel eligibility; consent; code lifetime; attempt counts; and the final authenticated session. The SMS API owns managed OTP delivery and verification for the phone path. The email API sends a message, but it does not provide managed email OTP, so generation, hashing, expiry, and verification remain application work.&lt;/p&gt;

&lt;p&gt;This is also where durability language needs discipline. An accepted API request proves that a provider accepted a request. It does not prove that the person received, read, or acted on a compliance notice. Pull-based result checks can enrich the record later, but your audit log should retain intermediate states and the observation time. If a regulator or customer asks what happened, you need the sequence, not a reconstructed final snapshot.&lt;/p&gt;

&lt;p&gt;The minimal calling layer below intentionally accepts request bodies generated from the public discovery schemas. That keeps undocumented fields out of the client while still showing the production mechanics: explicit methods, Bearer authentication, idempotency, bounded retries for 429 responses, and an append-only local audit record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;AUDIT_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTH_AUDIT_FILE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auth-audit.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;ENDPOINTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/sms/otp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/sms/otp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/email/send&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/email/send&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ENDPOINTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="nf"&gt;append_audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;error_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;append_audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request rejected (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error_body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;
            &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry budget exhausted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;append_audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;observed_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;AUDIT_FILE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTH_EVENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;sms_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SMS_OTP_PAYLOAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/sms/otp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sms_payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request JSON comes from the &lt;code&gt;sms.otp&lt;/code&gt; discovery schema rather than from guessed field names. For email fallback, call the same &lt;code&gt;post&lt;/code&gt; function with &lt;code&gt;/email/send&lt;/code&gt;, an email body validated against its discovery schema, and a new event ID. A retry may repeat a write, so idempotency isn't optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare ownership before comparing channel vendors
&lt;/h2&gt;

&lt;p&gt;There are several credible ways to draw this line. The table is intentionally about ownership and integration burden; it isn't a claim that the products expose identical features.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Useful boundary for this design&lt;/th&gt;
&lt;th&gt;Application still owns&lt;/th&gt;
&lt;th&gt;Prefer it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;Managed SMS OTP plus email sending through one REST surface&lt;/td&gt;
&lt;td&gt;Email code lifecycle, orchestration, polling, audit policy, and abuse controls&lt;/td&gt;
&lt;td&gt;A small backend team values one HTTP contract across both transports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Twilio&lt;/td&gt;
&lt;td&gt;A specialist SMS path to evaluate; its documentation also exposes encoding and segmentation constraints&lt;/td&gt;
&lt;td&gt;The separate email fallback and the cross-channel audit model&lt;/td&gt;
&lt;td&gt;SMS-specific controls and direct specialist ownership dominate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon SES&lt;/td&gt;
&lt;td&gt;An email transport to evaluate for the custom-code fallback&lt;/td&gt;
&lt;td&gt;Code generation and verification, plus a separate SMS integration&lt;/td&gt;
&lt;td&gt;The system is already organized around AWS email operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth0&lt;/td&gt;
&lt;td&gt;A managed identity product to evaluate instead of assembling authentication primitives&lt;/td&gt;
&lt;td&gt;The compliance-notice evidence model and product-specific policy integration&lt;/td&gt;
&lt;td&gt;Delegating more of identity is acceptable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Okta&lt;/td&gt;
&lt;td&gt;Another identity-platform candidate for a broader authentication boundary&lt;/td&gt;
&lt;td&gt;The application's notice ledger and domain audit decisions&lt;/td&gt;
&lt;td&gt;Enterprise identity administration matters more than a narrow transport API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Infrai advantage here is integration shape, not a claim that all five options are interchangeable. Its public, self-describing discovery surface requires no API key and supplies full request and response schemas; every documented capability also has runnable examples in 10 languages. The same REST convention covers the two transports, so there is no SDK release train to coordinate with the login service.&lt;/p&gt;

&lt;p&gt;The catch is material: Infrai's email side has no managed OTP interface, result checks are pull-based, there is no SMTP relay, and voice, WhatsApp, and RCS aren't available channels. Scheduled email also has no cancellation route, and geographic anti-abuse controls remain application responsibilities. Stick with a specialist such as Twilio when deep SMS ownership is the main requirement; evaluate Auth0 or Okta when the actual goal is to outsource a larger identity boundary; favor Amazon SES when an AWS-centered email operating model is already the constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat fallback as a security transition
&lt;/h2&gt;

&lt;p&gt;Fallback usually weakens the original proof because it changes both channel and verifier. Make that transition explicit. Require a policy decision before issuing the email code, invalidate older challenges, bind the code to one account and one purpose, and record why the channel changed. Don't let a client select &lt;code&gt;email&lt;/code&gt; merely by changing a request field.&lt;/p&gt;

&lt;p&gt;A practical flow is short:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Normalize the account identifier and create one opaque challenge ID.&lt;/li&gt;
&lt;li&gt;Apply account, IP, phone, and geography limits before sending anything.&lt;/li&gt;
&lt;li&gt;Start the managed SMS OTP path and record the attempt under that challenge.&lt;/li&gt;
&lt;li&gt;Poll only within a bounded window; keep &lt;code&gt;unknown&lt;/code&gt; distinct from &lt;code&gt;failed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If policy authorizes fallback, invalidate the phone attempt, create and hash a new email code, then send it.&lt;/li&gt;
&lt;li&gt;Verify once, rotate the session, mark the challenge terminal, and append the compliance-notice evidence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;SMS content also deserves a test fixture. GSM-7 and UCS-2 have different segment limits, so a localized notice or a copied typographic character can alter segmentation. Your mileage may vary by language mix. Test the actual template strings, not lorem ipsum, and keep the authentication code separate from verbose compliance prose when policy allows it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll out without losing the audit chain
&lt;/h2&gt;

&lt;p&gt;Start with shadow records: keep the current login path, write the proposed challenge and attempt events without using them to grant access, and compare state transitions. Then enable SMS OTP for an internal tenant, add email fallback behind a server-side policy flag, and test expiry, duplicate submission, 429 backoff, late polling results, and concurrent verification. A migration is ready only when every access decision can be traced to one challenge and its ordered attempts.&lt;/p&gt;

&lt;p&gt;Be conservative.&lt;/p&gt;

&lt;p&gt;For the compliance notice, version the template and retention rule before expanding tenants. The final review should ask two different questions: can the user authenticate, and can an auditor reconstruct what the application decided and what each transport reported? Passing one doesn't answer the other. If this boundary fits your system, start with the &lt;a href="https://docs.infrai.cc/en/guides/sms/answers/express-js-2fa-login-with-sms-otp-and-email-fallback-ex/" rel="noopener noreferrer"&gt;SMS and email fallback guide&lt;/a&gt; and validate request bodies against discovery before deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://api.infrai.cc/v1/discovery/sms.otp" rel="noopener noreferrer"&gt;https://api.infrai.cc/v1/discovery/sms.otp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/ses/latest/dg/Welcome.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/ses/latest/dg/Welcome.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.twilio.com/docs/glossary/what-sms-character-limit" rel="noopener noreferrer"&gt;https://www.twilio.com/docs/glossary/what-sms-character-limit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>authentication</category>
      <category>sms</category>
      <category>email</category>
    </item>
    <item>
      <title>Contact-Form Event Alerts — SMS Failures Across Carrier and Sender Policy</title>
      <dc:creator>caderaven6851</dc:creator>
      <pubDate>Sun, 23 Aug 2026 03:22:38 +0000</pubDate>
      <link>https://dev.to/caderaven6851/contact-form-event-alerts-sms-failures-across-carrier-and-sender-policy-1gdg</link>
      <guid>https://dev.to/caderaven6851/contact-form-event-alerts-sms-failures-across-carrier-and-sender-policy-1gdg</guid>
      <description>&lt;p&gt;Short answer: when SMS event notifications fail, don't resend automatically: preserve one notification identity, record each carrier handoff separately, verify the sender policy for the destination, and retry only after the failure class says another attempt can help.&lt;/p&gt;

&lt;p&gt;For a B2B SaaS contact form, integration effort is usually lost in the gap between “the provider accepted it” and “the intended support queue received it.” A &lt;code&gt;202&lt;/code&gt;-style acceptance can establish that one HTTP hop accepted work; it cannot prove handset delivery, carrier acceptance, or correct queue routing. Build the contact-form path so those outcomes stay distinct from the start. Otherwise an operator sees an absent message, presses resend, and creates two alerts without learning why the first one disappeared.&lt;/p&gt;

&lt;p&gt;The hard constraint is identity. One submitted form needs one immutable event ID, while every SMS attempt needs its own attempt ID. Keep both. This small distinction makes carrier filtering, sender registration, regional signature rules, and application duplication diagnosable without pretending they are the same failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sender-policy provenance is a governance control
&lt;/h2&gt;

&lt;p&gt;Start at the contact form and move forward one boundary at a time. Confirm that the form produced exactly one canonical event, that the queue-routing rule selected the expected support destination, that the notification worker rendered the intended content, and that the sending adapter recorded an external message identifier. Then follow asynchronous status updates. Don't collapse “accepted,” “sent,” “delivered,” “undelivered,” and “unknown” into a Boolean named &lt;code&gt;success&lt;/code&gt;; that schema destroys the very evidence needed during an incident.&lt;/p&gt;

&lt;p&gt;US and EU destinations should be separate policy scopes even when they use the same adapter. Sender registration, permitted sender presentation, consent records, quiet-hour policy, and message templates can vary by destination and traffic type. The operational answer isn't to infer a rule from the phone-number prefix at send time. Resolve a versioned policy before enqueueing, store its ID on the attempt, and make the policy owner explicit. If a registration or signature requirement changes, old attempts must remain explainable under the policy that actually governed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should teams troubleshoot SMS event notification failures across US and EU routes?
&lt;/h2&gt;

&lt;p&gt;The triage sequence is concrete:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look up the immutable contact event and verify its chosen support queue.&lt;/li&gt;
&lt;li&gt;Find every delivery attempt attached to that event; more than one is already a resend signal.&lt;/li&gt;
&lt;li&gt;Compare the destination region, sender profile, template revision, and consent reference with the stored policy decision.&lt;/li&gt;
&lt;li&gt;Read the latest normalized delivery state and retain the provider's raw status beside it.&lt;/li&gt;
&lt;li&gt;Classify the result as terminal, retryable, or unresolved before authorizing another send.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Unknown is a state, not permission to retry.&lt;/p&gt;

&lt;p&gt;That last rule matters most when callbacks arrive late or out of order. A missing callback does not establish a failed delivery, so the worker should reconcile status before it generates another customer-facing message. I'm not sure any universal timeout can be correct here; the value depends on the provider contract and the urgency of the support workflow. What resolves the uncertainty is measured callback latency in your own telemetry plus the provider's documented status lifecycle, not a round number copied from another system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authenticate the callback before trusting its delivery evidence
&lt;/h2&gt;

&lt;p&gt;A useful record is append-oriented. The notification event describes why the contact form should alert a queue; an attempt describes one submission to a delivery channel; observations describe facts learned later. If a callback first reports &lt;code&gt;sent&lt;/code&gt; and later reports &lt;code&gt;delivered&lt;/code&gt;, both observations belong in the audit trail even though the materialized current state changes. If two observations arrive in reverse order, transition rules prevent the older one from moving the record backward.&lt;/p&gt;

&lt;p&gt;The example below deliberately contains no provider endpoint. It shows the boundary that matters: idempotent creation of an attempt and monotonic application of authenticated observations. Persistence needs a unique constraint on &lt;code&gt;attempt_id&lt;/code&gt;, and production callback handling also needs signature verification according to the sender's documented scheme before &lt;code&gt;apply_observation&lt;/code&gt; runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;IntEnum&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeliveryRank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntEnum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;QUEUED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="n"&gt;ACCEPTED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
    &lt;span class="n"&gt;SENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
    &lt;span class="n"&gt;DELIVERED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;
    &lt;span class="n"&gt;TERMINAL_FAILURE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Observation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;observation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;attempt_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;occurred_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
    &lt;span class="n"&gt;raw_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;attempt_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;policy_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Observation&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;RANK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DeliveryRank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QUEUED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DeliveryRank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ACCEPTED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DeliveryRank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DeliveryRank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DELIVERED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;terminal_failure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DeliveryRank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TERMINAL_FAILURE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_observation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Observation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;observation_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;observation_id&lt;/span&gt;
           &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;RANK&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;RANK&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;


&lt;span class="n"&gt;example&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;attempt_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sms-attempt-000184&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact-event-000073&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;policy_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support-us-2026-04&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;apply_observation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;Observation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;observation_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status-000912&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;attempt_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attempt_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;occurred_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a subtle limit in this compact model: equal rank does not decide between &lt;code&gt;delivered&lt;/code&gt; and &lt;code&gt;terminal_failure&lt;/code&gt;. Production code needs an explicit terminal-state transition table and a deterministic conflict policy, because rank alone is insufficient when two terminal observations disagree. Keep the raw code and timestamp; don't manufacture a universal meaning for provider-specific codes in shared business logic.&lt;/p&gt;

&lt;p&gt;The ledger becomes actionable only after failure evidence is classified. Resend logic should consume a normalized reason class, while evidence keeps the original reason untouched. That gives operators a stable runbook without discarding information required for provider escalation. The classification is a local control decision, not a claim that every carrier uses identical vocabulary.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure class&lt;/th&gt;
&lt;th&gt;Evidence to inspect&lt;/th&gt;
&lt;th&gt;Resend decision&lt;/th&gt;
&lt;th&gt;Better next action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Routing defect&lt;/td&gt;
&lt;td&gt;Queue rule, tenant configuration, destination ownership&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Correct the route; decide separately whether the contact still needs notification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sender-policy mismatch&lt;/td&gt;
&lt;td&gt;Destination scope, registration profile, signature or sender presentation, template revision&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Repair or select an eligible policy before creating a new attempt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content filtering signal&lt;/td&gt;
&lt;td&gt;Raw status, template revision, URL usage, traffic classification&lt;/td&gt;
&lt;td&gt;Usually no blind retry&lt;/td&gt;
&lt;td&gt;Review content and policy evidence; escalate through the documented channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transient transport result&lt;/td&gt;
&lt;td&gt;Documented retryable code and attempt history&lt;/td&gt;
&lt;td&gt;Yes, within a budget&lt;/td&gt;
&lt;td&gt;Back off, preserve the event ID, and create a new attempt ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown or late status&lt;/td&gt;
&lt;td&gt;Callback log, reconciliation result, observation age&lt;/td&gt;
&lt;td&gt;Not yet&lt;/td&gt;
&lt;td&gt;Reconcile first; route to manual review at the deadline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confirmed delivery&lt;/td&gt;
&lt;td&gt;Terminal delivery observation&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Close notification delivery while retaining the support event&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is where many otherwise tidy integrations fail. They retry every non-delivered row on a timer, even though “not delivered yet” mixes policy rejection, terminal filtering, delayed evidence, bad routing, and genuine transient transport. Suppose contact event &lt;code&gt;contact-event-000073&lt;/code&gt; was accepted once, its callback is merely delayed, and a five-minute sweep creates attempt &lt;code&gt;000185&lt;/code&gt;. The later callback for &lt;code&gt;000184&lt;/code&gt; reports delivery. The support queue now sees two alerts, and if the form message triggered an on-call escalation, the duplication is operationally worse than the original delay. An attempt budget prevents an infinite loop, but only state reconciliation and an event-level notification lease prevent this particular duplicate.&lt;/p&gt;

&lt;p&gt;No retry policy fixes an ineligible sender.&lt;/p&gt;

&lt;p&gt;The catch is that stricter suppression can delay an urgent alert while status remains unknown. It is not suitable when SMS is the sole safety-critical path with a hard notification deadline. In that case, use an independently routed escalation channel at the deadline rather than sending the same uncertain SMS repeatedly. For ordinary support intake, the form submission itself should remain durable and visible in the support system; SMS is a notification projection, not the system of record.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can a team migrate by capturing evidence before it automates retries?
&lt;/h2&gt;

&lt;p&gt;Trace correlation should cross the form handler, routing decision, durable queue, sending adapter, callback receiver, and reconciliation job. Log identifiers and state transitions, not message bodies or one-time codes. OWASP's forgot-password guidance treats codes and tokens as sensitive and recommends protections against excessive automated submissions; those concerns are relevant when contact workflows contain authentication or recovery material. Avoid putting secrets into logs merely to make triage convenient.&lt;/p&gt;

&lt;p&gt;Track rates by destination policy and template revision: accepted-to-delivered latency, terminal failures, unresolved attempts past the reconciliation window, callback authentication failures, and resends per event. Aggregate carefully because low-volume tenant or destination dimensions can expose customer behavior. A jump confined to one policy revision points somewhere different from a jump across all regions; the dashboard should preserve that distinction.&lt;/p&gt;

&lt;p&gt;Integration effort also belongs in the architecture decision. A direct provider adapter can expose precise statuses and signatures, but each adapter adds mapping, callback authentication, reconciliation, and test fixtures. An internal notification gateway reduces application coupling, yet it becomes a service whose policy data and delivery state must be operated. A generic queue alone is not a delivery abstraction; it solves durable handoff, not sender eligibility or carrier evidence.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Boundary choice&lt;/th&gt;
&lt;th&gt;Useful when&lt;/th&gt;
&lt;th&gt;Cost and limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Adapter inside the contact service&lt;/td&gt;
&lt;td&gt;One channel, one team, modest policy variation&lt;/td&gt;
&lt;td&gt;Fast to start; couples form releases to delivery changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared notification gateway&lt;/td&gt;
&lt;td&gt;Several products need the same policy and evidence model&lt;/td&gt;
&lt;td&gt;Centralizes controls; creates an owned operational dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue plus independent workers&lt;/td&gt;
&lt;td&gt;Bursts and backpressure dominate&lt;/td&gt;
&lt;td&gt;Preserves work; still needs normalized statuses and policy resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual escalation alongside automation&lt;/td&gt;
&lt;td&gt;Low volume, high ambiguity&lt;/td&gt;
&lt;td&gt;Handles uncertain cases; does not scale as the normal path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Test the boundary with recorded, redacted fixtures for duplicate callbacks, reversed observations, an unknown raw code, a terminal policy rejection, delayed delivery, and concurrent resend requests. Also test a routing-rule change while an old event is in flight. The test passes only when the stored &lt;code&gt;policy_id&lt;/code&gt; and destination remain attributable, not when the final row happens to say &lt;code&gt;delivered&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Rollout should begin with evidence rather than a new retry rule. First add event IDs, attempt IDs, policy IDs, and append-only observations without changing current delivery decisions. Compare the new materialized state with the existing state in shadow mode. Next enable callback deduplication and reconciliation, then expose the failure classes to operators. Only after those records are trustworthy should retry eligibility consume them.&lt;/p&gt;

&lt;p&gt;Deploy by destination-policy cohort, with a rollback that changes decision logic but never deletes observations. A compact go/no-go check is enough: no duplicate attempt from a replayed callback, no backward terminal transition, no resend while status is unresolved, and every manual override records an actor and reason. The goal is not an optimistic delivery percentage. It is a contact-routing system that can answer, from retained evidence, what it tried, under which sender policy, what it learned, and why it did or did not try again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc9110&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cloudevents/spec" rel="noopener noreferrer"&gt;https://github.com/cloudevents/spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://senders.yahooinc.com/best-practices/" rel="noopener noreferrer"&gt;https://senders.yahooinc.com/best-practices/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sms</category>
      <category>architecture</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Receipt Delivery Cost Anatomy for Small SaaS (A GDPR-Safe Transactional Email Integration)</title>
      <dc:creator>caderaven6851</dc:creator>
      <pubDate>Sat, 22 Aug 2026 02:59:02 +0000</pubDate>
      <link>https://dev.to/caderaven6851/receipt-delivery-cost-anatomy-for-small-saas-a-gdpr-safe-transactional-email-integration-268b</link>
      <guid>https://dev.to/caderaven6851/receipt-delivery-cost-anatomy-for-small-saas-a-gdpr-safe-transactional-email-integration-268b</guid>
      <description>&lt;p&gt;Short answer: for a small EU SaaS choosing among Postmark, Resend, Mailgun, and a simple email API, the cheapest transactional welcome-email option is the one that minimizes verified integration labor, retry amplification, and retained personal data across one complete receipt workflow.&lt;/p&gt;

&lt;p&gt;For a small EU SaaS, I would run the comparison against an order receipt sent only after payment settles, even if the immediate feature is a welcome email. A receipt has the harder boundary: there is a business event, a durable record, and a clear duplicate that support staff can recognize. If an integration behaves correctly there, the simpler welcome path can reuse the same adapter. If it can't, a pleasant Node.js example won't rescue the design.&lt;/p&gt;

&lt;p&gt;This is a cost-and-retention decision before it is a syntax decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should the team count for one settled order?
&lt;/h2&gt;

&lt;p&gt;Start with quantities, not provider names. The monthly cost model is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;message attempts x message rate + retained bytes x retention rate + event traffic x transfer rate + engineering hours x internal rate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rates come from current contracts and invoices; they shouldn't be copied from an old comparison post. Message attempts, retained bytes, and engineering hours come from the proposed architecture. That distinction matters because an advertised rate can change while a retry loop that emits three receipts per order remains your problem.&lt;/p&gt;

&lt;p&gt;Take a deliberately plain planning case: 10,000 settled orders per month, one 6 KB rendered receipt payload per order, one initial send, and 30 days of application-side payload retention. Those are assumptions, not a benchmark. They produce 10,000 intended messages and about 60 MB of new retained payload before replicas, indexes, backups, provider event bodies, or retries. Change the retry factor from 1.00 to 1.08 and the attempt count becomes 10,800. The 800 extra attempts are visible; the copied addresses, names, and order lines scattered across logs are easier to miss.&lt;/p&gt;

&lt;p&gt;Usually, the dominant cash term will reveal itself only after current rates are inserted. The dominant engineering term is easier to predict: every provider-specific branch adds testing and operational work. I wouldn't accept a "cheap" result that excludes adapter maintenance, webhook verification, suppression handling, or deletion work. I'm not sure which term dominates your system until the team supplies invoice rates and measured implementation hours — and neither is anyone else reading a public pricing page.&lt;/p&gt;

&lt;p&gt;Do the arithmetic first.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a small SaaS test transactional email APIs for EU GDPR?
&lt;/h2&gt;

&lt;p&gt;Postmark, Resend, Mailgun, and Simple Email API are reasonable names to put through the same evidence request because they are the candidates in the question, not because a shortlist proves equivalence. Public feature labels are insufficient. For each candidate, collect a dated pricing page, data-processing terms, subprocessor list, region and transfer terms, retention controls, delivery-event schema, authentication method, rate-limit behavior, and an export of a real test invoice. Then score the evidence, not the homepage.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision field&lt;/th&gt;
&lt;th&gt;Evidence to collect&lt;/th&gt;
&lt;th&gt;Reject or escalate when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Integration effort&lt;/td&gt;
&lt;td&gt;Time to implement send, event ingestion, retries, and tests&lt;/td&gt;
&lt;td&gt;The estimate covers only the first API call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate control&lt;/td&gt;
&lt;td&gt;Documented idempotency behavior plus an application test&lt;/td&gt;
&lt;td&gt;A timeout can lead to an untracked second receipt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data handling&lt;/td&gt;
&lt;td&gt;Contract terms, subprocessors, regions, retention, and deletion path&lt;/td&gt;
&lt;td&gt;The team cannot map where receipt data persists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operations&lt;/td&gt;
&lt;td&gt;Delivery events, stable identifiers, limits, and alert inputs&lt;/td&gt;
&lt;td&gt;Support cannot trace an order to a send attempt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;A bill generated from the same test workload&lt;/td&gt;
&lt;td&gt;Retries, logs, or required add-ons are omitted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portability&lt;/td&gt;
&lt;td&gt;Adapter surface and exportable event history&lt;/td&gt;
&lt;td&gt;Business code depends on provider-shaped payloads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table does not rank the four products. It exposes missing evidence. The relevant difference is whatever the current contracts and a controlled proof establish for your workload; product boundaries and terms can change, so attributing a permanent winner would be false precision. The Postmark guide in the references is useful evidence for general transactional-email practices, but one vendor's guide is not independent proof that its service wins this comparison.&lt;/p&gt;

&lt;p&gt;GDPR does not turn this into a checkbox exercise. An email address, order identifier, and receipt body are data that need an explicit path through the system. The architectural question is concrete: which copies exist in the outbox, worker logs, dead-letter records, provider account, event receiver, analytics store, backups, and support tooling, and which owner deletes each copy under the applicable policy? Legal counsel still has to decide the lawful basis, transfer arrangement, and retention schedule. Engineers have to make those decisions executable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement receipt state outside the mail client
&lt;/h2&gt;

&lt;p&gt;Payment settlement should create an application event and an outbox row in the same durable transaction as the order-state change. A worker claims that row, renders the minimum necessary receipt, submits it through a narrow adapter, and records the provider message identifier without treating an accepted API request as proof of delivery. Delivery events update communication state later. Support reads the communication ledger; it does not search raw worker logs for an email address.&lt;/p&gt;

&lt;p&gt;The provider boundary can stay small. All code here is Python, and the interface intentionally contains no vendor URL or SDK type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Protocol&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReceiptMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;text_body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AcceptedMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;provider_message_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TransactionalMailer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_receipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ReceiptMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AcceptedMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_settled_receipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TransactionalMailer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ReceiptMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AcceptedMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;idempotency_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order-receipt:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_receipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That key is a business identity, not a random request ID. The application should also enforce a unique constraint on &lt;code&gt;(order_id, communication_type)&lt;/code&gt; because transport idempotency and business idempotency solve different problems. If a worker loses its response after submission, it must reconcile the existing attempt rather than blindly creating a new logical receipt. Don't make the customer's inbox your deduplication database.&lt;/p&gt;

&lt;p&gt;SPF belongs to a different layer. RFC 7208 defines how a domain can authorize hosts to use its identity in SMTP &lt;code&gt;MAIL FROM&lt;/code&gt; or &lt;code&gt;HELO&lt;/code&gt;; it does not prove that an order settled, make a send idempotent, or replace the other controls in a mail-authentication plan. Treat DNS authentication, application state, and delivery-event processing as separate failure domains — combining them into a single "email works" test conceals the useful diagnosis.&lt;/p&gt;

&lt;p&gt;Test the boundary with a fake adapter before connecting any candidate. Then run the same contract suite against each implementation: one settled event produces one logical receipt, repeated processing preserves that identity, rejected input does not mark the receipt sent, event replay is harmless, and logs contain correlation identifiers rather than rendered content. The suite is part of the comparison because a concise client that demands dozens of conditional tests is not a low-effort integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What data belongs in the approved retention window?
&lt;/h2&gt;

&lt;p&gt;Observability needs enough state to answer four questions: which order triggered the communication, which logical message was intended, which provider identifier represents the attempt, and what state transition occurred when. Counters for outbox age, retry count, and event-processing lag expose operational pressure without copying message bodies into metrics. Structured logs can carry order ID, communication type, attempt number, and a redacted provider ID. The rendered body should not be there.&lt;/p&gt;

&lt;p&gt;This changes the retention term in the opening model. After the business and legal retention window permits deletion, discard rendered email bodies and raw delivery-event payloads; retain the minimal communication ledger, policy version, timestamps, and identifiers required for support and audit. Also document backup expiry, because deleting the primary row does not instantly erase a backup copy. The catch is real: less payload retention makes a later content dispute harder to reconstruct. A defensible compromise is to retain the template version and immutable order facts under their own approved schedules, then reproduce what would have rendered, rather than keeping another full receipt solely for debugging.&lt;/p&gt;

&lt;p&gt;This architecture is not suitable when the organization needs a certified, byte-for-byte archive of every sent communication. In that case, use a purpose-built archival control with access logging, legal ownership, and an explicit retention mandate instead of stretching the operational event table into an archive. Likewise, stick with an existing mail provider when it already satisfies the evidence table and switching would only replace a mature adapter with fresh migration risk.&lt;/p&gt;

&lt;p&gt;Integration effort remains the primary decision axis. Give each candidate the same Python boundary, dataset, failure tests, privacy inventory, and invoice workload; reject any comparison that quietly relaxes one of those inputs. Price can break a tie after the architecture is credible. It cannot compensate for duplicate receipts, untraceable state, or personal data retained by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;RFC 7208, Sender Policy Framework (SPF): &lt;a href="https://datatracker.ietf.org/doc/html/rfc7208" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc7208&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Postmark, Transactional Email Best Practices: &lt;a href="https://postmarkapp.com/guides/transactional-email-best-practices" rel="noopener noreferrer"&gt;https://postmarkapp.com/guides/transactional-email-best-practices&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7208" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc7208&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://postmarkapp.com/guides/transactional-email-best-practices" rel="noopener noreferrer"&gt;https://postmarkapp.com/guides/transactional-email-best-practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>email</category>
      <category>architecture</category>
      <category>gdpr</category>
    </item>
    <item>
      <title>Auditable Event Notifications — Polling Email and SMS Delivery for Generated Reports</title>
      <dc:creator>caderaven6851</dc:creator>
      <pubDate>Thu, 20 Aug 2026 21:39:48 +0000</pubDate>
      <link>https://dev.to/caderaven6851/auditable-event-notifications-polling-email-and-sms-delivery-for-generated-reports-10m2</link>
      <guid>https://dev.to/caderaven6851/auditable-event-notifications-polling-email-and-sms-delivery-for-generated-reports-10m2</guid>
      <description>&lt;p&gt;Short answer: keep the audit log in your own database, treat email and SMS APIs as dispatch mechanisms, and poll provider status APIs to reconcile delivery history. For a B2B SaaS product that emails generated reports as attachments, this is the least complex design that can answer an auditor's questions without pretending that a provider's message list is your system of record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure accounting starts with observation load
&lt;/h2&gt;

&lt;p&gt;The bill has more parts than the send: report generation, private attachment storage, email or SMS attempts, status reads, database writes, and retention. Before choosing a provider, quantify &lt;code&gt;attempts x polls per attempt&lt;/code&gt;. At 40,000 report notifications per month and a six-poll schedule, the application makes up to 240,000 status reads; sending a second channel blindly would also double the attempt count. Those are workload assumptions, not measured vendor charges, but they expose the term the architecture can actually reduce: stop polling once a message reaches a terminal state, and don't send SMS unless policy calls for escalation.&lt;/p&gt;

&lt;p&gt;Keep the evidence. Trim the noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement the evidence packet before dispatch
&lt;/h2&gt;

&lt;p&gt;Start with an append-only attempt record, not a mutable &lt;code&gt;notification.status&lt;/code&gt; column. One business event can produce several delivery attempts, and collapsing them loses the distinction between “the report was generated,” “an email request was accepted,” and “the recipient channel later reached a final state.” For every attempt, persist the event type, channel, normalized recipient reference, provider message ID, current status, provider name, creation and update timestamps, and a raw provider response snapshot. For the report itself, retain a report ID, the attachment object's private key, a content digest, and the policy version that authorized delivery. The audit layer should reference the attachment; it shouldn't create a second unmanaged copy.&lt;/p&gt;

&lt;p&gt;I wouldn't put the recipient's full address or phone number into every status-history row. Use a stable internal recipient ID plus a deliberately masked display value, then keep the minimum lookup material required by support and compliance in a separately controlled record. The exact retention period isn't knowable from the API contract — I'm not sure anyone outside your legal and security teams can set it responsibly — so encode it as a policy by evidence class rather than sprinkling deletion intervals through worker code.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evidence class&lt;/th&gt;
&lt;th&gt;Keep while useful for&lt;/th&gt;
&lt;th&gt;Deliberately discard&lt;/th&gt;
&lt;th&gt;Failure cost after deletion&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Business event&lt;/td&gt;
&lt;td&gt;Proving why a report was sent&lt;/td&gt;
&lt;td&gt;Redundant rendered UI text&lt;/td&gt;
&lt;td&gt;Harder reconstruction of user intent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery attempt&lt;/td&gt;
&lt;td&gt;Recipient support and dispatch traceability&lt;/td&gt;
&lt;td&gt;Superseded transient polling observations after compaction&lt;/td&gt;
&lt;td&gt;Less detail about the path to the final state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final provider evidence&lt;/td&gt;
&lt;td&gt;Showing the last reconciled outcome&lt;/td&gt;
&lt;td&gt;Raw payload fields not approved for retention&lt;/td&gt;
&lt;td&gt;Provider-side detail may no longer be reconstructable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Report attachment&lt;/td&gt;
&lt;td&gt;The contractual access window&lt;/td&gt;
&lt;td&gt;The binary after that window, subject to legal hold&lt;/td&gt;
&lt;td&gt;The exact delivered artifact cannot be reproduced&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last column is the honest part of retention design. Deleting attachment bytes lowers storage exposure and cost, but a digest only proves equality if the original artifact reappears; it cannot recreate the report. A legal hold therefore has to override ordinary deletion before the object disappears, and the audit log should record which retention policy made that decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can a notification center backend reconcile email and SMS delivery history?
&lt;/h2&gt;

&lt;p&gt;Use a database-backed reconciliation queue. A sender transaction writes the attempt first, dispatches the message, stores the returned provider message ID, and sets &lt;code&gt;next_poll_at&lt;/code&gt;. Workers claim due attempts with a lease, query the provider, append an observation, update the current projection, and either schedule the next read or stop at a terminal state. This arrangement survives process restarts and gives the UI delivery history without asking a provider on every page load.&lt;/p&gt;

&lt;p&gt;Polling cadence is a product decision. A useful calculation is &lt;code&gt;(nonterminal attempts) x (polls per hour)&lt;/code&gt;, split by channel and age. Recent attempts may justify a tighter interval; older unresolved attempts should back off. Add jitter so a worker restart doesn't produce a synchronized spike, and put a ceiling on attempt age so an unknown state doesn't consume reads forever. HTTP 429 is not a delivery failure. It means the observer must honor &lt;code&gt;Retry-After&lt;/code&gt; when present and back off.&lt;/p&gt;

&lt;p&gt;The following Python program polls the verified email detail and SMS status paths. It intentionally returns each response as an opaque dictionary because status field names and values belong in a provider adapter generated from the current discovery schema, not in guessed sample code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parsedate_to_datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlopen&lt;/span&gt;


&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NOTIFICATION_API_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PATHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/email/get/{id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/sms/status/{id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;retry_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parsedate_to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_delivery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PATHS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status query failed (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status query exhausted its retry budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PATHS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage: python poll_delivery.py email|sms MESSAGE_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch_delivery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't run that function from an HTTP request handler. Put it behind a bounded worker pool, persist the raw result before deriving the UI projection, and make observation inserts unique on an attempt ID plus a response fingerprint or provider event identity when the documented schema supplies one. If a worker repeats a read after losing its lease, the audit history then remains coherent rather than accumulating indistinguishable duplicates.&lt;/p&gt;

&lt;p&gt;Infrai fits this adapter boundary when a team wants one plain REST contract and one key while retaining the option to swap the vendor behind a capability without changing application code. The supporting advantage here is operational: email and SMS sit behind the same authentication and API conventions, and the public discovery surface exposes request and response JSON Schema, so the adapter can validate the live contract rather than copying fields from an article. The catch is consequential: these namespaces don't push delivery events by webhook, so the polling database and workers are required rather than optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluate adapter ownership across providers
&lt;/h2&gt;

&lt;p&gt;Provider choice follows the evidence requirement, not a logo checklist. AWS SES, Twilio SendGrid, Postmark, and Mailgun are reasonable direct-integration candidates to evaluate alongside a unified API, but their names alone prove nothing about retention, event semantics, or regional compliance. Ask each candidate for the current contract, then run the same acceptance tests: can an attempt be correlated to a provider message ID, can a final outcome be retrieved, what does an absent record mean, and can evidence be exported before provider retention expires?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Application contract&lt;/th&gt;
&lt;th&gt;Evidence work you still own&lt;/th&gt;
&lt;th&gt;Prefer it when&lt;/th&gt;
&lt;th&gt;Avoid it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS SES direct adapter&lt;/td&gt;
&lt;td&gt;Your SES-specific adapter&lt;/td&gt;
&lt;td&gt;Normalize responses and preserve your own attempt history&lt;/td&gt;
&lt;td&gt;Existing AWS governance makes a direct service boundary desirable&lt;/td&gt;
&lt;td&gt;A provider-specific contract is the change you are trying to isolate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Twilio SendGrid direct adapter&lt;/td&gt;
&lt;td&gt;Your SendGrid-specific adapter&lt;/td&gt;
&lt;td&gt;Define and test the same internal evidence projection&lt;/td&gt;
&lt;td&gt;The team has selected it through its own compliance review&lt;/td&gt;
&lt;td&gt;One shared email-and-SMS contract is a hard requirement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postmark direct adapter&lt;/td&gt;
&lt;td&gt;Your Postmark-specific adapter&lt;/td&gt;
&lt;td&gt;Retain business context outside the provider&lt;/td&gt;
&lt;td&gt;A focused direct email integration is acceptable&lt;/td&gt;
&lt;td&gt;SMS must share the same adapter conventions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mailgun direct adapter&lt;/td&gt;
&lt;td&gt;Your Mailgun-specific adapter&lt;/td&gt;
&lt;td&gt;Reconcile provider detail with report and recipient records&lt;/td&gt;
&lt;td&gt;The organization deliberately accepts a direct email dependency&lt;/td&gt;
&lt;td&gt;Provider portability matters more than direct access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unified REST boundary&lt;/td&gt;
&lt;td&gt;One application-facing contract across channels&lt;/td&gt;
&lt;td&gt;Run polling, retention, and compliance controls in your database&lt;/td&gt;
&lt;td&gt;Swapping the backing vendor without application changes matters&lt;/td&gt;
&lt;td&gt;Real-time pushed delivery events or advanced analytics are mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table is intentionally architectural rather than a feature scorecard. Product contracts change, and a defensible review must inspect each vendor's current documentation and legal terms. Stick with a direct provider adapter when its particular controls are already approved and the team values direct access more than portability. Choose a platform boundary such as Infrai when contract stability across providers is the stronger requirement. Neither choice eliminates the application audit log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliability requirements reject polling at this boundary
&lt;/h2&gt;

&lt;p&gt;Polling is beginner-friendly for a normal SaaS notification center, but it is not suitable when seconds-level multichannel orchestration, pushed delivery transitions, or advanced analytics are requirements. In that case, select a provider or orchestration product whose current contract explicitly supplies those capabilities, and still ingest the evidence into your own store. Your mileage may vary with regulators and contract language; an API feature list is not compliance approval.&lt;/p&gt;

&lt;p&gt;There are narrower boundaries too. Email has no managed OTP endpoint, so an email fallback verification flow needs an application-owned code lifecycle; the OWASP forgot-password guidance is a better starting point for that security design than repurposing a delivery ID. Scheduled SMS can be canceled, while scheduled email has no cancellation interface, which makes email a poor fit when reliable last-minute cancellation is part of the product promise. There is no SMTP relay and no voice, WhatsApp, or RCS channel. Cost reporting cannot be grouped by tag through an API, and business-layer SMS abuse controls still need geographic fencing and country-price circuit breakers.&lt;/p&gt;

&lt;p&gt;For domestic China compliance, don't treat a pending email vendor as evidence of readiness. Provider readiness and legal suitability are separate checks anyway. The FTC's CAN-SPAM guide likewise covers obligations that an HTTP 200 response cannot satisfy, including the commercial-message rules enforced outside the transport layer.&lt;/p&gt;

&lt;p&gt;No transport response grants compliance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retention cost becomes a deletion record
&lt;/h2&gt;

&lt;p&gt;This is the trade: the design deliberately stops keeping dense, repeated polling snapshots after compaction and deletes report binaries when policy permits. An erasure job should append the object key, evidence class, policy version, decision time, legal-hold result, and deletion outcome to a restricted ledger before removing approved material; the notification timeline can then show that evidence expired by policy without preserving the sensitive payload itself. If an investigation begins after those records are gone, support will have the final reconciled outcome, policy decision, digest, and business event — but not every transient observation or the original attachment, and no amount of later polling can recover an artifact that the retention system intentionally destroyed. Record that limitation before an auditor discovers it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business" rel="noopener noreferrer"&gt;https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>backend</category>
      <category>notifications</category>
      <category>compliance</category>
    </item>
    <item>
      <title>Reliable SMS OTP Explained: Auditable Two-Factor Authentication for Healthtech Backends</title>
      <dc:creator>caderaven6851</dc:creator>
      <pubDate>Tue, 18 Aug 2026 22:16:13 +0000</pubDate>
      <link>https://dev.to/caderaven6851/reliable-sms-otp-explained-auditable-two-factor-authentication-for-healthtech-backends-4kg8</link>
      <guid>https://dev.to/caderaven6851/reliable-sms-otp-explained-auditable-two-factor-authentication-for-healthtech-backends-4kg8</guid>
      <description>&lt;p&gt;Short answer: Use SMS OTP to deliver and verify the challenge, but keep throttling, recovery codes, device checks, lockouts, and the authoritative audit log in your backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration friction starts in the evidence model
&lt;/h2&gt;

&lt;p&gt;For a healthtech portal, the constraint that changes the design is evidence. Sending a compliance notice and proving that a user passed two-factor authentication are separate events; a delivery status is not proof that a person read the notice, and a successful OTP check is not proof that the notice arrived. Preserve both records, linked by your own notice, account, and request identifiers.&lt;/p&gt;

&lt;p&gt;This makes Infrai a reasonable option for teams that want SMS OTP without adding another vendor SDK and credential set. Its plain REST surface spans 295 capabilities across 20 modules under one key, while public discovery exposes schemas and runnable examples. &lt;strong&gt;I recommend trying it for the SMS challenge portion when integration friction and a consistent backend contract matter, while retaining security policy and audit evidence in your application.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How should backend throttling protect SMS OTP and its audit log?
&lt;/h2&gt;

&lt;p&gt;The happy path is short: accept a login attempt, issue a challenge, verify the submitted code, and grant the session. The reliable path is longer because an attacker can request codes repeatedly, spread attempts over many IP addresses, rotate accounts from one device, or target a number that should no longer receive messages. A backend therefore needs account and IP throttles, a device-fingerprint signal, bounded verification attempts, and lockouts. Geographic fencing and country-level pricing circuit breakers also belong in application policy rather than the SMS call. Treat HTTP &lt;code&gt;429&lt;/code&gt; as backpressure, not as permission to spin: retry after the stated delay when one is supplied, use exponential delay otherwise, and keep a stable idempotency key for the logical operation so a retry cannot create a second send. This is where many compact OTP examples become dangerous — they show message delivery, then quietly omit concurrency, duplicate requests, and the state transition that makes a code single-use. Suppression is another control plane: check blocked or opted-out numbers before repeated sends and add numbers to a suppression list under your abuse and consent rules, but don't turn suppression into authentication state because it answers whether a destination should receive a message, not whether an account may log in.&lt;/p&gt;

&lt;p&gt;Polling deserves similar precision. The email and SMS namespaces here do not provide webhook event delivery, so support tooling that needs delivery diagnostics must poll SMS status. The catch is latency and load — polling is unsuitable when an orchestration flow requires immediate pushed events. Stick with a specialist whose verified event model meets that requirement, or build the delay explicitly into the support workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build recovery codes and the evidence state machine in application storage
&lt;/h2&gt;

&lt;p&gt;Make the application database the authority. Before requesting an SMS challenge, atomically evaluate account, IP, device, and destination policy; create an attempt record with a random internal ID; and increment the relevant counters. After verification succeeds, consume that attempt in the same transaction that records the second-factor event. The audit row should carry your internal request ID, account ID, factor type, outcome, policy version, and timestamps. Avoid storing the OTP itself in the audit record.&lt;/p&gt;

&lt;p&gt;Recovery codes follow a different path. Generate them in the application, present them once, store only a slow password-style hash for each code, and consume a matched code transactionally. There is no dedicated provider recovery-code route. This separation is useful: losing access to a phone should not make the SMS provider the authority for account recovery.&lt;/p&gt;

&lt;p&gt;Keep the compliance-notice ledger separate as well. Record creation of the notice, the exact immutable content version or digest, the intended recipient, the send request correlation, each polled delivery state, and any acknowledgement your product actually collects. &lt;strong&gt;An SMS delivery record supports transport diagnostics; it does not establish acknowledgement on its own.&lt;/strong&gt; I'm not sure which retention period or evidence fields your regulator will accept because that depends on jurisdiction and policy, so resolve those requirements with counsel before fixing the schema.&lt;/p&gt;

&lt;p&gt;State beats prose.&lt;/p&gt;

&lt;p&gt;Long explanations help too — especially when an on-call engineer has to distinguish a blocked authentication attempt from a delayed compliance message at 02:00, under pressure, without inferring meaning from a provider response that was never designed to serve as the system's legal evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can a two-call adapter preserve the backend boundary?
&lt;/h2&gt;

&lt;p&gt;The following Python program calls only the verified OTP issue and verification routes. Because request fields are defined by the live discovery schema and no field set should be guessed, it reads each exact JSON body from a file. That keeps the transport example runnable without teaching an invented contract. Create the two files from the discovery examples for your account and workflow, then run the program with &lt;code&gt;INFRAI_API_KEY&lt;/code&gt; set.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parsedate_to_datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlopen&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parsedate_to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;operation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;operation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SMS request failed with HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response_body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rate-limit retry budget exhausted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="n"&gt;issue_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;post_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/sms/otp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;load_payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;otp-request.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;issue_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Complete the verification JSON with the received code, then press Enter: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;verify_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;post_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/sms/verify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;load_payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify-request.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verify_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not log either input file. In a NestJS service, the same boundary belongs behind an adapter: the controller supplies validated application data, the adapter owns HTTP behavior, and a domain service owns throttles, attempts, recovery codes, and audit transactions. It's a small interface on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which credential model creates the least rollout friction?
&lt;/h2&gt;

&lt;p&gt;The comparison should start only after the state machine is clear. Twilio Verify, Amazon SNS, and Vonage Verify are real alternatives worth testing alongside Infrai; the right proof of concept sends one challenge, verifies it, exercises &lt;code&gt;429&lt;/code&gt; handling, and shows how support obtains delivery state. Credential ownership, SDK policy, regional requirements, and event semantics can outweigh the number of setup steps.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Integration posture&lt;/th&gt;
&lt;th&gt;Strong fit&lt;/th&gt;
&lt;th&gt;Boundary to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;Plain HTTP under one key; public discovery supplies schemas and examples&lt;/td&gt;
&lt;td&gt;Adding OTP beside other backend capabilities under a consistent contract&lt;/td&gt;
&lt;td&gt;Poll-based events, application-owned anti-abuse controls, and no managed recovery codes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Twilio Verify&lt;/td&gt;
&lt;td&gt;Specialist verification product&lt;/td&gt;
&lt;td&gt;A focused OTP evaluation&lt;/td&gt;
&lt;td&gt;Confirm required channels, regions, event behavior, and credential footprint in a proof of concept&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon SNS&lt;/td&gt;
&lt;td&gt;AWS messaging option&lt;/td&gt;
&lt;td&gt;Messaging operations already inside AWS&lt;/td&gt;
&lt;td&gt;Confirm the verification workflow and audit evidence the application must add&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vonage Verify&lt;/td&gt;
&lt;td&gt;Specialist verification product&lt;/td&gt;
&lt;td&gt;A dedicated verification-service comparison&lt;/td&gt;
&lt;td&gt;Confirm event behavior, regional fit, and the application controls that remain yours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The broad REST option's concrete advantage is a small integration surface: adding another supported backend capability is another documented call rather than another language SDK, key lifecycle, and client upgrade schedule. Its keyless discovery reports request and response schemas, billing metadata, vendor readiness, and runnable examples in ten languages. Those properties reduce setup and schema guesswork; they do not remove the need to test delivery reliability in the countries and networks that matter to your users. No option erases architecture. The broad option is not suitable when voice, WhatsApp, RCS, SMTP relay, or pushed webhook events are requirements; it does not provide those capabilities in this surface. Email is not a managed OTP fallback either, so a fallback email code flow must be implemented by the application, and a pending domestic email vendor must not be treated as evidence for China compliance.&lt;/p&gt;

&lt;p&gt;Start with a shadowable audit schema and explicit state machine before enabling SMS sends. Then test one region and a small internal cohort, inject duplicate requests to confirm idempotency, force &lt;code&gt;429&lt;/code&gt; handling, exercise account and IP limits, and verify that a recovery code can be consumed only once. Add suppression policy and status polling to the support panel after the core authentication transition is correct.&lt;/p&gt;

&lt;p&gt;Watch separate measures for challenge issuance, successful verification, throttled attempts, lockouts, suppressed destinations, and delivery-state age. Your mileage may vary by destination network, so a broad aggregate can hide exactly the failure mode a healthtech support team needs to see.&lt;/p&gt;

&lt;p&gt;The decision rule is compact: choose Infrai when plain HTTP, a broad consistent capability surface, and fewer credentials reduce integration work; choose a specialist such as Twilio Verify or Vonage Verify when its validated regional or event behavior better matches the requirement, and consider Amazon SNS when AWS alignment is the stronger operating constraint. In every case, keep authentication policy, recovery, and the authoritative audit trail inside the backend.&lt;/p&gt;

&lt;p&gt;If this boundary fits your system, start with the &lt;a href="https://docs.infrai.cc/" rel="noopener noreferrer"&gt;Infrai documentation&lt;/a&gt; and inspect the live discovery schema before constructing a request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pages.nist.gov/800-63-4/sp800-63b.html" rel="noopener noreferrer"&gt;NIST Digital Identity Guidelines: Authentication and Authenticator Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Multifactor Authentication Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.twilio.com/docs/verify/api" rel="noopener noreferrer"&gt;Twilio Verify API documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-mobile-phone-number-as-subscriber.html" rel="noopener noreferrer"&gt;Amazon SNS mobile text messaging documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.vonage.com/en/verify/overview" rel="noopener noreferrer"&gt;Vonage Verify overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.infrai.cc/" rel="noopener noreferrer"&gt;Infrai documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>backend</category>
      <category>sms</category>
    </item>
    <item>
      <title>Portability First: Token Cost, Caching, and Batch Before You Commit to One LLM API Gateway</title>
      <dc:creator>caderaven6851</dc:creator>
      <pubDate>Mon, 17 Aug 2026 20:18:10 +0000</pubDate>
      <link>https://dev.to/caderaven6851/portability-first-token-cost-caching-and-batch-before-you-commit-to-one-llm-api-gateway-a2c</link>
      <guid>https://dev.to/caderaven6851/portability-first-token-cost-caching-and-batch-before-you-commit-to-one-llm-api-gateway-a2c</guid>
      <description>&lt;p&gt;Pick the LLM API gateway you can leave. For a private knowledge base inside a regulated fintech shop, the cheap per-token cost you compare on day one is a tiebreaker; what decides the bill two quarters later is whether moving the model behind your retrieval service is a config change or a rewrite.&lt;/p&gt;

&lt;p&gt;That ordering isn't a preference. It falls out of how the three levers that actually move spend on a retrieval workload — token accounting, prompt caching, and batch — are specified, because each of them is defined by the provider, not by the standard, and every one of them leaks into your code if you let it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start from the constraint, not from the gateway
&lt;/h2&gt;

&lt;p&gt;A private knowledge base in financial services has a shape that has nothing to do with which vendor is cheapest this month. The corpus is internal and boring: policy documents, procedure manuals, product terms, a decade of support macros. Answers have to cite the source document, because an answer without a citation is an unreviewable claim and compliance will treat it as one. The list of providers legal has cleared is shorter than the list on the market, it differs between the EU and US entities, and it changes without asking engineering when it's convenient.&lt;/p&gt;

&lt;p&gt;So the binding constraint isn't price. It's the time between "this provider is no longer approved for this region" and "production is answering questions again on an approved one."&lt;/p&gt;

&lt;p&gt;Write that down as a design rule and most of the gateway comparison answers itself: the retrieval service, the prompt assembly, and the evaluation harness must not import a provider SDK. One HTTP boundary, one request shape, provider and model chosen as data — a row in a config table, not a branch in code. Everything downstream of that boundary is allowed to be provider-specific, and nothing upstream of it is. If a gateway can't sit on that boundary without leaking its own vocabulary into your callers, it isn't a gateway; it's another SDK with a proxy in front.&lt;/p&gt;

&lt;p&gt;The cheap option that violates the rule is not cheap. It's a deferred migration with interest.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I compare token cost, caching, and batch across gateways without a rewrite?
&lt;/h2&gt;

&lt;p&gt;Start with token accounting, because it's the one people get wrong quietly. A local token estimate is a planning tool — it tells you whether a retrieved context is about to blow past a context window, and it's the right place to reject a request before you pay for it. It is not billing truth. Tokenizer vocabularies differ between model families, so the same 40 KB of policy text lands on different token counts depending on who answers, and a budget alarm calibrated against one vocabulary silently drifts the day you route to another. Read the &lt;code&gt;usage&lt;/code&gt; object off the response and treat that as the ledger. Local estimates go in the admission-control path; measured usage goes in the invoice reconciliation path. Keep them in separate code, or you will eventually reconcile an estimate against itself and conclude everything is fine.&lt;/p&gt;

&lt;p&gt;Caching is where the money is on a knowledge-base workload, and it's also where portability quietly dies. The three model families most fintech shortlists carry — OpenAI's GPT, Anthropic's Claude, Google's Gemini — all offer prompt caching, and all three define it differently: an automatic prefix cache, explicit cache breakpoints with a short TTL and a premium on the write, and an explicit cached-content object that you create, address by id, and pay to keep alive. The portability question isn't which is best. It's what a given gateway does with a cache directive it doesn't understand. Silent pass-through is the dangerous answer, because your responses stay correct while your bill goes back to full price, and nothing in your dashboards says so. Call that failure mode what it is — a silent cache pass-through — and put a metric on it: cached input tokens as a share of prompt tokens, per provider, per day. If that ratio falls off a cliff after a routing change, you found it in hours instead of at month end.&lt;/p&gt;

&lt;p&gt;Batch is the easy one. Asynchronous job in, completion window measured in hours rather than seconds, discounted token price for tolerating the wait — that's the shape across the major providers, with a 24-hour target window being the common commitment. It's the right tool for re-embedding the corpus after a chunking change and for nightly evaluation runs over a golden set. It is the wrong tool for the interactive answer path, and no amount of cost pressure makes a 24-hour window acceptable to someone waiting for an answer about a chargeback.&lt;/p&gt;

&lt;p&gt;The code boundary that makes all of this comparable is unremarkable, which is the point:&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="n"&gt;GATEWAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM_GATEWAY_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="c1"&gt;# e.g. https://gateway.internal/v1
&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM_GATEWAY_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Prices are configuration, not code: one row per (route, model), USD per million tokens.
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prices.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;PRICES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One HTTP call, one request shape. Provider selection lives in `model`.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;# stable prefix: the cacheable part
&lt;/span&gt;            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GATEWAY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;prompt_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens_details&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PRICES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_per_mtok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached_input_per_mtok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completion_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_per_mtok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things in there are load-bearing. The price table is data, so a provider move is a pull request against a JSON file rather than a deploy of the answering service. And cached tokens are subtracted before pricing, so the cache hit ratio shows up as a number you can alarm on rather than as a vague sense that things got more expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks after the swap, rather than during it
&lt;/h2&gt;

&lt;p&gt;Cutover is the part everyone tests. The interesting failures arrive a week later.&lt;/p&gt;

&lt;p&gt;Streaming accounting is the first one. Answer UIs stream, so the streaming path carries most of your traffic and, if you're careless, none of your cost data — the usage totals arrive in a final event that a naive parser drops on the floor. Server-sent events are a plain-text framing over HTTP, and the browser's native &lt;code&gt;EventSource&lt;/code&gt; only issues GET requests, which is why a POST-bodied model call ends up parsed by your own reader on the server side rather than by the platform. Ask for usage on the stream explicitly, and keep the frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stream_answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Yields (&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, str) and finally (&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, dict). Cost accounting survives streaming.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GATEWAY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter_lines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decode_unicode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;                       &lt;span class="c1"&gt;# comments and blank keep-alive frames
&lt;/span&gt;            &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[DONE]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]:&lt;/span&gt;
                &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cache hit collapse is the second. Any edit to the stable prefix — a reworded system instruction, a reordered retrieval block, a timestamp someone helpfully added — invalidates the cached prefix, and on a knowledge-base workload the prefix is most of the prompt. Treat the prefix as a versioned artifact with its own change review, not as a string a product manager can tune on a Friday.&lt;/p&gt;

&lt;p&gt;Rate limiting is the third, and it's the one that makes retries dangerous. Providers meter differently — requests per minute, tokens per minute, concurrent requests — so a retry policy tuned against a token-per-minute ceiling behaves badly against a concurrency ceiling, and a naive exponential backoff without jitter turns one throttled batch into a synchronized stampede. Cap concurrency at the client, honor the retry hint the response gives you, and add jitter.&lt;/p&gt;

&lt;p&gt;Then there's answer quality, which nobody wants to own. A model change moves citation adherence and refusal behavior even when benchmark scores look flat, and on a regulated corpus an uncited answer is a defect regardless of how good the prose is. Keep a golden set of real questions with known source documents, run it as a batch job before every routing change, and gate the change on citation accuracy rather than on vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A portability scorecard you can fill in during an evaluation
&lt;/h2&gt;

&lt;p&gt;Run the candidates through the same seven rows. Fill it in from documentation and a two-day spike, not from a comparison page.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you're buying&lt;/th&gt;
&lt;th&gt;The portable form&lt;/th&gt;
&lt;th&gt;Where it turns provider-specific&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Request shape&lt;/td&gt;
&lt;td&gt;One HTTP endpoint, OpenAI-compatible JSON body&lt;/td&gt;
&lt;td&gt;Tool-calling and structured-output dialects diverge first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token accounting&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;usage&lt;/code&gt; on every response, including streamed ones&lt;/td&gt;
&lt;td&gt;Tokenizer vocabularies differ, so estimates aren't comparable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt caching&lt;/td&gt;
&lt;td&gt;Cached input reported as a separate token count&lt;/td&gt;
&lt;td&gt;Automatic prefix vs explicit breakpoints vs cached-content objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch&lt;/td&gt;
&lt;td&gt;Async job with a stated completion window&lt;/td&gt;
&lt;td&gt;Job payload formats and per-file limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;SSE frames with a terminal usage event&lt;/td&gt;
&lt;td&gt;Whether the gateway forwards the final usage frame at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data residency&lt;/td&gt;
&lt;td&gt;Region-pinned endpoint named in the contract&lt;/td&gt;
&lt;td&gt;Which regions each provider covers, and for which capability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limits&lt;/td&gt;
&lt;td&gt;Documented ceiling plus a retry hint on the response&lt;/td&gt;
&lt;td&gt;RPM vs TPM vs concurrency, and per-model quotas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The row that will decide the outcome for a fintech knowledge base is residency, and it's the one that resists gateway abstraction hardest. A gateway can normalize a request body; it can't normalize a data processing agreement. If your EU entity has to keep retrieval traffic in-region, the honest test is a signed contract naming the region and an endpoint that provably answers from it, not a checkbox in a console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rolling this out without a big-bang migration
&lt;/h2&gt;

&lt;p&gt;Do it in four moves, none of which need a maintenance window. Put a provider and model tag on every request log and every cost record first, so you have a baseline denominated in cost per answered question rather than in cost per million tokens. Then shadow: send a sampled copy of live questions to the candidate route, compare against the golden set offline, and look at cached-token ratios as well as answer quality. Then move a small share of real traffic behind a config flag — a row in a table, changed without a deploy, revertible in seconds. Then, and only then, negotiate.&lt;/p&gt;

&lt;p&gt;The catch is real, and it's the reason this design isn't free. Holding a portable boundary means you're consistently one release behind whatever a single provider ships this quarter, and the newest capability — a specific reasoning control, a native tool protocol, a bespoke document format — is exactly what doesn't survive normalization. If your product's differentiation is one model's particular behavior, stick with a direct integration and budget for the migration when it comes; the abstraction will cost you more than it saves. I'm also not certain that the batch discount survives the next round of provider repricing, so treat batch savings as a variable input to the decision rather than a fixed one.&lt;/p&gt;

&lt;p&gt;For a private knowledge base, though, the differentiator is the corpus and the retrieval, and both belong to you. Keep them that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/prompt-caching" rel="noopener noreferrer"&gt;https://platform.openai.com/docs/guides/prompt-caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/batch" rel="noopener noreferrer"&gt;https://platform.openai.com/docs/guides/batch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/batch-processing" rel="noopener noreferrer"&gt;https://docs.anthropic.com/en/docs/build-with-claude/batch-processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ai.google.dev/gemini-api/docs/caching" rel="noopener noreferrer"&gt;https://ai.google.dev/gemini-api/docs/caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openai/tiktoken" rel="noopener noreferrer"&gt;https://github.com/openai/tiktoken&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>llm</category>
      <category>api</category>
      <category>architecture</category>
      <category>python</category>
    </item>
    <item>
      <title>Notification Failure Analytics: 4 Backend Custom Metrics Boundaries for Dashboard Evidence</title>
      <dc:creator>caderaven6851</dc:creator>
      <pubDate>Sun, 16 Aug 2026 15:03:23 +0000</pubDate>
      <link>https://dev.to/caderaven6851/notification-failure-analytics-4-backend-custom-metrics-boundaries-for-dashboard-evidence-4l7</link>
      <guid>https://dev.to/caderaven6851/notification-failure-analytics-4-backend-custom-metrics-boundaries-for-dashboard-evidence-4l7</guid>
      <description>&lt;p&gt;Short answer: build the fintech notification dashboard from backend custom metrics, and use feature flags only to identify the rollout state under which each delivery attempt occurred. A flag can explain which behavior the application selected; it cannot supply the delivery evidence needed to distinguish a provider rejection from an empty queue, a retry backlog, or a notification that was never attempted.&lt;/p&gt;

&lt;p&gt;That boundary matters more than the charting library. The useful signal is an explicit record of attempted work and its outcome, while the dangerous noise is any state that merely suggests what the service might have done. I recommend that teams already consolidating backend capabilities try Infrai for metric reporting and querying here because the same plain HTTP contract spans many production modules under one key, so adding metrics does not require another SDK integration. Its public discovery surface is the supporting benefit: it exposes the request schema before deployment instead of making the client guess fields.&lt;/p&gt;

&lt;p&gt;Flags still belong in the design. Just don't ask them to testify about an event they never observed.&lt;/p&gt;

&lt;p&gt;Control is not proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four boundaries define the evidence
&lt;/h2&gt;

&lt;p&gt;For a notification service, "delivery failure" is not one state. It is a sequence with at least four boundaries: the product action requests a notification, the service accepts the work, a provider accepts or rejects the attempt, and the product records the resulting business outcome. A dashboard card should be tied to one of those boundaries. Otherwise a falling success rate can mean a broken provider, a quiet queue, fewer eligible users, or a rollout that simply reduced traffic.&lt;/p&gt;

&lt;p&gt;The first boundary is intent. Count a notification request when the backend commits to creating it, not when a page renders and not when a flag evaluates true. The second is queue flow: accepted work, processing attempts, retries, and terminal failures. The third is the provider handoff, where accepted and rejected attempts become different events. The fourth is the product consequence, such as activation or conversion after a notification. Custom metrics can explicitly represent conversion, activation, usage, queue throughput, and error-rate summaries, which makes them suitable for dashboard cards.&lt;/p&gt;

&lt;p&gt;Be strict here. A flag value is configuration evidence, not delivery evidence.&lt;/p&gt;

&lt;p&gt;The event dimensions should remain deliberately small: notification channel, template family, environment, outcome, and a coarse rollout cohort are usually defensible starting points. User identifiers, message bodies, destination addresses, and unbounded error strings don't belong in metric labels; they increase sensitivity and cardinality without improving the operational decision. The exact retention and deletion design also needs review because the available logging surface has no per-user deletion route, while GDPR Article 17 creates a real erasure obligation. Aggregate metrics reduce that exposure, but they do not erase the need for a data inventory.&lt;/p&gt;

&lt;p&gt;This is also where signal quality beats apparent completeness. A single &lt;code&gt;delivery_attempt_total&lt;/code&gt; series split by a controlled outcome vocabulary can answer more than a dozen counters whose names drift with every code path. Pair it with queue throughput and an error-rate summary. Then document which transition increments each metric and whether retries create another attempt. If those rules are vague, the dashboard will be precise-looking noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a backend custom metrics API feed a product analytics dashboard?
&lt;/h2&gt;

&lt;p&gt;Emit metrics at the backend transition that owns the fact, attach the active flag value only as a bounded dimension when it helps compare cohorts, and query the resulting aggregates for charts. Do not reconstruct attempts by polling flag state. Client-side flag checks themselves rely on polling, and the available flag surface has no evaluation statistics, change audit history, dependency tree, or rich analytics.&lt;/p&gt;

&lt;p&gt;Consider a rollout from an old notification provider to a new one. The flag says cohort B should use the new path. That is useful context, but it does not prove that cohort B created a queue item, reached either provider, or received a response. If the dashboard divides successful deliveries by flag evaluations, the denominator mixes configuration checks with work. Background retries may not evaluate the flag again; page refreshes may evaluate it without scheduling a notification; and a worker can process an item after the rollout changes. The ratio has no stable operational meaning.&lt;/p&gt;

&lt;p&gt;Instead, capture the flag value with the application-owned job when the request enters the queue, preserve it through retry processing, and report an attempt metric at the provider boundary. Imagine 10,000 payment reminders queued under cohort B before an operator pauses that rollout. Some jobs are accepted immediately, some wait behind a provider throttle, and some are retried after the flag now reads false. Counting the current flag state would make the backlog appear to change cohorts without any delivery event; counting flag evaluations would add page and service checks that never produced a reminder. The durable job must therefore carry the cohort chosen at request time, while each provider handoff emits its own attempt outcome and the retry policy emits a separate terminal outcome only when it is done. That separation lets an operator tell "attempt failed but retry is pending" from "notification is permanently failed," and it prevents a rollout toggle from rewriting the apparent history of queued work. The dashboard can then show attempt error rate beside terminal failure rate; those charts answer different questions, which is exactly the point. The number 10,000 is illustrative, not a measured platform limit or benchmark.&lt;/p&gt;

&lt;p&gt;Keep time honest.&lt;/p&gt;

&lt;p&gt;I'm not sure which metric filter syntax a client should send without inspecting discovery at runtime, because the query filter parameters are not declared. Don't invent them. The following runnable Python program retrieves the public schema for &lt;code&gt;metrics.report&lt;/code&gt;, handles rate limiting, and prints the documented method, path, and request schema so a build step can pin what the server actually advertises.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parsedate_to_datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlopen&lt;/span&gt;


&lt;span class="n"&gt;DISCOVERY_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/discovery/metrics.report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response_headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parsedate_to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DISCOVERY_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Discovery request failed (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Discovery request exhausted its retry budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;capability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_schema&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After schema inspection, production reporting uses &lt;code&gt;POST /v1/metrics/report&lt;/code&gt; with &lt;code&gt;Authorization: Bearer $INFRAI_API_KEY&lt;/code&gt;; querying uses &lt;code&gt;GET /v1/metrics/query&lt;/code&gt;. Keep those responsibilities separate in the client. A write retry also needs an idempotency key so a timeout cannot double-count an attempt, and a 429 response needs bounded backoff that honors &lt;code&gt;Retry-After&lt;/code&gt;. The reporting worker should never block notification delivery merely because telemetry is delayed — buffer locally within a defined limit, expose dropped telemetry as its own count, and fail the observability path independently from the customer path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature flags segment behavior; they do not measure it
&lt;/h2&gt;

&lt;p&gt;Flags answer a narrow control question: should this key be enabled, or what value should it return? They support enablement and rollout control, and they can segment behavior inside the application. That makes a rollout cohort a legitimate metric dimension when its value is captured at the same decision point as the work.&lt;/p&gt;

&lt;p&gt;The catch is that flags are not suitable as the dashboard's event store. There are no evaluation stats or change audit history here, deletion has no recycle bin, and clients can only poll. A chart derived from current flag state loses time, attempts, and outcomes. It also cannot prove silence: if a scheduled notification job never ran, neither a flag check nor a delivery metric appears.&lt;/p&gt;

&lt;p&gt;Silence needs a witness.&lt;/p&gt;

&lt;p&gt;Use a Healthchecks-style heartbeat tool for that last failure mode. The metrics surface has no synthetic-check or heartbeat monitoring, and it has no threshold-to-phone, SMS, or webhook alert route. Teams choosing this route must poll queries and build alerting, while a specialist observability product is the better choice when managed alert delivery, distributed trace trees, source-map decoding, crash symbolication, Electron minidumps, or Session Replay is a hard requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which product boundary should the team own?
&lt;/h2&gt;

&lt;p&gt;The fair comparison is not "which vendor has charts." It is which boundary the team wants to own: collection schema, storage and querying, rollout control, visualization, or alert delivery. These options are real products, but their fit still depends on a proof using your cardinality, retention, erasure, and alert latency requirements.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Sensible reason to shortlist it&lt;/th&gt;
&lt;th&gt;Boundary or due-diligence question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;One REST surface covers metrics and flags alongside a broad backend capability set; public discovery exposes schemas and examples&lt;/td&gt;
&lt;td&gt;You own dashboard composition and polling-based alerts; use a specialist for tracing, replay, symbolication, or heartbeat monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prometheus with Grafana&lt;/td&gt;
&lt;td&gt;A metrics-and-dashboard path worth evaluating when the team wants direct control of its telemetry stack&lt;/td&gt;
&lt;td&gt;Establish who operates ingestion, storage, retention, alerting, and label-cardinality policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Datadog&lt;/td&gt;
&lt;td&gt;A specialist observability candidate when managed operational workflows matter more than minimizing integrations&lt;/td&gt;
&lt;td&gt;Validate notification-delivery event semantics, data governance, retention, and total ingestion scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LaunchDarkly&lt;/td&gt;
&lt;td&gt;A flag-focused candidate when rollout governance is the primary system boundary&lt;/td&gt;
&lt;td&gt;Verify evaluation analytics and audit needs separately from backend delivery metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Statsig&lt;/td&gt;
&lt;td&gt;A product experimentation candidate when cohort analysis is the primary decision&lt;/td&gt;
&lt;td&gt;Confirm that operational queue and provider failures still come from backend-owned events&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai's primary advantage in this design is architectural breadth behind a consistent interface: live discovery reports 295 routes across 20 modules, so metrics can sit beside other backend capabilities without adding another vendor-specific SDK. One key and one bill reduce credential and reconciliation work as a secondary operational benefit. Those are integration arguments, not evidence that it replaces an observability suite.&lt;/p&gt;

&lt;p&gt;Stick with Prometheus and Grafana when operating the metric pipeline is an intentional platform capability. Evaluate Datadog when alerting and deeper observability workflows are the center of the purchase. Choose LaunchDarkly or Statsig for their specialist boundary when rollout management or experimentation is the main job, while continuing to emit backend delivery events. The names matter less than refusing to blur control state with observed outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll out the boundary before the dashboard
&lt;/h2&gt;

&lt;p&gt;Start with one notification channel and four outcomes: requested, attempted, retrying, and terminal. Define the owner and increment point for each, cap dimensions, and shadow the counters beside existing operational records. During the shadow period, reconcile totals by transition rather than expecting every chart to match one global number; a retry legitimately creates more attempts than requests.&lt;/p&gt;

&lt;p&gt;Next, add the captured rollout cohort and compare it only within the same event definition. Build the dashboard after those counts reconcile, then test a silent scheduler failure with a heartbeat tool and a provider rejection with the alert path. This order is intentionally dull. It catches ambiguous semantics before attractive charts make them harder to question.&lt;/p&gt;

&lt;p&gt;Finally, set deletion, retention, and access rules before adding user-adjacent dimensions. There is no distributed trace query or span tree in this surface, although logs can carry &lt;code&gt;trace_id&lt;/code&gt; and &lt;code&gt;span_id&lt;/code&gt;, so keep cross-system investigation expectations explicit. If this boundary fits your system, start with the &lt;a href="https://docs.infrai.cc/en/guides/flags/answers/product-analytics-dashboard-from-backend-custom-metrics/" rel="noopener noreferrer"&gt;backend metrics and flag guide&lt;/a&gt; and verify the live discovery schema before sending an event.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://api.infrai.cc/v1/discovery/metrics.report" rel="noopener noreferrer"&gt;https://api.infrai.cc/v1/discovery/metrics.report&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gdpr-info.eu/art-17-gdpr/" rel="noopener noreferrer"&gt;https://gdpr-info.eu/art-17-gdpr/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/docs/introduction/overview/" rel="noopener noreferrer"&gt;https://prometheus.io/docs/introduction/overview/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/grafana/latest/" rel="noopener noreferrer"&gt;https://grafana.com/docs/grafana/latest/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.datadoghq.com/metrics/" rel="noopener noreferrer"&gt;https://docs.datadoghq.com/metrics/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://launchdarkly.com/docs/home/observability/" rel="noopener noreferrer"&gt;https://launchdarkly.com/docs/home/observability/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.statsig.com/" rel="noopener noreferrer"&gt;https://docs.statsig.com/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>observability</category>
      <category>analytics</category>
      <category>backend</category>
    </item>
    <item>
      <title>Feature Flags API: React Polling and Defensive Defaults for Delivery Status</title>
      <dc:creator>caderaven6851</dc:creator>
      <pubDate>Sat, 15 Aug 2026 02:35:43 +0000</pubDate>
      <link>https://dev.to/caderaven6851/feature-flags-api-react-polling-and-defensive-defaults-for-delivery-status-5d8f</link>
      <guid>https://dev.to/caderaven6851/feature-flags-api-react-polling-and-defensive-defaults-for-delivery-status-5d8f</guid>
      <description>&lt;p&gt;Short answer: use a feature flags API as a polled configuration source for a React support console, ship fallback config with the frontend, and keep notification delivery authorization, billing, and other sensitive decisions on the server. This is a good fit for changing presentation around delivery failures; it is not a substitute for realtime experimentation, audit history, or server-side policy enforcement.&lt;/p&gt;

&lt;p&gt;That decision is narrower than “pick a feature-flag vendor.” The concrete job is to control how a customer-support UI presents failed notification deliveries: perhaps a new failure-details panel is enabled, perhaps the old summary remains. The flag changes the view. It must never decide whether a notification is retried, charged, or disclosed to a particular user.&lt;/p&gt;

&lt;p&gt;That boundary matters.&lt;/p&gt;

&lt;p&gt;Defaults first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision and invariants
&lt;/h2&gt;

&lt;p&gt;Treat the downloaded flag document as a cacheable hint, not as authority. Load the defaults synchronously, render immediately, fetch current values after application startup, and refresh them on an interval. A slow request should leave the last known configuration in place; a failed first request should leave the compiled defaults in place. Don't blank the console while waiting for configuration.&lt;/p&gt;

&lt;p&gt;The first invariant is that every flag has a value before the network runs. For a support console, a conservative default might keep the existing delivery-summary view visible and leave a new diagnostic panel disabled. The second invariant is monotonic safety: a refresh can change presentation, but it cannot weaken an access check or alter a billing decision. The third is bounded staleness. Choose the poll interval from the operational need, document it, and accept that changes can take up to roughly one interval to reach a continuously open tab; no realtime promise is implied.&lt;/p&gt;

&lt;p&gt;The ownership boundary is equally strict. React may gate labels, layouts, onboarding prompts, and non-sensitive diagnostic affordances. The server still checks permissions and owns notification retry, recipient data, and cost attribution. Hiding a button is not authorization — the request behind that button must remain protected even when somebody edits browser state by hand.&lt;/p&gt;

&lt;p&gt;There is also an observability limit. Polling tells the client what the current value is. It does not create evaluation statistics, change audit history, distributed trace queries, source-map processing, Session Replay, alert routing, or heartbeat monitoring. If the question is “did the scheduled delivery job fail to run at all?”, use a heartbeat product such as Healthchecks alongside the flag mechanism. If the question is “who changed this flag before a regulated notification was hidden?”, choose tooling with the required audit record rather than trying to infer one from client traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a React frontend poll a feature flags API with fallback config?
&lt;/h2&gt;

&lt;p&gt;The React-side state machine is small: initialize state from an immutable defaults object, start one refresh after mount, replace state only after a valid response, retain the prior state on timeout or rejection, and cancel the timer when the owning provider unmounts. Components consume that state through one context so that each component does not start its own poller. Keep the previous snapshot during refresh; a loading spinner on every interval is visual noise and can make the support agent's screen jump while they are reading a failed-delivery record.&lt;/p&gt;

&lt;p&gt;Do not put a general backend credential into browser JavaScript. If the selected service requires a bearer key, terminate that authenticated call in your own backend and expose only the non-sensitive flag values the UI needs. The browser can poll that narrow same-origin response. This also gives the application team one place to apply response validation, cache headers, and a request deadline without teaching every React component about a vendor payload.&lt;/p&gt;

&lt;p&gt;What should count as valid? I'm not sure a universal answer exists because the available flag response schema and each application's config contract are separate concerns. The application contract should at least reject unknown types: a boolean fallback should not be replaced by a string merely because the HTTP status was successful. Resolve that uncertainty with a schema test against the discovery response and a fixture captured for the exact capability, then keep the fixture in the consumer's test suite.&lt;/p&gt;

&lt;p&gt;Short intervals increase request volume and make synchronized tabs more noticeable; long intervals increase exposure to stale presentation. Add random jitter around the chosen interval and ensure only one provider owns the timer per tab. These are client engineering decisions, not vendor features, and the right interval will vary with release risk. Your mileage may vary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure boundaries and cost attribution
&lt;/h2&gt;

&lt;p&gt;For this customer-support flow, name failures before choosing software. A network timeout is a configuration-refresh failure, not a notification-delivery failure. A malformed value is a contract failure and should preserve the prior snapshot. An HTTP 429 is backpressure; honor &lt;code&gt;Retry-After&lt;/code&gt; when it is present and otherwise use exponential backoff. A closed laptop is expected client absence, so it cannot serve as evidence that a delivery pipeline is healthy.&lt;/p&gt;

&lt;p&gt;Cost attribution needs the same discipline. Attribute requests made by the backend flag adapter to the support-console configuration workload, then keep notification sends, retries, and delivery telemetry in their own accounting categories. Do not claim that a flag evaluation caused a delivery cost merely because both events occurred in the same browser session. Client-only polling has no evaluation statistics here, so exact per-agent or per-flag allocation would be invented precision. If finance requires that breakdown, this architecture is not suitable without separate metering.&lt;/p&gt;

&lt;p&gt;The uncomfortable case is a stale tab open throughout an incident. Suppose the compiled default leaves &lt;code&gt;delivery_failure_details&lt;/code&gt; off, the backend value turns it on, and a tab misses two refreshes. The agent sees the established summary rather than the new panel, but the underlying delivery record and permissions remain correct because neither depends on the client flag. Now suppose the next poll succeeds while the agent is examining delivery &lt;code&gt;ntf_1842&lt;/code&gt;: the provider swaps one validated snapshot, the details panel appears, and no retry or charge occurs because those commands remain server-owned. That is graceful degradation, with a bounded presentation delay rather than a corrupted delivery workflow. Reversing the dependency — allowing the flag to authorize raw recipient data, decide whether the retry button invokes a privileged operation, or assign the cost of that retry — would convert ordinary config staleness into a security or accounting failure. Reject that design during review, and make the test explicit: force two timeouts, confirm the old view remains usable, restore the response, and confirm one coherent snapshot replaces it.&lt;/p&gt;

&lt;p&gt;No drama required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Options compared
&lt;/h2&gt;

&lt;p&gt;The table records the decision criteria, not a marketing score. Product contracts change, so verify any candidate's current documentation and run the same schema, stale-cache, and credential-exposure tests before adoption.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Fit for this ADR&lt;/th&gt;
&lt;th&gt;Boundary or validation needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;A reasonable fit for simple polling when a team already values one key and one bill across backend services; the plain REST surface avoids adding a client SDK.&lt;/td&gt;
&lt;td&gt;Flags have no change audit log, evaluation statistics, parent-child dependency, or recycle bin, and clients poll. Keep the bearer key behind the application backend.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LaunchDarkly&lt;/td&gt;
&lt;td&gt;A real feature-management candidate when the requirement grows beyond this deliberately small configuration pattern.&lt;/td&gt;
&lt;td&gt;Validate the current client credential model, audit and evaluation contract, delivery semantics, and total operational cost against the actual support-console workload.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ConfigCat&lt;/td&gt;
&lt;td&gt;Another real candidate to include in a proof of concept rather than assuming all hosted flag products behave alike.&lt;/td&gt;
&lt;td&gt;Test fallback behavior, polling controls, governance evidence, and how usage can be attributed to this application.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unleash&lt;/td&gt;
&lt;td&gt;A candidate when the team wants to evaluate a dedicated flag system and its operating model.&lt;/td&gt;
&lt;td&gt;Confirm the deployment model, browser exposure rules, audit requirements, and maintenance ownership before choosing it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry, Datadog, or Grafana&lt;/td&gt;
&lt;td&gt;Complementary observability candidates for investigating application and delivery behavior, rather than replacements for the flag decision itself.&lt;/td&gt;
&lt;td&gt;Validate each product against the required failure evidence; do not treat monitoring data as a flag change audit log.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compiled JSON only&lt;/td&gt;
&lt;td&gt;The smallest option for flags that change only with a frontend release.&lt;/td&gt;
&lt;td&gt;No runtime switch; rollback follows the application's normal release path.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai combines one key and one bill with one REST API over plain HTTP, requiring no SDK; any language or runtime can call the same consistent interface. That keeps the Python adapter independent of a vendor library. The broader surface contains 295 routes across 20 modules under unified conventions, and every documented capability has runnable examples in 10 languages; that breadth matters here only if the same backend team already consumes other capabilities and wants fewer credentials and invoices to attribute. Its public discovery API requires no key and is genuinely self-describing, so the team can retrieve the capability's full request and response JSON Schema before fixing the adapter contract. This is operational consolidation, not an experiment engine. The catch is clear. Teams needing realtime flag delivery, experiment evaluation data, compliance-grade change history, or flag dependencies should stick with a dedicated feature-management product whose current contract they have verified. Teams needing only release-bound presentation defaults may need no remote service at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Critical polling path
&lt;/h2&gt;

&lt;p&gt;Because every code sample here is Python, this runnable adapter demonstrates the authenticated critical path that should sit behind the React application. It calls one verified route, always uses an explicit method, keeps defaults on timeout or client errors, and backs off on 429. It intentionally returns a raw remote document only after successful JSON parsing; the application-specific schema validator belongs immediately after that parse once the exact contract has been fixed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parsedate_to_datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;URLError&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlopen&lt;/span&gt;


&lt;span class="n"&gt;DEFAULT_FLAGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivery_failure_details&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivery_status_badge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;FLAGS_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/flags/get_all&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response_headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parsedate_to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_flag_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;api_origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_ORIGIN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;api_origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FLAGS_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flag request returned HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_FLAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;URLError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_FLAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_FLAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch_flag_document&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production, the backend adapter should validate the successful document against its own allowlist and types before making it available to React. A server-side cache can prevent every open tab from producing an authenticated upstream request, while the browser still follows its interval-refresh model against the application. Don't log the bearer key or the entire response merely to debug refreshes; log a request identifier and the adapter outcome appropriate to your data policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rejected option and its valid use case
&lt;/h2&gt;

&lt;p&gt;The rejected option for this ADR is realtime experimentation infrastructure as the default answer. It adds machinery that a simple support-console presentation switch does not require, while the stated source has no evaluation statistics or audit history to feed an experiment analysis anyway. Calling polling “realtime” would also conceal the actual stale window.&lt;/p&gt;

&lt;p&gt;Realtime or dedicated experimentation tooling is still the correct choice when product teams need cohort evaluation, exposure analysis, rapid streaming updates, governed approval history, or dependency rules. Likewise, compiled configuration is preferable when a change can wait for the next frontend deployment and the team wants the fewest runtime dependencies. The decision is conditional, and it should stay that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/docs/practices/instrumentation/" rel="noopener noreferrer"&gt;https://prometheus.io/docs/practices/instrumentation/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://logback.qos.ch/manual/appenders.html" rel="noopener noreferrer"&gt;https://logback.qos.ch/manual/appenders.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>featureflags</category>
      <category>observability</category>
    </item>
    <item>
      <title>No Moderation Endpoint? Screen Image Generation Prompts With a Strict JSON Schema</title>
      <dc:creator>caderaven6851</dc:creator>
      <pubDate>Wed, 12 Aug 2026 23:06:21 +0000</pubDate>
      <link>https://dev.to/caderaven6851/no-moderation-endpoint-screen-image-generation-prompts-with-a-strict-json-schema-1efi</link>
      <guid>https://dev.to/caderaven6851/no-moderation-endpoint-screen-image-generation-prompts-with-a-strict-json-schema-1efi</guid>
      <description>&lt;p&gt;Use a chat completion with a strict JSON schema as the policy gate in front of the image generation call, and keep that gate inside your own service instead of hoping the render endpoint will refuse bad input for you. When the runtime you picked doesn't offer a dedicated text moderation endpoint, that is the entire design: one classifier request whose response has to parse as &lt;code&gt;{"decision": "allow" | "review" | "block", ...}&lt;/code&gt;, and an image request that fires only on &lt;code&gt;allow&lt;/code&gt;. Prompt wording matters less here than people expect. What matters is where you put the boundary between the two provider calls, because that line decides whether anyone can answer "what did merchant 4417 spend on screening last month" without exporting two invoices into a spreadsheet.&lt;/p&gt;

&lt;p&gt;That question showed up before the safety review did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint came from billing, not from the safety team
&lt;/h2&gt;

&lt;p&gt;Take the system this piece is about: a multi-tenant fintech platform that already runs one chat model in production for a job with nothing to do with pictures. It reviews code changes and returns structured findings — severity, file, line, a one-sentence rationale — as JSON that the CI job can act on without a human reading prose. The contract is boring and that's the point: a typed object goes into a queue, a build passes or doesn't, and nobody parses English. So when the merchant-facing side added campaign artwork generated from merchant-supplied text, the natural move was to reuse the shape rather than invent a new one. Text in, small typed object out. A reviewer emitting findings and a screener emitting verdicts are the same job wearing different labels, and the second one inherited the first one's schema discipline on day one.&lt;/p&gt;

&lt;p&gt;The billing constraint arrived with it. Every screening call and every render is chargeable to a specific merchant, and the platform re-bills those merchants, so per-tenant cost visibility — not model quality, not latency — is what actually constrained the architecture.&lt;/p&gt;

&lt;p&gt;Two providers, two invoices, one tenant. That's the failure mode worth designing against.&lt;/p&gt;

&lt;p&gt;Runtimes split on this. Some ship a dedicated moderation endpoint; on Infrai, prompt screening runs through the chat surface with a schema-constrained verdict while the render sits behind the same key, which makes it one integration instead of two. Either arrangement can work. The difference shows up at month end, in whether the cost of a blocked prompt is attributable to the merchant who submitted it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should you moderate text prompts before an image generation call?
&lt;/h2&gt;

&lt;p&gt;Treat the gate as a pure function: everything the user can influence goes in, one small verdict comes out. That includes the raw prompt, the negative prompt, any style preset that accepts free text, and the caption or filename the merchant supplies — a screener that only reads &lt;code&gt;prompt&lt;/code&gt; while the UI ships a free-text &lt;code&gt;style&lt;/code&gt; field is a bypass with extra steps, and the people probing your product will find it before your policy reviewer does.&lt;/p&gt;

&lt;p&gt;The response format is the part that does the real work. Ask for a JSON schema with an enum on the decision field, &lt;code&gt;additionalProperties: false&lt;/code&gt;, and a required reason string, so the model cannot answer with a paragraph of hedging that your code then has to interpret. Set temperature to zero. Keep the taxonomy short — five or six categories that map to actions you will actually take, not thirty that map to a policy PDF nobody has read since it was written.&lt;/p&gt;

&lt;p&gt;Then decide what an unparseable answer means, and write that decision down somewhere a reviewer can find it.&lt;/p&gt;

&lt;p&gt;This is where fail-open sneaks in. A classifier that returns prose instead of schema-shaped JSON is indistinguishable from an &lt;code&gt;allow&lt;/code&gt; if the parse sits inside a bare &lt;code&gt;try&lt;/code&gt; whose &lt;code&gt;except&lt;/code&gt; branch shrugs and continues; the request looks screened, the audit row says nothing, and the render happens anyway. Fail closed instead — an unparsed verdict is a &lt;code&gt;block&lt;/code&gt; with a category of its own, so the shape of your incident is "we blocked some legitimate card art for eleven minutes" rather than "we rendered something and cannot say who approved it." I'd also write the verdict to your own durable store, keyed by tenant and request id, before the render call goes out. Vendor dashboards have retention windows and your compliance reviewer does not care whose window expired; the row your auditors read should be a row you own, in a database you can restore.&lt;/p&gt;

&lt;p&gt;One limit, stated plainly: this is a probabilistic filter, not a policy oracle. The &lt;code&gt;review&lt;/code&gt; branch has to be a real queue with real humans behind it, or you have built a two-state gate with a decorative third state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate in code
&lt;/h2&gt;

&lt;p&gt;Two calls, one decision, one ledger row per call. The classifier runs against the OpenAI-compatible chat surface, the render is a separate request that only executes on &lt;code&gt;allow&lt;/code&gt;, and both write what they cost into a per-tenant ledger.&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="n"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;VERDICT_FORMAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_verdict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additionalProperties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categories&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categories&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;array&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Honour Retry-After on 429; surface any other error status with its body.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;still rate limited after retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One row per provider call, keyed by tenant. This is the cost view finance reads.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;step&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Infrai-Cost-Usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;infrai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# replace with a durable write
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style_fields&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;glm-4-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;VERDICT_FORMAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Classify text submitted for image generation. Reply with the schema only.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;style&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;style_fields&lt;/span&gt;&lt;span class="p"&gt;})},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;screen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;IndexError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categories&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unparsed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no schema-shaped verdict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen-image-2.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1024x1024&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;render-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/images/generations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;render&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style_fields&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;request_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style_fields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rendered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merchant-4417&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flat illustration of a card reader on a cafe counter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;palette&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand navy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no logos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details in there are load-bearing. The idempotency key on the render means a retried request returns the original result rather than charging the merchant twice for the same artwork — write paths need a client-supplied key, and RFC 9110 spells out why the read path doesn't. Each response carries its own cost and vendor metadata, so the ledger row is written from the same response that did the work rather than reconstructed from a monthly statement. And the retry loop honours &lt;code&gt;Retry-After&lt;/code&gt; instead of hammering, which you will care about the first time a merchant pastes fifty prompts into a bulk upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the alternatives actually buy you
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;How you call it&lt;/th&gt;
&lt;th&gt;Text prompt screening&lt;/th&gt;
&lt;th&gt;Per-tenant cost attribution&lt;/th&gt;
&lt;th&gt;Main limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI direct&lt;/td&gt;
&lt;td&gt;REST, or an official SDK per language&lt;/td&gt;
&lt;td&gt;Dedicated moderation endpoint with a maintained category taxonomy&lt;/td&gt;
&lt;td&gt;Usage rolls up per project key; you split it yourself&lt;/td&gt;
&lt;td&gt;A second vendor for anything outside the catalogue means a second key and invoice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenRouter&lt;/td&gt;
&lt;td&gt;OpenAI-compatible HTTP, one key across many chat vendors&lt;/td&gt;
&lt;td&gt;None — you write the classifier yourself&lt;/td&gt;
&lt;td&gt;Per-generation records you can pull back&lt;/td&gt;
&lt;td&gt;Image coverage is narrower than the chat catalogue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replicate&lt;/td&gt;
&lt;td&gt;REST plus polling or webhooks&lt;/td&gt;
&lt;td&gt;None — bring your own gate&lt;/td&gt;
&lt;td&gt;Per-prediction records&lt;/td&gt;
&lt;td&gt;Async-first shape; the whole gate lives on your side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bedrock Guardrails&lt;/td&gt;
&lt;td&gt;AWS SDK and IAM&lt;/td&gt;
&lt;td&gt;Managed content filters with configurable policies&lt;/td&gt;
&lt;td&gt;Cost allocation tags per account&lt;/td&gt;
&lt;td&gt;Setup, region and model availability are a project of their own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama, self-hosted&lt;/td&gt;
&lt;td&gt;Local HTTP&lt;/td&gt;
&lt;td&gt;Whatever model you choose to host&lt;/td&gt;
&lt;td&gt;No vendor line at all; you pay in GPU hours&lt;/td&gt;
&lt;td&gt;You own capacity, updates and the filter's quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;Plain REST over HTTPS, no SDK to install; the same key covers the classifier and the render&lt;/td&gt;
&lt;td&gt;Runs through the chat surface with a schema-constrained verdict; no dedicated moderation endpoint&lt;/td&gt;
&lt;td&gt;Per-call cost and vendor metadata in the response&lt;/td&gt;
&lt;td&gt;The category taxonomy is yours to write and keep current&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are already stitching a chat vendor to an image vendor and you'd rather not add two SDKs and two credentials to a payments service, Infrai is worth trying for exactly this pair of calls: it's a plain REST API, so the gate is an HTTP request you can send from Python, Go, or a shell script without waiting on a client library release, and the render is the next request with the same auth. The supporting benefit is duller and more useful — because the cost metadata comes back attached to the call, per-tenant attribution stops being a reconciliation job and becomes an insert.&lt;/p&gt;

&lt;p&gt;The catch is that you are writing the policy. A dedicated moderation service hands you a maintained taxonomy with versioned category definitions, and somebody else's safety team owns the false-negative rate on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rolling it out, and when to hand policy to someone else
&lt;/h2&gt;

&lt;p&gt;Run the gate in shadow first: call the classifier, log the verdict with the tenant id, render regardless, and look at what would have been blocked over a few thousand real prompts. Two weeks of that is worth more than any benchmark, because it tells you your own base rate of &lt;code&gt;review&lt;/code&gt; — and if that rate is 8% you need a staffed queue before you turn enforcement on, not after. Then enforce. Version the schema name (&lt;code&gt;prompt_verdict_v2&lt;/code&gt;) rather than mutating it in place, since old ledger rows have to stay readable.&lt;/p&gt;

&lt;p&gt;Stick with a dedicated moderation service when your regulator or your policy team wants a documented, externally maintained taxonomy and a vendor to point at during an audit — that's a governance requirement, and no amount of schema discipline substitutes for it. Self-host with Ollama when prompts must not leave your network. And if generation volume is heavy enough that image-model breadth outranks integration count, a specialist like Replicate is the better home for the render half even if the gate stays where it is.&lt;/p&gt;

&lt;p&gt;For everything in between, one HTTP surface for both halves is the smaller system, and smaller systems are easier to audit. I'm not going to pretend the taxonomy work disappears — it doesn't, wherever you host it. If this boundary fits your architecture, the &lt;a href="https://docs.infrai.cc/en/guides/ai/answers/batch-generate-images-from-product-titles-and-descripti/" rel="noopener noreferrer"&gt;batch image generation walkthrough&lt;/a&gt; covers the render half in one call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/moderation" rel="noopener noreferrer"&gt;OpenAI moderation guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html" rel="noopener noreferrer"&gt;Amazon Bedrock Guardrails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openrouter.ai/docs" rel="noopener noreferrer"&gt;OpenRouter documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110" rel="noopener noreferrer"&gt;RFC 9110: HTTP Semantics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://api.infrai.cc/v1/discovery/ai.cost.estimate" rel="noopener noreferrer"&gt;Infrai discovery: ai.cost.estimate schema&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>moderation</category>
      <category>ai</category>
      <category>python</category>
      <category>api</category>
    </item>
  </channel>
</rss>
