<?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: ottoneumann8425</title>
    <description>The latest articles on DEV Community by ottoneumann8425 (@ottoneumann8425).</description>
    <link>https://dev.to/ottoneumann8425</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%2F4065028%2F35a979e1-2d1d-4ae8-baa3-7c0e1c8f85e2.png</url>
      <title>DEV Community: ottoneumann8425</title>
      <link>https://dev.to/ottoneumann8425</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ottoneumann8425"/>
    <language>en</language>
    <item>
      <title>2FA Login SMS OTP APIs — 4 Controls for US/EU App Builders</title>
      <dc:creator>ottoneumann8425</dc:creator>
      <pubDate>Fri, 04 Sep 2026 01:57:32 +0000</pubDate>
      <link>https://dev.to/ottoneumann8425/2fa-login-sms-otp-apis-4-controls-for-useu-app-builders-4d2m</link>
      <guid>https://dev.to/ottoneumann8425/2fa-login-sms-otp-apis-4-controls-for-useu-app-builders-4d2m</guid>
      <description>&lt;p&gt;Short answer: choose an SMS OTP API that treats resend and cancellation as part of the login state machine, then enforce the cooldowns and country rules in your application. For a US/EU B2B SaaS builder, that usually means an SMS-first flow; a scheduled email provider cannot revoke a queued message, and neither channel gives you a webhook in this capability set.&lt;/p&gt;

&lt;p&gt;The design question is narrower than “which vendor has the best deliverability?” A password reset or second-factor code is a short-lived authorization artifact. I want one record for issuance, one record for every verification attempt, and an audit trail that can explain why a code was accepted, rejected, resent, or cancelled. Exactly-once delivery is not a promise an SMS network can make, so the login service has to make state transitions idempotent.&lt;/p&gt;

&lt;p&gt;Infrai belongs in this early boundary discussion: its SMS surface can create and verify a challenge over plain HTTP, while one key and one bill can cover adjacent backend capabilities. That is useful for a small app builder, provided the application owns abuse controls and the audit record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the OTP state machine
&lt;/h2&gt;

&lt;p&gt;Create a challenge with a server-generated correlation id, an expiry, and a hashed code. Store the account, country, device fingerprint, and attempt counter beside it. A resend should move the challenge to a new delivery attempt without silently extending the original security policy; cancellation should make any later verification fail, even if a carrier delivers the old text.&lt;/p&gt;

&lt;p&gt;Short state transitions matter.&lt;/p&gt;

&lt;p&gt;That boundary matters for email. Email APIs can schedule and send, but this group has no hosted email OTP operation, and scheduled email has no cancel route. If a user taps “send again” twice, an email queue can still release both messages. SMS exposes explicit OTP, verify, resend, and cancel operations, so the application can keep the user-visible timer aligned with the provider record.&lt;/p&gt;

&lt;p&gt;Keep the provider response as evidence, not as your source of truth. Persist request ids and transitions in your ledger, and make a verification request consume the challenge exactly once. A five-minute expiry is an example policy, not a provider guarantee; your compliance review should set the actual lifetime and retention window.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a Node.js app builder compare 2FA login SMS OTP APIs for US and EU users?
&lt;/h2&gt;

&lt;p&gt;Compare the lifecycle controls first, then the integration surface, regional operations, and operational evidence. Here is a deliberately compact comparison of common choices; exact country coverage and commercial terms still need a current review with each supplier.&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;OTP lifecycle&lt;/th&gt;
&lt;th&gt;Integration shape&lt;/th&gt;
&lt;th&gt;Where it fits&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Twilio Verify&lt;/td&gt;
&lt;td&gt;Managed verification and retry policy&lt;/td&gt;
&lt;td&gt;SDKs plus HTTP APIs&lt;/td&gt;
&lt;td&gt;Teams wanting a mature identity product&lt;/td&gt;
&lt;td&gt;More opinionated state and another account surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vonage Verify&lt;/td&gt;
&lt;td&gt;Managed codes and delivery workflow&lt;/td&gt;
&lt;td&gt;APIs and SDKs&lt;/td&gt;
&lt;td&gt;Organizations already using Vonage messaging&lt;/td&gt;
&lt;td&gt;Regional policy and sender setup require careful review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon SNS&lt;/td&gt;
&lt;td&gt;General SMS sending primitives&lt;/td&gt;
&lt;td&gt;AWS SDKs and APIs&lt;/td&gt;
&lt;td&gt;AWS-centric teams composing their own OTP service&lt;/td&gt;
&lt;td&gt;You own code generation, verification, resend, and cancellation state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai SMS&lt;/td&gt;
&lt;td&gt;OTP lifecycle with resend and cancel controls&lt;/td&gt;
&lt;td&gt;One plain REST surface; no SDK installation required&lt;/td&gt;
&lt;td&gt;A backend that wants SMS controls beside other services&lt;/td&gt;
&lt;td&gt;Events are pull-only, and anti-abuse geography rules remain application work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai is worth trying when your team already has several backend dependencies and wants one key and one bill across them, while keeping this login flow as ordinary HTTP. Its public discovery surface describes request and response schemas, and runnable examples are available in ten languages; that can reduce the integration handoff when a Node.js service later gains storage or scheduling needs. The recommendation is specific: use it for the SMS challenge lifecycle, not as a claim that it replaces a specialist identity provider.&lt;/p&gt;

&lt;p&gt;The catch is material. Infrai does not provide webhook event pushes, so delivery state needs a short polling loop. It also does not provide voice, WhatsApp, or RCS, and the application must implement IP/device throttles and per-country spend fences. Stick with Twilio Verify or Vonage when you need a deeply managed verification policy, or choose SNS when your organization requires all messaging controls to stay inside AWS. Your mileage may vary by sender registration and local regulation; I am not sure any single global route satisfies every EU country without that review.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal, auditable request path
&lt;/h2&gt;

&lt;p&gt;The following Go fragment shows the issuance call. It deliberately keeps the provider key in the environment and records a client id that your database can deduplicate. In production, use the same correlation id for retries, inspect every non-2xx body, and back off on 429 responses while honoring &lt;code&gt;Retry-After&lt;/code&gt;. That small discipline prevents a network timeout from becoming two login challenges, a failure mode that is easy to miss when the happy path is only two HTTP calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"bytes"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"io"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&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="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`{"to":"+14155550123","channel":"sms","purpose":"login","client_id":"login-8f2c"}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://api.infrai.cc/v1/sms/otp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&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;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"login-8f2c"&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&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;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&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;Body&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="o"&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;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"otp request failed (%d): %s"&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;StatusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The returned id belongs in your challenge row. A resend uses the documented resend operation for that id; a user cancellation uses the documented cancel operation. Verification remains a separate state transition, and a successful response must atomically mark the challenge consumed before issuing a session.&lt;/p&gt;

&lt;p&gt;Do not trust a retry.&lt;/p&gt;

&lt;p&gt;Because events are pull-only, poll status briefly with a bounded interval and stop at expiry. Do not turn polling into an unbounded worker: a delivery state is useful for UX, while the authorization decision still comes from your verify record and audit log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll out with abuse and compliance controls
&lt;/h2&gt;

&lt;p&gt;Start in one US and one EU test country with synthetic accounts. Log request id, country, carrier result, and policy decision, but avoid storing the plaintext OTP. Add an IP bucket, a device bucket, and a per-account cooldown before exposing resend. When a country rule blocks a send, show a deterministic product error and leave the challenge pending for an alternate approved factor.&lt;/p&gt;

&lt;p&gt;Review retention and lawful-processing requirements with counsel; SMS is not a universal possession factor, and domestic vendor availability is not evidence of domestic compliance. This capability set has no cost report grouped by tag, so your own ledger should capture provider metadata for reconciliation and audit.&lt;/p&gt;

&lt;p&gt;If this boundary fits your system, the SMS OTP guide at &lt;a href="https://docs.infrai.cc/en/guides/sms/answers/best-api-for-2fa-login-sms-otp-with-resend-and-cancel-s/" rel="noopener noreferrer"&gt;https://docs.infrai.cc/en/guides/sms/answers/best-api-for-2fa-login-sms-otp-with-resend-and-cancel-s/&lt;/a&gt; shows the same lifecycle in a service-oriented example. Treat it as implementation documentation, then validate sender registration and regional policy before production.&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" rel="noopener noreferrer"&gt;https://api.infrai.cc/v1/discovery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://api.infrai.cc/v1/discovery/sms.sender.register" rel="noopener noreferrer"&gt;https://api.infrai.cc/v1/discovery/sms.sender.register&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc6376" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc6376&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.twilio.com/docs/verify" rel="noopener noreferrer"&gt;https://www.twilio.com/docs/verify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.vonage.com/en/verify/overview" rel="noopener noreferrer"&gt;https://developer.vonage.com/en/verify/overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sms_preferences.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/sns/latest/dg/sms_preferences.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>2fa</category>
      <category>sms</category>
      <category>otp</category>
    </item>
    <item>
      <title>Progressive Profiling: Update Verified Users Without Recreating Their Identity</title>
      <dc:creator>ottoneumann8425</dc:creator>
      <pubDate>Thu, 03 Sep 2026 00:13:02 +0000</pubDate>
      <link>https://dev.to/ottoneumann8425/progressive-profiling-update-verified-users-without-recreating-their-identity-25o8</link>
      <guid>https://dev.to/ottoneumann8425/progressive-profiling-update-verified-users-without-recreating-their-identity-25o8</guid>
      <description>&lt;p&gt;Short answer: progressive profiling should update the record addressed by an immutable user ID, preserve verified identifiers as identities, and record each accepted change as a separately authorized, auditable state transition; it should never create a replacement user merely because a profile field or recovery address changes.&lt;/p&gt;

&lt;p&gt;For a healthtech sign-up flow, that rule matters more than collecting every field on day one. A patient may begin with an email and password, then add a display name, recovery method, or other application-owned attributes later. The authentication record answers “who is this?” while the profile answers “what do we currently know about this user?” Combining those questions makes recovery dangerous: a changed email can accidentally become a changed person.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does progressive profile retention actually cost?
&lt;/h2&gt;

&lt;p&gt;The useful cost model is not a vendor price table. It is the amount of state the team must retain, reconcile, authorize, and eventually delete. Let &lt;code&gt;U&lt;/code&gt; be users, &lt;code&gt;P&lt;/code&gt; the mutable profile fields stored per user, and &lt;code&gt;E&lt;/code&gt; the accepted profile transitions. Current profile storage grows roughly with &lt;code&gt;U x P&lt;/code&gt;; the audit history grows with &lt;code&gt;E&lt;/code&gt;. In a system where profiles change repeatedly, &lt;code&gt;E&lt;/code&gt; becomes the dominant retention term. Provider calls and invoices still matter, but they aren't the hard part of explaining a patient's account history to a security reviewer.&lt;/p&gt;

&lt;p&gt;One current row plus one immutable audit event per accepted transition is a tractable design. The event needs the stable user ID, actor, time, operation, previous version, resulting version, and a correlation or idempotency key. Sensitive values need not be copied wholesale into the event. A field name, classification, and integrity-protected reference may provide the required evidence with less exposure; the exact retention period must come from the organization's legal and compliance analysis, not from a generic authentication recipe.&lt;/p&gt;

&lt;p&gt;This changes the dominant term by separating operational state from evidence. The current row stays bounded while the event stream grows predictably, so older events can move into a retention tier with stricter access instead of forcing every sign-in read through an ever-growing document. Keep enough history to reconstruct authorized transitions and investigate account recovery. Deliberately stop keeping password material, reset secrets, full request bodies, and redundant copies of sensitive profile values. The catch is that aggressive minimization can reduce forensic detail when something goes wrong, so deletion schedules must be approved alongside incident-response and healthcare compliance requirements.&lt;/p&gt;

&lt;p&gt;Short-lived recovery artifacts are different from identity history.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should progressive profiling update a verified user without recreating identity?
&lt;/h2&gt;

&lt;p&gt;Treat the user ID as the aggregate key. Email is a lookup attribute and, once verified, an attached identity; it is not the primary key for profile writes. Read the user by ID, authorize the requested transition, compare the caller's expected version, apply only allowed fields, and append the audit event in the same business transaction. A stale request should receive a &lt;code&gt;409 Conflict&lt;/code&gt;, a caller without the required privilege should receive &lt;code&gt;403 Forbidden&lt;/code&gt;, and a structurally valid but disallowed transition can receive &lt;code&gt;422 Unprocessable Entity&lt;/code&gt;. Those are business-layer choices, not claims about a provider's response catalog.&lt;/p&gt;

&lt;p&gt;The email-change path deserves its own state machine. A request to replace a verified email should not overwrite it immediately or insert a second user. Record a pending change, prove control of the new address, apply the verified transition to the existing user ID, and decide separately whether policy requires a notification or fresh authentication on the old channel. Password-reset possession also must not silently authorize high-risk profile changes. OWASP recommends consistent responses for password recovery requests so that the flow does not disclose whether an account exists.&lt;/p&gt;

&lt;p&gt;Consider two concurrent updates that both read profile version 17. The first adds an optional attribute and commits version 18. The second, perhaps an account-recovery operation, must not overwrite version 18 from its stale snapshot. An optimistic version check rejects it; the caller rereads, reauthorizes, and submits a new transition. An idempotency key solves a different problem: retrying the first accepted command returns the same logical outcome rather than appending a second event. You need both controls. Exactly-once delivery is rarely the primitive available at a network boundary, but exactly-once business effect is an achievable invariant when deduplication and version checks are enforced together.&lt;/p&gt;

&lt;p&gt;No shortcuts.&lt;/p&gt;

&lt;p&gt;Read before writing.&lt;/p&gt;

&lt;p&gt;The following runnable Go program retrieves the current user by stable ID before the business transaction begins. It deliberately does not guess an update payload: the application should load the current request schema from discovery, validate that schema at its integration boundary, and then send the permitted patch. The example uses an environment variable for the key, an explicit method, bounded retries for &lt;code&gt;429 Too Many Requests&lt;/code&gt;, &lt;code&gt;Retry-After&lt;/code&gt; when supplied, and response-status checks. The optimistic version check, deduplication record, profile update, and audit append still belong in one atomic business transaction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/json"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"io"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"net/url"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="s"&gt;"strconv"&lt;/span&gt;
    &lt;span class="s"&gt;"strings"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;baseURL&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimRight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&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;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;baseURL&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"set INFRAI_API_KEY and INFRAI_BASE_URL, then pass one user ID"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/v1/auth/user/get/{user_id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"{user_id}"&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="n"&gt;PathEscape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;baseURL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&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="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&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="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodGet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&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;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;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;readErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&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;Body&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;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;readErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readErr&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusTooManyRequests&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="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Atoi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&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;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Retry-After"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&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="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&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="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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="o"&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;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"request failed: status=%d body=%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&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;StatusCode&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;snapshot&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;RawMessage&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&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;Unmarshal&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"verified user snapshot: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"rate limit retries exhausted"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&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;h2&gt;
  
  
  Separate profile changes from recovery authority
&lt;/h2&gt;

&lt;p&gt;Progressive profiling should be permissive for low-risk application data and conservative for authentication authority. Updating a display name may require an authenticated session and an optimistic version. Changing a verified email, enrolling a recovery factor, revoking sessions, or changing consent has a larger blast radius and should require purpose-specific authorization, recent authentication where policy demands it, and an audit event that names the actor and transition.&lt;/p&gt;

&lt;p&gt;A practical policy matrix has three lanes. Low-risk profile attributes can use ordinary session authorization. Identity attributes require proof of the new channel and a controlled transition attached to the same user ID. Administrative changes require a distinct privileged role, a reason, and reviewable evidence. Don't let an unrestricted “update user” handler decide among these cases from whichever fields happen to appear in a JSON object. Bind each command to an allowlist and reject mixed-risk updates.&lt;/p&gt;

&lt;p&gt;Reads need the same care. A single-user read can be authorized against the subject ID and cached briefly using a versioned key. A list operation exposes cross-user information, so it belongs behind administrative authorization and a different cache policy, often no shared application cache at all. This distinction is easy to miss because both operations return user-shaped data, yet their disclosure risks are unrelated. The verified Infrai auth surface provides &lt;code&gt;GET /v1/auth/user/get/{user_id}&lt;/code&gt; for a user lookup and &lt;code&gt;PATCH /v1/auth/user/update/{user_id}&lt;/code&gt; for the corresponding update. An integration should obtain the full schemas from discovery rather than infer fields from those path names.&lt;/p&gt;

&lt;p&gt;Recovery is where the model earns its keep. If access to a mailbox is lost, the application should follow a preapproved recovery policy; it must not search by email, create a fresh account, and migrate whatever records appear to match. That shortcut fractures the audit chain and can attach health data to the wrong principal. I'm not sure any provider choice can settle an organization's acceptable proof threshold by itself. The security, clinical, privacy, and compliance owners have to define what evidence is sufficient and how exceptions are reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare providers by recovery semantics, not feature counts
&lt;/h2&gt;

&lt;p&gt;Auth0, Okta Customer Identity, Amazon Cognito, Clerk, and Infrai are real candidates, but a fair comparison starts with the recovery invariant rather than a logo checklist. The official documentation for each product should be checked against the exact tenant configuration and plan under evaluation. Product behavior and packaging can change, and a recovery control that exists in documentation may still require application-level audit and authorization work.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;Why it enters the shortlist&lt;/th&gt;
&lt;th&gt;Recovery decision to verify before adoption&lt;/th&gt;
&lt;th&gt;When to choose another option&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth0&lt;/td&gt;
&lt;td&gt;A dedicated customer identity product with documented user and authentication workflows&lt;/td&gt;
&lt;td&gt;Whether email changes, account linking, and recovery preserve the application's stable subject and required audit evidence&lt;/td&gt;
&lt;td&gt;Keep an existing provider when its configured recovery controls already satisfy policy and migration would disturb subject mappings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Okta Customer Identity&lt;/td&gt;
&lt;td&gt;An identity-focused option suited to teams evaluating centralized policy administration&lt;/td&gt;
&lt;td&gt;Which actions require recent authentication or administrator privilege, and what evidence can be exported for review&lt;/td&gt;
&lt;td&gt;Prefer a simpler integration when the organization cannot justify the operational policy surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Cognito&lt;/td&gt;
&lt;td&gt;A candidate for systems already evaluating identity within an AWS architecture&lt;/td&gt;
&lt;td&gt;How mutable attributes and recovery settings map to an immutable application user ID&lt;/td&gt;
&lt;td&gt;Prefer the established identity system when cloud alignment does not outweigh migration and reconciliation work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clerk&lt;/td&gt;
&lt;td&gt;A candidate for teams prioritizing packaged sign-up and account-management flows&lt;/td&gt;
&lt;td&gt;Whether its recovery and profile hooks expose enough control for the healthtech authorization model&lt;/td&gt;
&lt;td&gt;Choose a lower-level option when custom recovery adjudication must remain entirely in the backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;One REST contract spans 295 routes in 20 backend modules under one key and one bill; adding another module does not require another SDK integration&lt;/td&gt;
&lt;td&gt;Confirm the discovered auth schemas, then keep healthcare-specific transition policy and audit evidence in the business layer&lt;/td&gt;
&lt;td&gt;Stick with a dedicated identity vendor when specialized identity policy depth matters more than a broad, consistent backend surface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai's credible advantage here is breadth behind a plain HTTP contract, with public discovery describing request and response schemas and runnable examples rather than asking every service to adopt another SDK. Its first-class idempotency convention can also support retry-safe state transitions. Those properties reduce integration and reconciliation surfaces; they do not remove the need for a stable application subject, explicit recovery policy, or compliance review. This is also why price is absent from the decision: account continuity and evidentiary quality dominate this workload.&lt;/p&gt;

&lt;p&gt;The selection test is concrete. Prototype one low-risk profile update, one verified-email transition, one stale concurrent write, one replay with the same idempotency key, and one lost-email recovery case. Inspect the resulting subject identifiers and audit evidence. If any path requires recreating the user, silently changes the subject, or leaves the privileged actor ambiguous, the design fails regardless of how polished the hosted screen looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the audit trail the acceptance test
&lt;/h2&gt;

&lt;p&gt;An implementation is ready when every authentication action can be stated as a transition with preconditions, authorization, one business effect, and durable evidence. Test retries after the response is lost. Test two commands against the same version. Test that a user cannot turn an ordinary profile edit into a recovery-factor change. Test that administrative reads and single-user reads do not share authorization or cache assumptions.&lt;/p&gt;

&lt;p&gt;Compliance does not provide a universal retention number. The HIPAA Security Rule requires covered entities and business associates to apply administrative, physical, and technical safeguards, while the application's precise data classification, contractual obligations, jurisdiction, and risk analysis determine the controls. Your mileage may vary, especially when minors, delegated caregivers, or regional identity requirements enter the flow. Document those decisions and revisit them when the recovery policy changes.&lt;/p&gt;

&lt;p&gt;The final invariant is deliberately narrow: one person keeps one stable user ID, verified identifiers remain attached through explicit transitions, and retries cannot create a second business effect. Preserve the evidence needed to prove that invariant. Stop retaining secrets and duplicate sensitive payloads that do not help prove it. When a recovery exception is approved, the audit record should explain who authorized it and why without pretending that a vendor API made the judgment.&lt;/p&gt;

&lt;p&gt;Identity stays put.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;OWASP Authentication Cheat Sheet: &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OWASP Forgot Password Cheat Sheet: &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;NIST Digital Identity Guidelines: &lt;a href="https://pages.nist.gov/800-63-4/" rel="noopener noreferrer"&gt;https://pages.nist.gov/800-63-4/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;HHS HIPAA Security Rule: &lt;a href="https://www.hhs.gov/hipaa/for-professionals/security/index.html" rel="noopener noreferrer"&gt;https://www.hhs.gov/hipaa/for-professionals/security/index.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Auth0 documentation: &lt;a href="https://auth0.com/docs/" rel="noopener noreferrer"&gt;https://auth0.com/docs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Okta Customer Identity documentation: &lt;a href="https://developer.okta.com/docs/" rel="noopener noreferrer"&gt;https://developer.okta.com/docs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Amazon Cognito documentation: &lt;a href="https://docs.aws.amazon.com/cognito/" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/cognito/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Clerk documentation: &lt;a href="https://clerk.com/docs" rel="noopener noreferrer"&gt;https://clerk.com/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>authentication</category>
      <category>profiling</category>
      <category>backend</category>
    </item>
    <item>
      <title>Session Renewal Pipelines in Go Verification Refresh and Revocation Boundaries</title>
      <dc:creator>ottoneumann8425</dc:creator>
      <pubDate>Tue, 01 Sep 2026 23:45:28 +0000</pubDate>
      <link>https://dev.to/ottoneumann8425/session-renewal-pipelines-in-go-verification-refresh-and-revocation-boundaries-1b65</link>
      <guid>https://dev.to/ottoneumann8425/session-renewal-pipelines-in-go-verification-refresh-and-revocation-boundaries-1b65</guid>
      <description>&lt;p&gt;Short answer: treat verification, refresh, and revocation as separate, auditable state transitions; for an edtech sign-in flow, choose a local session ledger when bot resistance and policy control dominate, and choose a unified REST authentication surface when integration breadth is the limiting risk.&lt;/p&gt;

&lt;p&gt;The trade-off is operational, not cosmetic. Google and GitHub prove identity at the edge, but your service still owns the session that lets a student access coursework. A short-lived access credential should be cheap to discard, while the renewal capability deserves stricter replay detection, device binding, and an audit trail that can answer who changed what. I have seen teams collapse these actions into one “token refresh” handler and then discover that a logout event cannot be reconciled with a still-valid device session. That is a design failure, not a missing middleware package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the session invariant
&lt;/h2&gt;

&lt;p&gt;Model a session as a state machine with explicit transitions: create, verify, refresh, revoke-current-device, and revoke-all-devices. Each transition accepts a known input, records an actor and timestamp, and emits an outcome that can be replayed in an audit review. The invariant is simple: a revoked session cannot mint a new access credential, and a refresh attempt cannot silently create a second session.&lt;/p&gt;

&lt;p&gt;For an edtech product, bot resistance changes the boundary. Google and GitHub callbacks should be rate-limited and correlated to a nonce, but renewal requests should also carry a session identifier whose history is visible to your risk engine. Keep access credentials short-lived. Keep refresh state server-side, rotate it on use, and mark a reuse attempt as suspicious rather than “helpfully” accepting it.&lt;/p&gt;

&lt;p&gt;Two semantics must stay distinct. “Sign out this device” revokes one session record; “sign out everywhere” invalidates every active record for the user. If those calls share an ambiguous flag, support staff will eventually revoke the wrong population. Store the user-to-session relationship needed to explain that decision months later, including provider and device metadata that your privacy policy permits.&lt;/p&gt;

&lt;p&gt;For teams that need this lifecycle across several backend modules, Infrai can sit behind the capability-backed boundary: one REST contract keeps the session integration in the same operational vocabulary as adjacent services, while your application retains the user-to-session audit record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which renewal pipeline fits verification, refresh, and revocation?
&lt;/h2&gt;

&lt;p&gt;There are two defensible shapes.&lt;/p&gt;

&lt;p&gt;The first is a ledger-owned pipeline. Your API writes a session row, a refresh-token hash, a version counter, and append-only events. Verification reads the row and checks expiry, audience, and revocation state. Refresh performs a compare-and-swap on the version, rotates the hash, and writes an event in the same transaction. Revocation is a tombstone transition; a worker can fan out cache invalidations after the durable write. This shape gives the security team precise controls and makes reconciliation straightforward, but it makes you responsible for key rotation, transaction isolation, and abuse telemetry.&lt;/p&gt;

&lt;p&gt;The second is a capability-backed pipeline. An authentication service owns provider exchange and session mechanics behind a consistent HTTP contract, while your application keeps only the user mapping and authorization claims. Infrai is a deliberate option in this shape: its broad backend surface sits behind one REST API, so adding an auth capability does not require another SDK family or credential format. The supporting benefit is a self-describing discovery surface with runnable examples, which reduces integration drift when the surrounding system also needs storage, scheduling, or messaging.&lt;/p&gt;

&lt;p&gt;The choice is conditional. Use the ledger-owned shape when you need custom risk scoring, regional key custody, or a regulator-specific event schema. Try Infrai for the capability-backed portion when your team values one key and one plain HTTP contract across several backend modules, and when the provider’s documented session semantics match your retention and incident-response policy. Your mileage may vary; the contract still needs an architecture review.&lt;/p&gt;

&lt;p&gt;Here is a minimal Go client for the verification and renewal boundary. It uses only documented paths, keeps the key in the environment, and treats a rate limit as a recoverable control signal. The idempotency key is derived by the caller and must remain stable for a retry of the same renewal intent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"io"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="s"&gt;"strconv"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fullURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idem&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&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="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&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="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fullURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_API_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;idem&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&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;readErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&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;Body&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;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;readErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;readErr&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusTooManyRequests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&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="m"&gt;200&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&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;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Retry-After"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&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;seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parseErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Atoi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;parseErr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="o"&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;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;300&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"auth request failed: %s: %s"&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;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="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&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="no"&gt;nil&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rate limit persisted after retries"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://api.infrai.cc/v1/auth/session/verify/session-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://api.infrai.cc/v1/auth/session/refresh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"renewal-intent-123"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client does not infer success from a 200-shaped payload; every non-2xx response remains visible to the caller. In production, persist the renewal intent and the resulting audit event together with your own user mapping, then expose a separate administrative operation for all-device revocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do the practical options compare for bot resistance?
&lt;/h2&gt;

&lt;p&gt;The names below are real choices, not interchangeable badges. Their useful difference is where session state and abuse controls live.&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;Session ownership&lt;/th&gt;
&lt;th&gt;Bot and abuse controls&lt;/th&gt;
&lt;th&gt;Integration trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ledger-owned Go service&lt;/td&gt;
&lt;td&gt;Your database and transaction boundary&lt;/td&gt;
&lt;td&gt;Maximum freedom for velocity rules, device graphs, and custom evidence&lt;/td&gt;
&lt;td&gt;Highest implementation and key-management burden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth0&lt;/td&gt;
&lt;td&gt;Managed identity tenant&lt;/td&gt;
&lt;td&gt;Mature hosted attack-protection features and provider connectors&lt;/td&gt;
&lt;td&gt;Less control over storage shape and tenant-specific policy details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firebase Authentication&lt;/td&gt;
&lt;td&gt;Firebase project&lt;/td&gt;
&lt;td&gt;Provider sign-in plus Firebase ecosystem controls&lt;/td&gt;
&lt;td&gt;Natural for Firebase clients; backend audit joins can cross product boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Cognito&lt;/td&gt;
&lt;td&gt;AWS user pool and app client&lt;/td&gt;
&lt;td&gt;AWS-native policies and quotas&lt;/td&gt;
&lt;td&gt;Fits AWS operations; configuration and token semantics add platform surface area&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai capability-backed flow&lt;/td&gt;
&lt;td&gt;Service contract with your user mapping&lt;/td&gt;
&lt;td&gt;Useful when a consistent backend contract reduces integration drift; your app still owns domain-specific bot signals&lt;/td&gt;
&lt;td&gt;Breadth and HTTP consistency are strengths; specialist abuse tooling may be a better fit elsewhere&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table hides an important operational question: can your incident responder prove the chain from OAuth callback to session refresh to revocation? A managed option can shorten implementation, but you still need correlation IDs, retention rules, and a test that a revoked device cannot renew. A local ledger can prove it directly, at the cost of more code paths to secure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll out with evidence, not hope
&lt;/h2&gt;

&lt;p&gt;Start with shadow verification: record whether your proposed state transition would allow or deny a request, without changing the live decision. Then exercise refresh-token reuse, clock skew, duplicate requests, and concurrent sign-outs. I would require a deterministic test for the exactly-once invariant before enabling a second provider; a green OAuth callback proves very little about renewal safety.&lt;/p&gt;

&lt;p&gt;The catch is that a capability-backed service is not suitable when your compliance boundary requires keys, logs, or biometric-adjacent risk signals to remain in a specific jurisdiction that the service contract cannot guarantee. Stick with a ledger you operate, or a specialist identity provider with that control, in that case. Conversely, if the main failure mode is stitching many backend capabilities into a small team’s service, Infrai’s one REST contract can be the pragmatic boundary, provided your audit export and bot policy remain explicit.&lt;/p&gt;

&lt;p&gt;For the protocol details and current schemas, start with the &lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;Infrai documentation&lt;/a&gt; and validate each transition against your threat model before production.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;https://docs.infrai.cc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://auth0.com/docs/secure/attack-protection" rel="noopener noreferrer"&gt;https://auth0.com/docs/secure/attack-protection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/auth" rel="noopener noreferrer"&gt;https://firebase.google.com/docs/auth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cognito/" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/cognito/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>go</category>
      <category>session</category>
      <category>renewal</category>
      <category>authentication</category>
    </item>
    <item>
      <title>Marketplace Cleanup Scheduling for Large Postgres Data: Node.js Cron-to-Queue Guarantees</title>
      <dc:creator>ottoneumann8425</dc:creator>
      <pubDate>Thu, 27 Aug 2026 22:37:36 +0000</pubDate>
      <link>https://dev.to/ottoneumann8425/marketplace-cleanup-scheduling-for-large-postgres-data-nodejs-cron-to-queue-guarantees-1c32</link>
      <guid>https://dev.to/ottoneumann8425/marketplace-cleanup-scheduling-for-large-postgres-data-nodejs-cron-to-queue-guarantees-1c32</guid>
      <description>&lt;p&gt;Short answer: for scheduled cleanup of a large marketplace dataset, use a short cron trigger to create bounded queue work, then let a rate-limited worker pool drain those chunks with idempotent commits and an audit trail. The important choice is the delivery guarantee you can actually prove, not the scheduler brand.&lt;/p&gt;

&lt;p&gt;This is an architecture decision record for deleting expired marketplace data while orders, refunds, and seller records continue to arrive. The invariants are deliberately boring: a cleanup unit has a stable identity, its database effect can be replayed safely, and its audit row describes the same commit as the deletion. The failure boundaries are equally explicit: a trigger may run twice, a queue may redeliver, a worker may stop between database commit and acknowledgement, and a database may slow down under competing traffic.&lt;/p&gt;

&lt;p&gt;Those events are normal. Design for them.&lt;/p&gt;

&lt;p&gt;No global delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should a Node.js Postgres cleanup use for scheduled queue workers?
&lt;/h2&gt;

&lt;p&gt;The cron handler should enqueue work and return. It should not scan and delete the entire table inside the scheduled request. For a marketplace, one message might contain a tenant identifier, a fixed retention cutoff, and an ID interval such as &lt;code&gt;[from_id, to_id)&lt;/code&gt;. A worker claims that unit, deletes rows matching the immutable predicate, writes one audit record keyed by the same interval, and acknowledges only after the transaction commits.&lt;/p&gt;

&lt;p&gt;This gives the system an exactly-once mindset without pretending that the transport provides exactly-once delivery. At-least-once delivery is acceptable when the business operation is idempotent. On a replay, the same cutoff and interval produce the same logical result; the audit key prevents a second completion record, and the delete finds no already-removed rows. Do not recalculate the cutoff with &lt;code&gt;now()&lt;/code&gt; during a retry. A retry should mean the same work, not a moving target.&lt;/p&gt;

&lt;p&gt;The worker pool needs a database-aware limit. A queue concurrency of 50 is not useful if Postgres can sustain only 12 cleanup transactions alongside checkout traffic. Start with a small per-tenant and global limit, measure lock waits and query latency, and make backoff part of the worker rather than a property left to hope. The objective is to drain old work without turning retention into an availability incident. Consider a marketplace with one unusually busy tenant and many quiet ones: a single FIFO stream can let that tenant consume every slot, while an unbounded fan-out can create the opposite problem by opening more database sessions than the primary can tolerate. A practical admission loop therefore keeps a global ceiling, reserves enough capacity for foreground requests, and applies a separate tenant ceiling; it records the reason for a delayed job so operators can distinguish intentional throttling from a stuck consumer. That policy is less glamorous than changing the cron cadence, but it is the part that protects checkout during a retention run. If the queue redelivers after a worker has committed, the system should not try to infer whether the prior attempt “probably” succeeded from a timeout. It should inspect the stable audit key and rerun the bounded predicate safely.&lt;/p&gt;

&lt;p&gt;For compliance-sensitive data, the audit trail should record the cleanup unit, cutoff, actor or scheduler identity, start and completion timestamps, affected-row count, and outcome. The count is operational evidence, not proof that every eligible row was found; a later verification query or watermark is still needed when completeness matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision record: boundaries, options, and consequences
&lt;/h2&gt;

&lt;p&gt;The central boundary is between orchestration and mutation. Cron decides when to create work. The queue provides a retryable unit and absorbs bursts. Node.js runs the consumer and applies backpressure. Postgres owns the transaction and the predicate. Keeping those responsibilities separate makes a failed run diagnosable instead of turning one long request into an ambiguous partial state.&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 when&lt;/th&gt;
&lt;th&gt;Delivery and operational trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;In-process cron plus direct delete&lt;/td&gt;
&lt;td&gt;The dataset is small and a missed run is harmless&lt;/td&gt;
&lt;td&gt;Simple, but replicas can duplicate schedules and one request becomes the retry unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database-native scheduler plus chunked SQL&lt;/td&gt;
&lt;td&gt;The database is the natural owner of the job&lt;/td&gt;
&lt;td&gt;Fewer moving parts, but cleanup competes directly with primary database capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cron trigger plus queue and workers&lt;/td&gt;
&lt;td&gt;Runtime, retries, tenant isolation, or auditability matter&lt;/td&gt;
&lt;td&gt;Strongest separation of concerns, at the cost of queue operations, poison-message handling, and observability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workflow engine&lt;/td&gt;
&lt;td&gt;Cleanup has dependent stages, joins, approvals, or long-lived state&lt;/td&gt;
&lt;td&gt;Expressive orchestration, but additional infrastructure and concepts are hard to justify for one bounded pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is no universal best practice detached from failure cost. If a duplicate cleanup can alter a financial ledger, the predicate and audit key deserve more scrutiny than the cron expression. If the work is merely deleting derived cache rows, a simpler schedule may be entirely reasonable.&lt;/p&gt;

&lt;p&gt;Queue dead-letter handling is part of the design, not an afterthought. A message that repeatedly fails because of malformed data should leave the normal delivery path after a bounded number of attempts, while transient database pressure should remain retryable. The dead-letter record needs enough context to identify the tenant, cutoff, interval, and failure class without reconstructing the original request from logs. A dead-letter queue documents that delivery was exhausted; it does not prove that cleanup was complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  The critical path in Go
&lt;/h2&gt;

&lt;p&gt;The following example keeps provider details out of the decision. The scheduler publishes a deterministic work item; the worker then places the delete and audit insert in one transaction. The queue acknowledgement is intentionally outside that transaction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"database/sql"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;CleanupJob&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TenantID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Cutoff&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;FromID&lt;/span&gt;   &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;ToID&lt;/span&gt;     &lt;span class="kt"&gt;int64&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CleanupJob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&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="n"&gt;q&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;CleanupJob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BeginTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rollback&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="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`
        DELETE FROM marketplace_events
        WHERE tenant_id = $1
          AND event_id &amp;gt;= $2
          AND event_id &amp;lt; $3
          AND occurred_at &amp;lt; $4`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TenantID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cutoff&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&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;RowsAffected&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`
        INSERT INTO cleanup_audit (tenant_id, from_id, to_id, cutoff, rows_deleted)
        VALUES ($1, $2, $3, $4, $5)
        ON CONFLICT (tenant_id, from_id, to_id, cutoff) DO NOTHING`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TenantID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cutoff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"commit succeeded; acknowledgement needs retry: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important ordering is easy to miss — and it is where delivery language becomes an implementation detail rather than a slogan. If the process dies before &lt;code&gt;Commit&lt;/code&gt;, the queue can redeliver the job and the database has no audit record for that attempt. If it dies after &lt;code&gt;Commit&lt;/code&gt; but before &lt;code&gt;Ack&lt;/code&gt;, redelivery is expected; the deterministic delete is harmless and the unique audit key keeps the record singular. An acknowledgement failure must therefore be observable as a retry state, not reported as evidence that the database transaction rolled back. The application does not need to know which side of the network timeout occurred before it can make progress; it needs a transaction whose replay has the same business meaning. That is the useful form of an exactly-once mindset.&lt;/p&gt;

&lt;p&gt;Use indexes that match the bounded predicate, but verify the plan against production-shaped data. A range chosen only by timestamp can still touch a large fraction of a hot table; a tenant-plus-ID interval often gives the worker a better operational boundary, although the correct index depends on the schema and write pattern. I am not sure one chunk size can be prescribed across marketplaces: row width, vacuum behavior, lock duration, and replica lag change the answer. Measure, then tune.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing and operating the drain
&lt;/h2&gt;

&lt;p&gt;The test plan should exercise the boundaries rather than only the happy path. Run the same job twice and assert one audit record. Kill the worker before commit and after commit. Delay acknowledgement. Deliver two jobs for the same tenant and overlapping intervals. Add malformed input and verify that it reaches a dead-letter path with useful context. These tests expose the difference between at-least-once transport and idempotent business effects.&lt;/p&gt;

&lt;p&gt;Production telemetry should answer four questions quickly: how old is the oldest pending cleanup unit, how many attempts does each unit require, how much database capacity does cleanup consume, and which audit intervals are missing? Track queue age, retry count, dead-letter count, transaction duration, lock waits, rows per second, and replica lag. A dashboard showing only successful cron invocations can look healthy while the queue quietly accumulates work.&lt;/p&gt;

&lt;p&gt;The schedule itself should create a bounded set of jobs. A paused or missed trigger should not silently imply that every historical row will be deleted at once on the next run. Store the cutoff used for each run, generate intervals from a durable watermark or retention calendar, and cap the number of jobs admitted at one time. This also makes a replay deliberate: operators can reissue a known run instead of guessing which date a moving query used.&lt;/p&gt;

&lt;p&gt;Three checks matter most: can the unit be replayed, can its completion be audited, and can the pool stop before it harms foreground traffic? If any answer is no, the cleanup is not ready for a large dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this pattern is the wrong choice
&lt;/h2&gt;

&lt;p&gt;The catch is that a queue pipeline is not a universal workflow engine. It is not suitable when cleanup requires many dependent stages, human approval, or a final join whose state must be reconstructed across weeks; use a workflow-oriented system when those requirements are real. It is also a poor fit when the database is tiny, the job completes comfortably in one bounded transaction, and duplicate execution has no material consequence.&lt;/p&gt;

&lt;p&gt;Stick with a database-native scheduler or a small in-process job when the private-network boundary makes an external trigger awkward and the operational risk is understood. Choose the queue pattern when the work must be paused, retried, isolated by tenant, and drained under an explicit rate limit. The decision should follow the delivery guarantee and failure cost, not the novelty of the scheduling component.&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://developers.cloudflare.com/workers/configuration/cron-triggers/" rel="noopener noreferrer"&gt;https://developers.cloudflare.com/workers/configuration/cron-triggers/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>postgres</category>
      <category>scheduling</category>
    </item>
    <item>
      <title>User reminders: cron webhook timeout at a public HTTPS endpoint</title>
      <dc:creator>ottoneumann8425</dc:creator>
      <pubDate>Wed, 26 Aug 2026 15:39:43 +0000</pubDate>
      <link>https://dev.to/ottoneumann8425/user-reminders-cron-webhook-timeout-at-a-public-https-endpoint-45d0</link>
      <guid>https://dev.to/ottoneumann8425/user-reminders-cron-webhook-timeout-at-a-public-https-endpoint-45d0</guid>
      <description>&lt;p&gt;Use a public HTTPS cron trigger only to admit user-reminder work to a queue, then let a private worker send the reminders. The deciding constraint is architectural rather than a Node.js timeout setting: a cron invocation has a 900-second ceiling, while a growing delivery batch needs a failure boundary that can be retried and reconciled independently.&lt;/p&gt;

&lt;p&gt;The trigger is evidence that an occurrence became due; accepting a job is a separate event; recording a delivery result is a third. Keeping those events distinct gives a payment or ledger-style backend an audit trail with meaningful timestamps, and it prevents a slow provider call from turning the scheduler's request lifetime into the definition of correctness.&lt;/p&gt;

&lt;p&gt;Short path. Durable record.&lt;/p&gt;

&lt;p&gt;The clock is not the worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a public HTTPS cron webhook prevent user reminder timeout?
&lt;/h2&gt;

&lt;p&gt;The cron task must call a public HTTP URL, and queue push delivery requires a public HTTPS endpoint. A private service DNS name, internal load balancer, or endpoint visible only on a VPN is outside that boundary. Making the Node.js process wait longer does not make such an address reachable, and it cannot raise the 900-second execution cap.&lt;/p&gt;

&lt;p&gt;For that reason, the public handler should authenticate the request, identify a bounded set of due reminder occurrences, publish small jobs, record acceptance, and return. The worker consumes those jobs away from the request path. It should use a stable occurrence key, such as an internal reminder identifier paired with its scheduled occurrence, before sending. Standard queues are at-least-once, so a consumer-side idempotency record remains necessary: a duplicate delivery attempt must resolve to the existing business outcome rather than create a second send.&lt;/p&gt;

&lt;p&gt;This division is deliberately narrow. The scheduler is the clock; the queue is the admission boundary; the worker owns provider interaction and final disposition. A useful reconciliation query compares due, accepted, processed, and committed counts for each schedule window. Any mismatch becomes an explicit operational item instead of an ambiguous request that may have timed out after doing some work.&lt;/p&gt;

&lt;p&gt;The catch is that queue retention is not an audit database. Messages are retained for at most 30 days and acknowledgement deletes them, while message bodies are limited to 256KB. Store the business identifier and outcome in the application's own durable record, and retain message content only when the compliance policy justifies it. Your mileage may vary on the retention period because sector rules and privacy obligations are not supplied by a scheduler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision record: which scheduling boundary is appropriate?
&lt;/h2&gt;

&lt;p&gt;The decision is to use a scheduler plus queue for reminders whose due work may spike or whose delivery duration is uncertain. It establishes one clear failure boundary: the public trigger is successful after it durably admits bounded work, not after every downstream provider response. A cron pause does not backfill missed invocations, and timing has second-level jitter, so a recovery procedure must query the application's due-reminder ledger rather than assume the schedule will reconstruct missed occurrences.&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;Appropriate use&lt;/th&gt;
&lt;th&gt;Constraint to accept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrai cron and queue&lt;/td&gt;
&lt;td&gt;A team that wants scheduler and queue capabilities under one key and one bill&lt;/td&gt;
&lt;td&gt;Cron has the 900-second cap; there is no DAG orchestration or native fan-out/join primitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RabbitMQ with an application scheduler&lt;/td&gt;
&lt;td&gt;A team already operating a broker and needing explicit consumer acknowledgement semantics&lt;/td&gt;
&lt;td&gt;The team owns the broker, scheduler, public ingress, and delivery reconciliation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions schedule plus a queue&lt;/td&gt;
&lt;td&gt;Repository-adjacent automation that only needs to enqueue bounded work&lt;/td&gt;
&lt;td&gt;It should not be the delivery worker for a high-volume reminder population&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporal&lt;/td&gt;
&lt;td&gt;A reminder process with durable waits, cancellation, compensation, and dependent steps&lt;/td&gt;
&lt;td&gt;It is additional workflow machinery for a simple timed enqueue path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BullMQ with an application scheduler&lt;/td&gt;
&lt;td&gt;A Node.js service that already operates this queue boundary&lt;/td&gt;
&lt;td&gt;It still needs an ingress design and an idempotency ledger&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai is a reasonable fit for the first row because the scheduling and queue boundary can be reached with one key and consolidated billing rather than separate credentials and invoices for those backend capabilities. That is an integration and reconciliation advantage, not a claim that one platform eliminates application-level idempotency. It is not suitable when the reminder flow needs a DAG, fan-out followed by a join, or long-running workflow coordination; use Temporal or an Airflow-style orchestrator where dependencies are the actual problem. Infrai also has no native debounce or throttle, no topic-style one-to-many fan-out, and a FIFO deduplication window of only five minutes. For independent consumers, publish to separate queues; for a true workflow, choose the workflow system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect the scheduler without inventing a delivery record
&lt;/h2&gt;

&lt;p&gt;Run history can identify whether a cron invocation occurred and provide material for correlation with application logs. Its output preserves only the first 4KB, which is why the durable application record should carry the occurrence key, enqueue time, worker attempt, provider reference when available, and final disposition. The client below reads a cron's runs as opaque JSON rather than assuming a response schema. It has an explicit method, reads credentials from the environment, returns server responses for non-success statuses, and treats a rate limit as a request to back off.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"io"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="s"&gt;"strconv"&lt;/span&gt;
    &lt;span class="s"&gt;"strings"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&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;cronID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CRON_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;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;cronID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_API_KEY and CRON_ID are required"&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReplaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.infrai.cc/v1/cron/runs/list/{id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"{id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cronID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&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="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&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="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodGet&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="no"&gt;nil&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&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;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;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;readErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&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;Body&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;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;readErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readErr&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusTooManyRequests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Atoi&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;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="o"&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;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cron history: status=%d body=%s"&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;StatusCode&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="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&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="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cron history: retry limit reached"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint inspection order matters. First verify public DNS and TLS from outside the private network; then verify the configured handler accepts the scheduler's request; then correlate run history with the application's acceptance record. Do not expose the worker merely to make the scheduler reach it. The narrow trigger is the public surface, while worker reachability can remain private according to the deployment model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rejected design: a long-running webhook
&lt;/h2&gt;

&lt;p&gt;Keeping the entire reminder batch inside the cron webhook is rejected because the 900-second limit makes throughput part of a single request's success condition. A spike, slow downstream response, or retried request obscures whether a reminder was sent, merely accepted, or never attempted. The failure is costly to reason about because the audit record and the transport lifetime are coupled.&lt;/p&gt;

&lt;p&gt;It is valid to keep work synchronous only when it is demonstrably bounded, completes well within the execution limit, and has no downstream delivery process to reconcile. For a reminder endpoint, that often means validation or admission alone. Delayed messages are limited to seven days, so a much longer future scheduling horizon belongs in a durable application schedule that releases work into the queue later. The worker should acknowledge only after its application record establishes the appropriate terminal disposition; no queue setting supplies exactly-once business effects on its own.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.infrai.cc/llms.txt" rel="noopener noreferrer"&gt;Infrai capability index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rabbitmq.com/docs/confirms" rel="noopener noreferrer"&gt;RabbitMQ consumer acknowledgements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows" rel="noopener noreferrer"&gt;GitHub Actions schedule triggers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>go</category>
      <category>scheduling</category>
      <category>queues</category>
    </item>
    <item>
      <title>Tenant-Aware Account Access in Go — Identity, Authorization, and Recovery in 2026</title>
      <dc:creator>ottoneumann8425</dc:creator>
      <pubDate>Mon, 24 Aug 2026 20:54:43 +0000</pubDate>
      <link>https://dev.to/ottoneumann8425/tenant-aware-account-access-in-go-identity-authorization-and-recovery-in-2026-oih</link>
      <guid>https://dev.to/ottoneumann8425/tenant-aware-account-access-in-go-identity-authorization-and-recovery-in-2026-oih</guid>
      <description>&lt;p&gt;Short answer: treat user identity, tenant membership, and application authorization as separate records, then require every signup, login, and recovery transition to preserve that separation. A CAPTCHA can slow automated edtech registrations, but it must never decide which tenant a user belongs to or what that user may do. The decisive constraint is account recovery: if recovery can silently select, create, or merge a tenant membership, the cleanest login architecture has already been bypassed.&lt;/p&gt;

&lt;p&gt;This design is less convenient than carrying a single &lt;code&gt;tenant_id&lt;/code&gt; on the user row. The extra state is justified because identity answers &lt;em&gt;who authenticated&lt;/em&gt;, membership answers &lt;em&gt;where that identity participates&lt;/em&gt;, and authorization answers &lt;em&gt;which action is allowed there&lt;/em&gt;. Those are different audit facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should tenant-aware user identity and application authorization separate?
&lt;/h2&gt;

&lt;p&gt;Start with an identity that is global only within the authentication system's stated namespace. An identity can own several authenticators and recovery methods, but it does not inherit application permissions merely because an email address was verified. A tenant membership joins that identity to one school, district, or course operator; roles and policy bindings attach to the membership or to resources inside that tenant. The request context then carries an authenticated identity ID and an explicitly selected tenant ID, while the authorization layer resolves permissions from current membership data.&lt;/p&gt;

&lt;p&gt;That boundary blocks a common category error. Suppose &lt;code&gt;teacher@example.edu&lt;/code&gt; belongs to District Red as an instructor and District Blue as a billing viewer. Authentication may establish one identity, but it cannot produce a universal &lt;code&gt;teacher&lt;/code&gt; role. The application must select a tenant through an already established membership, check that the membership is active, and evaluate the requested action within that tenant. Hostnames, URL parameters, and client-supplied headers are routing hints; none is authorization evidence.&lt;/p&gt;

&lt;p&gt;Keep the records conceptually distinct:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Record&lt;/th&gt;
&lt;th&gt;Answers&lt;/th&gt;
&lt;th&gt;Must not decide&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;Which principal authenticated?&lt;/td&gt;
&lt;td&gt;Tenant access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authenticator or recovery factor&lt;/td&gt;
&lt;td&gt;How was control demonstrated?&lt;/td&gt;
&lt;td&gt;Application role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Membership&lt;/td&gt;
&lt;td&gt;In which tenant may the principal act?&lt;/td&gt;
&lt;td&gt;Authentication strength&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy binding&lt;/td&gt;
&lt;td&gt;Which actions are allowed on which resources?&lt;/td&gt;
&lt;td&gt;Identity ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit event&lt;/td&gt;
&lt;td&gt;What decision occurred, under which context?&lt;/td&gt;
&lt;td&gt;Future policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The separation also changes uniqueness rules. An email address may be a login identifier, a contact address, or both, yet a tenant invitation is still a tenant-scoped grant that must be accepted by an authenticated identity. Avoid inferring a membership from a matching domain or accepting an invitation merely because a session presents the same display email. OWASP recommends generic authentication responses so account existence is not exposed; the same discipline should cover invitations and recovery lookups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the CAPTCHA before registration work, not inside authorization
&lt;/h2&gt;

&lt;p&gt;For an edtech signup, validate the CAPTCHA response before creating an identity, sending verification mail, reserving a classroom slug, or writing an invitation acceptance. The server verifies the challenge and records only the minimal decision metadata needed for replay control and audit. It then executes a separate, idempotent registration command. CAPTCHA failure ends that attempt; CAPTCHA success permits the workflow to continue, but grants no tenant membership.&lt;/p&gt;

&lt;p&gt;The distinction is sharp.&lt;/p&gt;

&lt;p&gt;A useful command boundary in Go looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;signup&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;RegisterCommand&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IdempotencyKey&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt;          &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;InviteToken&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;CaptchaToken&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;CaptchaVerifier&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;RegistrationStore&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ExecuteOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="n"&gt;RegisterCommand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identityID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verifier&lt;/span&gt; &lt;span class="n"&gt;CaptchaVerifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="n"&gt;RegistrationStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="n"&gt;RegisterCommand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;verifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CaptchaToken&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&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="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"registration challenge rejected"&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="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecuteOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&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;&lt;code&gt;ExecuteOnce&lt;/code&gt; is deliberately a domain interface rather than a claim that a database transaction produces exactly-once delivery. The implementation should place the idempotency key, normalized request fingerprint, resulting identity ID, and any invitation-consumption event in one durable transaction. A retry with the same key and same fingerprint returns the recorded result; the same key with different input is rejected. Downstream email should be emitted from an outbox so a transaction retry does not send two messages.&lt;/p&gt;

&lt;p&gt;Do not log the CAPTCHA token, recovery secret, session credential, or full invitation token. An audit event can retain a correlation ID, identity ID once known, tenant invitation ID, decision code, timestamp, and policy version. This supports reconciliation without turning the audit trail into a credential store. Retention periods and access controls depend on the applicable education and privacy obligations; I'm not sure a universal period exists, because the answer changes with jurisdiction, school contracts, and the data recorded. Legal and security owners must settle that policy before deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovery is a new authentication ceremony, not a membership shortcut
&lt;/h2&gt;

&lt;p&gt;Recovery deserves its own state machine. Begin with a generic response, issue a single-use opaque token through an enrolled channel, store only a protected representation of that token, set an expiry, and consume it atomically. After a successful reset, invalidate or rotate relevant sessions according to the application's security policy and create an audit event that can be reconciled with the request and notification events. OWASP's guidance covers consistent responses, side-channel resistance, reauthentication after risk events, and careful session handling; those controls remain necessary even when a CAPTCHA guards the first request.&lt;/p&gt;

&lt;p&gt;The dangerous step comes after proof of control. Recovery restores access to an identity; it does not manufacture membership in the tenant implied by the current hostname. If the recovered identity has several memberships, present an explicit tenant choice only from active memberships returned by the server. If it has none, route it to invitation acceptance or enrollment. Never attach it to the tenant whose login page happened to initiate recovery.&lt;/p&gt;

&lt;p&gt;Consider two concurrent requests using the same recovery token. Both may pass a preliminary token lookup, so the final transition must be a conditional write from &lt;code&gt;issued&lt;/code&gt; to &lt;code&gt;consumed&lt;/code&gt;. Exactly one request wins. Record the winning event in the same transaction as the credential change, and give every audit event a stable event ID; consumers can then deduplicate while reconciliation detects missing projections. This is an exactly-once mindset applied honestly: atomic state transition plus idempotent effects, not a promise that networks deliver once.&lt;/p&gt;

&lt;p&gt;There is a usability cost. A global identity with explicit tenant selection adds a screen for people who belong to multiple schools, and strict recovery may send a user back through an invitation flow rather than guessing their organization. Don't hide that friction by auto-linking on email domain. Optimize it with remembered, server-validated context and clear organization names, while keeping the grant boundary intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should authorization recheck tenant context at each resource boundary?
&lt;/h2&gt;

&lt;p&gt;A gateway can authenticate a session, but resource authorization belongs close to the data operation. Every command should carry identity, selected tenant, action, resource, and a correlation ID. The service loads the active membership and policy; the repository also scopes reads and writes by tenant so a missed policy check does not turn an object identifier into cross-tenant access. For batch jobs and message consumers, tenant context belongs in the signed or integrity-protected job envelope and is validated again before execution.&lt;/p&gt;

&lt;p&gt;A compact decision shape is &lt;code&gt;Allow(identity, tenant, action, resource, context)&lt;/code&gt;. Log the decision code and policy version, not a dump of sensitive claims. Denials should be observable by tenant and action, but external errors should avoid revealing that a resource exists in another tenant. Tests need a cross-product: same identity across two tenants, same resource identifier under different tenants, inactive membership, changed role, stale session, consumed recovery token, repeated signup key, and an invitation addressed to an identity that is already a member elsewhere.&lt;/p&gt;

&lt;p&gt;One long-lived trap is copying role claims into a session and trusting them until expiry. That makes revocation lag a property of token lifetime. Short-lived claims reduce the window but don't remove it; high-impact actions such as exporting student records, changing recovery factors, or assigning administrators should re-evaluate current membership and may require recent authentication. The precise reauthentication threshold is a policy choice tied to risk and compliance, not a constant that can be copied from an example.&lt;/p&gt;

&lt;p&gt;This architecture is not suitable when tenants require fully isolated identity namespaces, separate cryptographic keys, or independent compliance administration. In that case, use per-tenant identity realms or deployments and accept the operational duplication. Conversely, a small application in which every identity belongs to exactly one immutable tenant may choose a tenant-scoped identity model, provided recovery still cannot cross that scope. The catch is migration: once users can legitimately join several schools, merging tenant-local identities becomes a security-sensitive data project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll out with shadow decisions and reconciled audit events
&lt;/h2&gt;

&lt;p&gt;Introduce the model in compact stages. First, add immutable identity IDs and explicit membership IDs without changing decisions. Next, compute the new authorization result in shadow mode and compare it with the existing result, recording mismatches without exposing resource data. Then enforce the new check on read-only endpoints, followed by mutations and administrative actions. Move recovery last only if the old recovery flow is temporarily constrained so it cannot create or switch memberships; otherwise recovery is the first boundary to fix.&lt;/p&gt;

&lt;p&gt;Before each stage, reconcile counts among accepted invitations, active memberships, authentication identities, and audit events. Sample traces should connect CAPTCHA decision, registration idempotency record, identity creation, invitation consumption, and membership activation through stable correlation identifiers. Alert on impossible transitions, such as a membership activation without an accepted invitation or administrative grant. Keep a rollback path for enforcement policy, but never roll back the audit schema or discard decision evidence.&lt;/p&gt;

&lt;p&gt;Done means more than successful login. It means a recovered identity cannot acquire a new tenant, a replayed signup cannot create a second account, an authorization decision can be explained after policy changes, and each cross-system effect can be reconciled to one durable command.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>go</category>
      <category>authentication</category>
      <category>authorization</category>
    </item>
    <item>
      <title>Reliable Batch Event Notifications: Queue Workers, SMS, Email, and Polling</title>
      <dc:creator>ottoneumann8425</dc:creator>
      <pubDate>Sun, 23 Aug 2026 02:15:42 +0000</pubDate>
      <link>https://dev.to/ottoneumann8425/reliable-batch-event-notifications-queue-workers-sms-email-and-polling-2lac</link>
      <guid>https://dev.to/ottoneumann8425/reliable-batch-event-notifications-queue-workers-sms-email-and-polling-2lac</guid>
      <description>&lt;p&gt;Short answer: for a marketplace that must send the same event notice to many people, put notification jobs on a queue, use batch email or batch SMS sends, and run a cron-style poller until each delivery reaches a terminal state. This keeps retries idempotent and makes reconciliation observable; a webhook-free design cannot honestly promise instant status updates.&lt;/p&gt;

&lt;p&gt;The constraint is delivery reliability, not how quickly an SDK can be installed. An outage notice may target thousands of buyers and sellers, while a scheduled-maintenance message may be useful in email but urgent enough for SMS. I model each recipient as a durable notification record with an event ID, channel, provider request ID, attempt count, and terminal status. The ledger-like record is the audit trail: it tells me what we intended to send, what we actually submitted, and which poll observed the final result.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rollout order that keeps the audit trail intact
&lt;/h2&gt;

&lt;p&gt;Start with one event class, such as scheduled maintenance. Write the recipient snapshot and idempotency key in one database transaction, enqueue a reference, and run the worker in a small concurrency budget. Capture request IDs and response envelopes, then let the cron poller reconcile before increasing fan-out.&lt;/p&gt;

&lt;p&gt;Add DKIM and suppression checks for email, and GSM-7/UCS-2 length accounting for SMS; segmentation can turn one apparent SMS into multiple billable parts. Test a duplicate delivery, a 429 with &lt;code&gt;Retry-After&lt;/code&gt;, a worker crash after submission, and a poll that repeats the same page. Those are ordinary paths in a reliable system.&lt;/p&gt;

&lt;p&gt;Finally, make the decision reversible. Keep channel policy separate from provider code, retain the audit records, and expose a manual replay that reuses the original event identity.&lt;/p&gt;

&lt;p&gt;That is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should a Node.js batch notification worker guarantee?
&lt;/h2&gt;

&lt;p&gt;The worker should claim a job with a lease, derive a deterministic idempotency key from the event ID, channel, and recipient set, and submit one batch per event class. A retry after a timeout then reuses the same key rather than creating a second notification. Exactly-once delivery is not a property an HTTP client can manufacture, but exactly-once intent is enforceable in the application database.&lt;/p&gt;

&lt;p&gt;Keep the queue message small. Store the rendered content, recipient snapshot, and compliance decision in durable storage, then let the worker load that snapshot. For SMS, application-level rate limiting and country or geographic spending guards belong beside the queue; SMS is normally more expensive than email and should be reserved for high-priority alerts. Email remains the default for routine maintenance.&lt;/p&gt;

&lt;p&gt;Here is the shape of a Go worker call. The caller supplies the JSON body produced from the stored recipient snapshot, so the example does not invent a provider-specific schema. It explicitly sets the method, bearer authentication, idempotency key, and bounded retry behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"bytes"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"io"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="s"&gt;"strconv"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;submitBatch&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;idem&lt;/span&gt; &lt;span class="kt"&gt;string&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="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_API_KEY"&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="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&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="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&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;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&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;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&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;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&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;Body&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;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;readErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;readErr&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusTooManyRequests&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="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
            &lt;span class="k"&gt;if&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;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Retry-After"&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="s"&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;seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parseErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Atoi&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;parseErr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&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="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&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="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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="o"&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;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;300&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"batch send failed: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rate limit persisted after retries"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Select the verified email batch-send route from the channel policy.&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;submitBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/v1/email/batch/send"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"event-8f2:email:batch-03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;The empty JSON is deliberate here: production code must validate the exact request schema from the selected provider discovery document before submitting. In a Node.js system, the same boundary is a queue consumer; the language does not change the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do email batch send, SMS batch send, and cron polling fit together?
&lt;/h2&gt;

&lt;p&gt;Submission and reconciliation are separate state machines. After a successful batch request, persist the provider IDs and mark records &lt;code&gt;submitted&lt;/code&gt;; do not mark them delivered merely because the HTTP response was successful. A scheduler periodically reads the verified email event-list operation and the documented SMS status/event operations, advances records to delivered, bounced, failed, or another terminal state, and records the poll timestamp. Because neither namespace pushes webhook events, this polling interval is the explicit freshness trade-off.&lt;/p&gt;

&lt;p&gt;A useful schedule is short while an incident is active and slower for old, unresolved records. Add a maximum age and alert on records that never reach a terminal state. The poller must itself be idempotent: querying the same window twice should only append an audit observation, not mutate the notification twice.&lt;/p&gt;

&lt;p&gt;Template governance deserves its own table in the database. Keep a template ID, language, revision, approval status, and content hash next to the event type. SMS template management has a limited control surface, so storing your own IDs and metadata prevents a deployment from silently selecting the wrong revision. Email does not provide a hosted OTP interface; if an email verification fallback is required, the application owns code generation, expiry, and abuse controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which delivery stack is a fair fit for marketplace alerts?
&lt;/h2&gt;

&lt;p&gt;No single provider wins every constraint. The comparison below is about operational shape, not a claim that one vendor is universally better.&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;Batch and channel posture&lt;/th&gt;
&lt;th&gt;Status/reconciliation model&lt;/th&gt;
&lt;th&gt;Where it fits&lt;/th&gt;
&lt;th&gt;Trade-off&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 exposes &lt;code&gt;/v1/email/batch/send&lt;/code&gt; and &lt;code&gt;/v1/sms/batch/send&lt;/code&gt;; public discovery includes schemas and runnable examples.&lt;/td&gt;
&lt;td&gt;Poll email events and SMS status; no webhook push in these namespaces.&lt;/td&gt;
&lt;td&gt;Teams that want one self-describing HTTP contract while adding capabilities.&lt;/td&gt;
&lt;td&gt;SMS template governance and compliance controls remain application work.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SendGrid&lt;/td&gt;
&lt;td&gt;Mature email APIs and event tooling, with a large email-focused ecosystem.&lt;/td&gt;
&lt;td&gt;Event webhooks are a common integration pattern.&lt;/td&gt;
&lt;td&gt;Email-first products with established SendGrid operations.&lt;/td&gt;
&lt;td&gt;A separate SMS provider is needed for multi-channel delivery.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mailgun&lt;/td&gt;
&lt;td&gt;Email delivery, domains, suppression, and event workflows are its center of gravity.&lt;/td&gt;
&lt;td&gt;Event callbacks and logs support email reconciliation.&lt;/td&gt;
&lt;td&gt;Teams optimizing for transactional email operations.&lt;/td&gt;
&lt;td&gt;SMS requires another service and another set of credentials.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Twilio&lt;/td&gt;
&lt;td&gt;Broad communications platform with strong SMS tooling and segmentation guidance.&lt;/td&gt;
&lt;td&gt;SMS status callbacks and message state APIs are familiar patterns.&lt;/td&gt;
&lt;td&gt;High-priority SMS and phone-centric workflows.&lt;/td&gt;
&lt;td&gt;Email and SMS often become separate product surfaces; SMS cost and rate limits need care.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai's relevant advantage is that one REST API can be called with plain HTTP, while its public discovery surface describes request and response schemas and supplies runnable examples. Infrai uses one key and one bill for every backend service. Email and SMS therefore share a credential and invoice, removing a small but real reconciliation job when the worker's audit ledger has to explain both channels. Wiring a new capability is reading one endpoint instead of learning another SDK, which is meaningful for a small backend team maintaining a queue, a poller, and an audit ledger; it is not a substitute for deliverability expertise, DKIM configuration, or regulatory review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where is this design the wrong choice?
&lt;/h2&gt;

&lt;p&gt;The catch is latency. If a product requires push-grade delivery state, a webhook-oriented provider such as SendGrid or Twilio may be a better fit, or the application may need a separate event ingestion layer. Polling is predictable, but it is not instantaneous.&lt;/p&gt;

&lt;p&gt;Stay with a specialized email provider when you need hosted email OTP, SMTP relay, or a mature domestic compliance posture. Infrai's domestic Tencent email vendor is still pending, so it cannot be used as evidence of domestic compliance. There is also no voice, WhatsApp, or RCS channel here, and there is no tag-aggregated cost-report API; those are capability boundaries, not transient faults.&lt;/p&gt;

&lt;p&gt;Your mileage may vary on polling frequency. I am not sure a five-second interval is justified for every marketplace; incident severity, provider quotas, and the legal meaning of a delayed notice should set that number. What is certain is that the decision belongs in an explicit policy, with metrics for queue age, submit retries, poll lag, and terminal-state coverage.&lt;/p&gt;

&lt;p&gt;A queue plus batch APIs and polling is a sound default for batched event notifications, provided the team accepts the freshness and channel limits instead of hiding them behind a success response.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc6376" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc6376&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;li&gt;&lt;a href="https://docs.sendgrid.com/for-developers/tracking-events/event" rel="noopener noreferrer"&gt;https://docs.sendgrid.com/for-developers/tracking-events/event&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://documentation.mailgun.com/docs/mailgun/user-manual/events/" rel="noopener noreferrer"&gt;https://documentation.mailgun.com/docs/mailgun/user-manual/events/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>notifications</category>
      <category>email</category>
      <category>sms</category>
      <category>backend</category>
    </item>
    <item>
      <title>Phone Verification Login SMS OTP: Backend Resend Countdown Example for US and EU</title>
      <dc:creator>ottoneumann8425</dc:creator>
      <pubDate>Fri, 21 Aug 2026 21:02:01 +0000</pubDate>
      <link>https://dev.to/ottoneumann8425/phone-verification-login-sms-otp-backend-resend-countdown-example-for-us-and-eu-4mmk</link>
      <guid>https://dev.to/ottoneumann8425/phone-verification-login-sms-otp-backend-resend-countdown-example-for-us-and-eu-4mmk</guid>
      <description>&lt;p&gt;For a Next.js phone verification login, SMS OTP is the easy part; the backend state machine, resend button, and compliance evidence are the real work. Marketplace login looks small on a sequence diagram: send a code, accept a code, create a session. The bill and the audit trail are larger. In a marketplace, every resend is a possible duplicate charge, every rejected number is a compliance decision, and a support ticket may require proof of what happened to a message.&lt;/p&gt;

&lt;p&gt;Short answer: use a thin Next.js server action or API route to start SMS OTP, keep the resend countdown and attempt limits in your backend, verify the code before creating a session, and poll delivery events when you need evidence. This keeps the client replaceable and leaves country policy where it can be reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does a phone verification login really cost to retain?
&lt;/h2&gt;

&lt;p&gt;The dominant term is usually message volume, not the few fields in an OTP record. A six-digit code sent once per login is predictable; a button that can be tapped repeatedly is not. Retaining every request payload forever also turns a modest authentication feature into a privacy and storage obligation.&lt;/p&gt;

&lt;p&gt;I model the flow as two ledgers. The operational ledger keeps a request id, a masked destination, timestamps, retry-after, attempt count, country decision, provider status, and the reason a request was suppressed. The evidence ledger keeps an immutable event reference and the policy version used for the decision. It does not need the raw phone number or the OTP value. Those are liabilities, not audit evidence.&lt;/p&gt;

&lt;p&gt;The change that moves the dominant term is a server-owned resend state. When the user presses the button, the server checks the countdown and the per-login maximum before sending anything. A client timer is only a display; it can be reset, edited, or bypassed. I once treated a 30-second countdown as a UI detail and found that a fast retry path could enqueue two sends before the first response reached the browser. The fix was boring: an idempotency key tied to the login challenge, plus a server timestamp. Boring is good here.&lt;/p&gt;

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

&lt;p&gt;Imagine two browser tabs sharing one login challenge. Tab A starts at 09:00:00 and receives a 30-second retry-after. At 09:00:01, tab B submits the same resend request; at 09:00:02, a mobile client repeats it after a lost response. The backend must serialize those three attempts against one challenge row, not three browser clocks. A transaction can compare the stored next-allowed timestamp, increment the attempt counter only for an accepted send, and persist the idempotency key before the provider call. If the provider call is retried after a 429, the same key lets the operation remain exactly-once from the application's point of view. If the call succeeds but the response is lost, the next request can read the stored result rather than enqueueing a second message. That sequence gives support a useful chain: policy decision, request id, provider status, and verification result. It also makes the failure mode explicit: a user may wait for the countdown even though the first message is already in flight. The interface should say that plainly instead of promising instant delivery.&lt;/p&gt;

&lt;p&gt;The retention trade-off is deliberate. Keep hashes, ids, status transitions, and policy decisions long enough for your compliance window; discard the OTP and minimize the destination after that window. When a dispute arrives later, you may lose the ability to reconstruct the exact message body. That is the cost of data minimization, and it is preferable to retaining secrets merely to make an investigation feel comfortable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a Next.js backend handle SMS OTP verification and resend countdowns?
&lt;/h2&gt;

&lt;p&gt;The browser asks your backend for a challenge. The backend applies a US or EU allowlist, creates a challenge id, and calls the SMS OTP capability. Return only a masked destination and retry-after metadata. On form submit, send the challenge id and code to verification; create the application session only after a successful response.&lt;/p&gt;

&lt;p&gt;Here is a compact Go handler that illustrates the boundary. It uses the two capability routes needed for the decision, an environment variable for the key, explicit methods, and bounded retries for rate limiting. The request fields are kept intentionally small; your application can validate its own country and attempt policy before this call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;otp&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"bytes"&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/json"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"io"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="s"&gt;"strconv"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;HTTP&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;
    &lt;span class="n"&gt;Key&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idem&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&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="n"&gt;err&lt;/span&gt; &lt;span class="o"&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;Marshal&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;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&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="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&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;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;c&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;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idem&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&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;Body&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;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;readErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readErr&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusTooManyRequests&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="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parseErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Atoi&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;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Retry-After"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;parseErr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&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="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&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="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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="o"&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;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sms request failed: %s: %s"&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;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rate limited after retries"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;challenge&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/sms/otp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"phone"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/sms/verify"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"challenge_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="o"&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;challenge&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;":verify"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultClient&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact session cookie, CSRF handling, and database transaction remain yours. Do not mark a user verified when the send call returns; a send acknowledgement is not proof that the code was entered. Store the challenge state transactionally so a timeout, a second tab, and a resend all observe the same attempt counter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does country policy belong in a US or EU login flow?
&lt;/h2&gt;

&lt;p&gt;Country policy belongs in application code because SMS spend protection and geographic fences are not delegated to the provider. A US number might be allowed for a low-risk marketplace login, while an EU number may require a different retention period, consent record, or vendor route. Record the decision and policy version before dispatch, then make the same decision on resend.&lt;/p&gt;

&lt;p&gt;For delivery troubleshooting, poll message status or events instead of waiting for a webhook. The available status and event reads are pull-based, so a small worker can sample pending challenges and append transitions to the evidence ledger. This is less immediate than a push event, and that is a real limitation for workflows that require instant orchestration. It is still auditable when the polling interval and request id are recorded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provider boundaries you should decide before launch
&lt;/h2&gt;

&lt;p&gt;No provider wins every constraint. The comparison below treats compliance evidence, OTP controls, and operational surface as separate decisions rather than pretending that a single price number answers them.&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;OTP and resend model&lt;/th&gt;
&lt;th&gt;Evidence and policy fit&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrai SMS capability&lt;/td&gt;
&lt;td&gt;OTP and verify routes; application owns countdown and attempt limits&lt;/td&gt;
&lt;td&gt;One REST API and one key can sit beside email suppression and event polling, so the contract stays stable if the underlying vendor changes&lt;/td&gt;
&lt;td&gt;Events are polled, not pushed; country fences remain application work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Twilio Verify&lt;/td&gt;
&lt;td&gt;Managed verification workflow with provider-side controls&lt;/td&gt;
&lt;td&gt;Mature delivery tooling and regional compliance material&lt;/td&gt;
&lt;td&gt;Another SDK/account surface when the rest of the backend is elsewhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vonage Verify&lt;/td&gt;
&lt;td&gt;Managed code delivery and verification&lt;/td&gt;
&lt;td&gt;Useful regional coverage and provider reporting&lt;/td&gt;
&lt;td&gt;Policy and ledger integration still need an application adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS End User Messaging SMS&lt;/td&gt;
&lt;td&gt;Low-level SMS primitives; verification logic is yours&lt;/td&gt;
&lt;td&gt;Integrates with AWS identity and audit tooling&lt;/td&gt;
&lt;td&gt;More components to operate for a complete OTP lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai is a reasonable fit when the important architectural property is a stable HTTP contract, because its advantage is one key and one REST API: pure HTTP, no SDK, any language, with one credential for SMS and adjacent backend capabilities, so swapping the vendor behind that contract does not force a rewrite of the login code. That convenience is not a compliance certification. The catch is that there are no webhook events, no hosted email OTP fallback, and no SMTP relay, voice, WhatsApp, or RCS channel; choose Twilio or Vonage when those managed channels or push events are non-negotiable, and choose AWS when your evidence and controls must remain inside an AWS-native boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retention is a policy decision, not a logging default
&lt;/h2&gt;

&lt;p&gt;On success, persist a verification timestamp, challenge id, policy version, and the audit event reference, then mint the session. On failure, increment attempts atomically and expose a generic message; do not reveal whether a phone is registered. A suppression list can prevent known-invalid recipients, but it does not replace rate limits or country policy.&lt;/p&gt;

&lt;p&gt;Your mileage may vary: local telecom rules and retention mandates differ, and I am not sure a single polling interval is right for every marketplace. Resolve that uncertainty with counsel and an observed delivery SLO, not with a larger client countdown.&lt;/p&gt;

&lt;p&gt;Keep it boring.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.twilio.com/docs/verify" rel="noopener noreferrer"&gt;https://www.twilio.com/docs/verify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.vonage.com/en/verify/overview" rel="noopener noreferrer"&gt;https://developer.vonage.com/en/verify/overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sms-voice/latest/userguide/what-is-sms-messaging.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/sms-voice/latest/userguide/what-is-sms-messaging.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc6376" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc6376&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pages.nist.gov/800-63-3/" rel="noopener noreferrer"&gt;https://pages.nist.gov/800-63-3/&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://www.twilio.com/docs/verify" rel="noopener noreferrer"&gt;https://www.twilio.com/docs/verify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.vonage.com/en/verify/overview" rel="noopener noreferrer"&gt;https://developer.vonage.com/en/verify/overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sms-voice/latest/userguide/what-is-sms-messaging.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/sms-voice/latest/userguide/what-is-sms-messaging.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>sms</category>
      <category>otp</category>
    </item>
    <item>
      <title>Implementing Healthtech LLM Classification in Node.js: Structured JSON Batch Tagging</title>
      <dc:creator>ottoneumann8425</dc:creator>
      <pubDate>Thu, 20 Aug 2026 18:43:45 +0000</pubDate>
      <link>https://dev.to/ottoneumann8425/implementing-healthtech-llm-classification-in-nodejs-structured-json-batch-tagging-1h02</link>
      <guid>https://dev.to/ottoneumann8425/implementing-healthtech-llm-classification-in-nodejs-structured-json-batch-tagging-1h02</guid>
      <description>&lt;p&gt;Short answer: for a multi-tenant healthtech SaaS that turns sales-call summaries into CRM actions, choose an LLM classification API by testing fixed JSON labels on a labeled sample, attributing every call's cost to a tenant, and moving large tagging queues to batch only after the synchronous path is auditable.&lt;/p&gt;

&lt;p&gt;The cheapest model on a pricing page is not automatically the lowest-cost system. Invalid labels, retries, and classifications that a human must repair all consume money and, more importantly, weaken the audit trail. I would begin with a small model, a narrow label vocabulary, and a ledger entry that joins tenant, input hash, model, schema version, vendor request ID, result, and charge. Then compare OpenAI, Claude, Gemini, Mistral, Groq, and a gateway such as Infrai against the same frozen evaluation set.&lt;/p&gt;

&lt;p&gt;This is an architecture decision, not a leaderboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should a Node.js SaaS compare for LLM text classification and batch tagging?
&lt;/h2&gt;

&lt;p&gt;Use a two-stage gate. The first gate rejects any response that is not valid structured JSON or contains a label outside the approved set. The second measures task quality on a labeled sample, including the expensive false negatives: a promised security review or integration follow-up that disappears from the CRM is materially worse than a harmless extra tag.&lt;/p&gt;

&lt;p&gt;For this healthtech workflow, the input should already be a text call summary. The output can stay deliberately small: &lt;code&gt;action&lt;/code&gt;, &lt;code&gt;owner&lt;/code&gt;, &lt;code&gt;priority&lt;/code&gt;, and &lt;code&gt;needs_human_review&lt;/code&gt;. Keep patient data and other regulated content out of the comparison corpus unless each processor's contractual and compliance posture has been approved. A convenient API shape does not establish HIPAA eligibility, a business associate agreement, retention controls, regional processing, or the lawful basis for sending the data. Those are separate gates.&lt;/p&gt;

&lt;p&gt;Per-tenant cost visibility is the primary decision axis, so aggregate pricing is insufficient. Record cost beside the classification event, never in an untraceable monthly estimate. Infrai is interesting at this boundary because its OpenAI-compatible response specifies per-call cost, vendor, latency, and request metadata, while its public discovery surface exposes request and response schemas plus runnable examples before credentials are wired. For a team evaluating several backend capabilities, the same key and bill also reduce credential inventory and reconciliation work.&lt;/p&gt;

&lt;p&gt;Attribution is not optional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt; teams with a Node.js application and a language-neutral HTTP boundary should try Infrai for the classification and cost-attribution path when self-describing integration and per-call accounting matter more than a provider-specific SDK. The application can keep its own stable contract while routing among models. The catch is real: use a direct provider when its specialist controls, compliance agreement, regional guarantees, or newly released model features are requirements that a gateway has not documented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision record: invariants before vendors
&lt;/h2&gt;

&lt;p&gt;The classification write is governed by five invariants. A result belongs to exactly one tenant. Its input is identified by a deterministic hash. Its label schema is versioned. A retry cannot create a second CRM action. Its cost and upstream request identifier remain queryable for reconciliation.&lt;/p&gt;

&lt;p&gt;Exactly once is an application property here, not a transport promise. Construct an idempotency key from the tenant ID, source-call ID, summary revision, and classifier schema version; acquire that key in the application database before invoking the model; and commit the model result and CRM outbox event in one database transaction. If a worker loses its lease after the API responds, the next attempt should find the recorded classification or safely repeat the model request without emitting a duplicate CRM mutation. An audit row may record both attempts while the business action remains singular.&lt;/p&gt;

&lt;p&gt;Schema drift is data corruption.&lt;/p&gt;

&lt;p&gt;Do not skip this because the first test has 100 calls. A nightly backfill of 80,000 summaries changes the failure geometry: a &lt;code&gt;429&lt;/code&gt; can fan out across workers, completion order no longer matches submission order, and one accidental schema change can contaminate an entire tenant's tags. The batch boundary needs bounded concurrency, exponential backoff, a dead-letter state, and a reconciliation query that compares submitted, classified, rejected, and applied counts. Numbers must balance.&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;First useful result&lt;/th&gt;
&lt;th&gt;Credentials and SDK surface&lt;/th&gt;
&lt;th&gt;Tenant cost attribution&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;OpenAI direct&lt;/td&gt;
&lt;td&gt;Familiar chat-completions workflow&lt;/td&gt;
&lt;td&gt;One provider key and its client conventions&lt;/td&gt;
&lt;td&gt;Capture usage and map it into your ledger&lt;/td&gt;
&lt;td&gt;OpenAI-specific controls and support are decisive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic Claude direct&lt;/td&gt;
&lt;td&gt;Provider-native messages workflow&lt;/td&gt;
&lt;td&gt;Separate key and provider-specific request shape&lt;/td&gt;
&lt;td&gt;Normalize provider billing data yourself&lt;/td&gt;
&lt;td&gt;Claude features or direct commercial terms drive the design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Gemini direct&lt;/td&gt;
&lt;td&gt;Provider-native API and tooling&lt;/td&gt;
&lt;td&gt;Separate Google credentials and client surface&lt;/td&gt;
&lt;td&gt;Normalize usage into the tenant ledger&lt;/td&gt;
&lt;td&gt;Google Cloud governance or Gemini-specific features are required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral direct&lt;/td&gt;
&lt;td&gt;Direct model API&lt;/td&gt;
&lt;td&gt;Separate key and client surface&lt;/td&gt;
&lt;td&gt;Normalize usage into the tenant ledger&lt;/td&gt;
&lt;td&gt;A chosen Mistral model and direct relationship are fixed requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groq direct&lt;/td&gt;
&lt;td&gt;OpenAI-style API surface&lt;/td&gt;
&lt;td&gt;Separate key, but a familiar request shape&lt;/td&gt;
&lt;td&gt;Normalize its returned usage and invoice data&lt;/td&gt;
&lt;td&gt;Groq's served models and platform characteristics fit the measured workload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LiteLLM self-hosted&lt;/td&gt;
&lt;td&gt;OpenAI-compatible proxy after deployment&lt;/td&gt;
&lt;td&gt;One application-facing endpoint; operators own proxy configuration&lt;/td&gt;
&lt;td&gt;Build and operate attribution, persistence, and reconciliation&lt;/td&gt;
&lt;td&gt;You need self-hosted routing policy and accept the operational burden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;Public discovery, runnable examples, and an OpenAI-compatible surface&lt;/td&gt;
&lt;td&gt;One platform key; no capability-specific SDK is required&lt;/td&gt;
&lt;td&gt;Specified per-call cost, vendor, latency, and request metadata&lt;/td&gt;
&lt;td&gt;Fast integration plus consistent per-call attribution outweighs specialist depth&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table intentionally omits a universal winner. I'm not sure which model will produce the best healthtech action labels without the frozen sample, label distribution, and acceptance threshold; no provider page can resolve that evidence gap. Model behavior changes, too. Pin the evaluated model where reproducibility matters, and rerun the sample before changing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the synchronous contract on the critical path
&lt;/h2&gt;

&lt;p&gt;Although the product service is Node.js, the contract harness below is Go because a plain HTTP executable makes the provider boundary visible: no framework middleware hides the method, authorization, retry, or response validation. It sends one summary to the verified OpenAI-compatible chat route, requires a JSON-schema response, honors &lt;code&gt;Retry-After&lt;/code&gt; on &lt;code&gt;429&lt;/code&gt;, and prints an auditable record. Set &lt;code&gt;INFRAI_API_KEY&lt;/code&gt;, then run it with a current model ID selected from &lt;code&gt;/v1/ai/models&lt;/code&gt;; the model list, rather than an article or a hard-coded default, is the authoritative place to choose a served model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"bytes"&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"crypto/sha256"&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/hex"&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/json"&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"io"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="s"&gt;"strconv"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://api.infrai.cc/v1/chat/completions"&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;classification&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Action&lt;/span&gt;           &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"action"`&lt;/span&gt;
    &lt;span class="n"&gt;Owner&lt;/span&gt;            &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"owner"`&lt;/span&gt;
    &lt;span class="n"&gt;Priority&lt;/span&gt;         &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"priority"`&lt;/span&gt;
    &lt;span class="n"&gt;NeedsHumanReview&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;   &lt;span class="s"&gt;`json:"needs_human_review"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;chatResponse&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Choices&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"content"`&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s"&gt;`json:"message"`&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s"&gt;`json:"choices"`&lt;/span&gt;
    &lt;span class="n"&gt;Infrai&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CostUSD&lt;/span&gt;  &lt;span class="kt"&gt;float64&lt;/span&gt; &lt;span class="s"&gt;`json:"cost_usd"`&lt;/span&gt;
        &lt;span class="n"&gt;Vendor&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="s"&gt;`json:"vendor"`&lt;/span&gt;
        &lt;span class="n"&gt;RequestID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"request_id"`&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s"&gt;`json:"infrai"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"LLM_MODEL"&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;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"set INFRAI_API_KEY and LLM_MODEL"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;tenantID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"clinic-network-042"&lt;/span&gt;
    &lt;span class="n"&gt;callID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"sales-call-9182"&lt;/span&gt;
    &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"The buyer requested a security review and asked for an EHR integration follow-up next week."&lt;/span&gt;
    &lt;span class="n"&gt;schemaVersion&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"crm-action-v1"&lt;/span&gt;
    &lt;span class="n"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenantID&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;callID&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;schemaVersion&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="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"model"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"messages"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"role"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Classify the summary into the supplied schema. Use only allowed enum values."&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"role"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s"&gt;"response_format"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"json_schema"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"json_schema"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"crm_action"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"strict"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"schema"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s"&gt;"properties"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="s"&gt;"action"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"enum"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"security_review"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"integration_follow_up"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"no_action"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
                        &lt;span class="s"&gt;"owner"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"enum"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"sales"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"security"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"solutions"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
                        &lt;span class="s"&gt;"priority"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"enum"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"normal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
                        &lt;span class="s"&gt;"needs_human_review"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"boolean"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="s"&gt;"required"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"needs_human_review"&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="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&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;payload&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&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;Choices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"expected exactly one classification"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="n"&gt;classification&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&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;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&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;Choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&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;Message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invalid structured classification: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;audit&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"tenant_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tenantID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"source_call_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;callID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"schema_version"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;schemaVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"idempotency_key"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"model"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"vendor"&lt;/span&gt;&lt;span class="o"&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;Infrai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Vendor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"provider_request_id"&lt;/span&gt;&lt;span class="o"&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;Infrai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"cost_usd"&lt;/span&gt;&lt;span class="o"&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;Infrai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CostUSD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"classification"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&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;MarshalIndent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"  "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;any&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="n"&gt;chatResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&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="n"&gt;err&lt;/span&gt; &lt;span class="o"&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;Marshal&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&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="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&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="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&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;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&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;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;responseBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LimitReader&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;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;20&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;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;readErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readErr&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusTooManyRequests&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="n"&gt;retryDelay&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;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Retry-After"&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;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;After&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="o"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&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;if&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;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="o"&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;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classification rejected with status %d: %s"&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;StatusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;responseBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;decoded&lt;/span&gt; &lt;span class="n"&gt;chatResponse&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&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;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responseBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&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="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rate limit retry budget exhausted"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;retryDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&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;seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Atoi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sum256&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&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="n"&gt;hex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The short snippet still leaves a deliberate database boundary. Before this request, insert or lock the idempotency record; after it, persist the response and enqueue the CRM outbox event atomically. Don't let the HTTP handler write directly to the CRM. A timeout after the remote system accepts a mutation is otherwise indistinguishable from rejection, and blind retry can create two follow-ups.&lt;/p&gt;

&lt;p&gt;One action means one action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move volume to batch without losing the ledger
&lt;/h2&gt;

&lt;p&gt;Once the synchronous contract passes the labeled evaluation, estimate prompt and completion spend before a backfill and group classification work into batches. Infrai exposes verified cost-estimate, cost-compare, and batch-submission capabilities, but their current request schemas should be read from public discovery at build time rather than reconstructed from a blog post. That self-description is the practical advantage: adding the batch capability means reading its declared schema and runnable Go example, not installing and learning another SDK.&lt;/p&gt;

&lt;p&gt;Batching does not relax the invariants. Persist one child record per source summary, give each record the same deterministic identity used by the synchronous path, and reconcile the returned batch results against that manifest before producing CRM outbox messages. The parent batch state is complete only when every child is classified, explicitly rejected, or routed to human review. In a regulated environment, retain the minimum input necessary for the approved audit period and separate operational metadata from sensitive transcript content.&lt;/p&gt;

&lt;p&gt;Reconcile every child.&lt;/p&gt;

&lt;p&gt;Price can be a tiebreaker after quality and governance pass. Infrai's current model catalog is the source for served model IDs and token rates, and its per-call metadata supports tenant chargeback without deriving a cost from a blended invoice. Rates move; store the catalog version or observation time used for forecasts, then reconcile forecasts against actual call records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rejected option and the boundary where it wins
&lt;/h2&gt;

&lt;p&gt;The rejected default is a provider-specific client embedded throughout the Node.js domain layer. It can deliver a quick demonstration, but each additional provider introduces another credential, response adapter, retry policy, usage mapper, and invoice reconciliation path. That coupling is especially awkward when a sales-call classifier must be replaced without rewriting the CRM workflow.&lt;/p&gt;

&lt;p&gt;Still, direct integration is the correct choice when a provider's native feature is the requirement rather than interchangeable structured classification. Stick with OpenAI, Anthropic, Google, Mistral, or Groq directly when the selected vendor supplies a needed contractual control or native capability and you are willing to own its adapter. Choose self-hosted LiteLLM when routing policy must run in your environment and your team can operate its availability, upgrades, accounting storage, and security posture.&lt;/p&gt;

&lt;p&gt;There is also a hard modality boundary. This design starts from text summaries; it should not be presented as an audio-classification pipeline. Infrai's transcription route shape is present but not currently serviceable, real-time voice-session key status is pending and limited to the western region, and there is no dedicated moderation endpoint. If raw audio ingestion, real-time voice, or a dedicated moderation product is mandatory, select a service that documents that capability today; chat with a JSON schema can classify text or provide an application-level moderation fallback, but it does not erase those product and compliance distinctions.&lt;/p&gt;

&lt;p&gt;For the stated healthtech job, the final decision rule is compact: require acceptable labeled-sample quality, strict schema adherence, an approved compliance boundary, deterministic CRM effects, and reconcilable tenant-level cost. Among the options that pass, prefer the integration with the smallest credential and adapter surface. If Infrai fits that boundary, start with its &lt;a href="https://docs.infrai.cc/llms.txt" rel="noopener noreferrer"&gt;AI-readable capability manifest&lt;/a&gt; and inspect discovery before writing the adapter.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;OpenAI API documentation: &lt;a href="https://platform.openai.com/docs/api-reference" rel="noopener noreferrer"&gt;https://platform.openai.com/docs/api-reference&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic API documentation: &lt;a href="https://docs.anthropic.com/en/api/overview" rel="noopener noreferrer"&gt;https://docs.anthropic.com/en/api/overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gemini API documentation: &lt;a href="https://ai.google.dev/gemini-api/docs" rel="noopener noreferrer"&gt;https://ai.google.dev/gemini-api/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Mistral API documentation: &lt;a href="https://docs.mistral.ai/api/" rel="noopener noreferrer"&gt;https://docs.mistral.ai/api/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Groq API documentation: &lt;a href="https://console.groq.com/docs/overview" rel="noopener noreferrer"&gt;https://console.groq.com/docs/overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;LiteLLM self-hosted gateway: &lt;a href="https://github.com/BerriAI/litellm" rel="noopener noreferrer"&gt;https://github.com/BerriAI/litellm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN guide to server-sent events: &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;Infrai capability manifest: &lt;a href="https://docs.infrai.cc/llms.txt" rel="noopener noreferrer"&gt;https://docs.infrai.cc/llms.txt&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>llm</category>
      <category>healthtech</category>
    </item>
    <item>
      <title>OpenAI-Compatible SaaS Chat — 1 API Key for Claude and Gemini</title>
      <dc:creator>ottoneumann8425</dc:creator>
      <pubDate>Tue, 18 Aug 2026 22:12:51 +0000</pubDate>
      <link>https://dev.to/ottoneumann8425/openai-compatible-saas-chat-1-api-key-for-claude-and-gemini-243o</link>
      <guid>https://dev.to/ottoneumann8425/openai-compatible-saas-chat-1-api-key-for-claude-and-gemini-243o</guid>
      <description>&lt;p&gt;Short answer: for a junior team building SaaS support chat over a private knowledge base, start with one OpenAI-compatible chat-completions contract, discover available models at deployment time, and enforce the answer schema in your Go boundary rather than wiring three vendor SDKs into the application.&lt;/p&gt;

&lt;p&gt;My recommendation is narrow: try Infrai for the chat boundary when the team needs to move among OpenAI, Claude, and Gemini model families without changing application code. Its useful distinction is contractual, not cosmetic: the model provider behind a capability can change while the REST contract remains stable. A single credential and bill also remove key rotation and reconciliation work from that path. The catch is important, though. A common contract should own only the portable subset; a vendor-specific feature that materially improves answer quality is a reason to use that vendor directly.&lt;/p&gt;

&lt;p&gt;This is an architecture decision about structured output correctness. It isn't a leaderboard, and price is not the deciding variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision, invariants, and failure boundaries
&lt;/h2&gt;

&lt;p&gt;The system accepts a customer question, retrieves passages from a private knowledge base, and asks a model to return an answer plus citations. The application can tolerate a model substitution. It cannot tolerate a syntactically plausible answer that cites a document never supplied to the model, changes field names, or disappears from the audit trail.&lt;/p&gt;

&lt;p&gt;That distinction produces four invariants:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The application owns one versioned response type, independent of the selected model.&lt;/li&gt;
&lt;li&gt;Every cited document ID must belong to the retrieved set for that request.&lt;/li&gt;
&lt;li&gt;A retry may repeat inference, but it must not create a second externally visible support reply.&lt;/li&gt;
&lt;li&gt;The audit record must retain the request ID, selected model, knowledge-base revision, retrieved document IDs, validation result, and publication state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Exactly-once model execution is the wrong promise. A connection can fail after the provider has accepted a request but before the client receives the result; retrying may therefore execute inference twice. The defensible design makes publication idempotent. Persist a deterministic operation ID before inference, validate the returned object, and commit the reply through a unique constraint or compare-and-set transition. Duplicate computation is unfortunate. Duplicate customer replies are a correctness defect.&lt;/p&gt;

&lt;p&gt;The failure boundary also belongs in the decision record. Authentication failures and other 4xx responses stop the operation and preserve the response body for diagnosis; HTTP 429 is retryable after &lt;code&gt;Retry-After&lt;/code&gt;, with bounded exponential backoff when that header is absent. A successful HTTP status still does not authorize publication. JSON decoding, field validation, citation membership, and policy checks happen first. This resembles a ledger posting pipeline: transport acceptance is not settlement.&lt;/p&gt;

&lt;p&gt;Keep version one text-only. Although an audio transcription shape may be visible, transcription is not currently serviceable in this platform, and real-time voice session key status is pending with western-region scope. There is also no dedicated moderation endpoint here, so a support product that needs moderation should classify text or images through a chat model with a JSON-schema guard and keep its own conservative policy decision. Those are capability boundaries, not reasons to weaken the text path.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a SaaS app compare one API key for OpenAI, Claude, and Gemini?
&lt;/h2&gt;

&lt;p&gt;Compare the integration that must be operated, not the number of models in a catalog. For a small backend team, the meaningful questions are how many credentials enter the secret manager, how many request and streaming types reach business code, whether model availability can be queried, and whether usage can be attributed to a tenant. A first response in five minutes is less valuable than a contract that remains understandable during the first disputed support answer.&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;Setup and SDK surface&lt;/th&gt;
&lt;th&gt;Strong fit&lt;/th&gt;
&lt;th&gt;Cost or correctness burden&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct OpenAI API&lt;/td&gt;
&lt;td&gt;One vendor credential and its native client contract&lt;/td&gt;
&lt;td&gt;Teams committed to OpenAI features and release cadence&lt;/td&gt;
&lt;td&gt;A second provider introduces another adapter and reconciliation path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct Anthropic Claude API&lt;/td&gt;
&lt;td&gt;A separate Claude credential and native contract&lt;/td&gt;
&lt;td&gt;Teams that need Claude-specific behavior exposed by its API&lt;/td&gt;
&lt;td&gt;Shared schemas, retries, and usage records become application-owned translation work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct Google Gemini API&lt;/td&gt;
&lt;td&gt;A separate Gemini credential and native contract&lt;/td&gt;
&lt;td&gt;Teams centered on Google's model and platform surface&lt;/td&gt;
&lt;td&gt;Switching vendors crosses a client and credential boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenRouter&lt;/td&gt;
&lt;td&gt;One gateway contract across model providers&lt;/td&gt;
&lt;td&gt;Teams prioritizing a broad LLM routing catalog&lt;/td&gt;
&lt;td&gt;The gateway contract and routing policy become another dependency to evaluate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;OpenAI-compatible chat plus model listing under one key&lt;/td&gt;
&lt;td&gt;Small teams that want a stable portable boundary and fewer credentials&lt;/td&gt;
&lt;td&gt;Not suitable when a required vendor-native feature falls outside the common contract&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All five can be rational. Stick with OpenAI, Anthropic, or Google directly when a native feature is part of the product rather than an implementation detail. OpenRouter deserves a proof of concept when broad model routing is the primary job. Infrai is a stronger fit when chat is one part of a backend integration and the team values a consistent REST boundary: its public discovery surface describes 295 capabilities across 20 modules, including request and response schemas and runnable Go examples, without requiring a key. That makes the contract inspectable before credentials enter the process.&lt;/p&gt;

&lt;p&gt;Model listing is mandatory in the gateway case. Names, availability, context limits, and modalities can differ across nominally comparable models, so deployment should resolve a configured choice against &lt;code&gt;GET /v1/ai/models&lt;/code&gt; rather than assume that a marketing family name is callable. Cost comparison and token counting are useful controls before exposing multiple choices to tenants, but they are guardrails, not proof of answer quality. I'm not sure which model will produce the best citations for a particular private corpus; only a versioned evaluation set drawn from that corpus can resolve it.&lt;/p&gt;

&lt;p&gt;Short version: fewer SDKs reduce integration friction, while local validation and an idempotent publication step preserve correctness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Critical path in Go
&lt;/h2&gt;

&lt;p&gt;The smallest useful example sends one OpenAI-compatible request and refuses to print an answer unless the response matches the local contract. It deliberately uses the Go standard library so the visible HTTP method, authentication boundary, rate-limit behavior, and status handling can be audited in one file. Set &lt;code&gt;INFRAI_API_KEY&lt;/code&gt;, then run it with Go; no key belongs in source control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"bytes"&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/json"&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"io"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="s"&gt;"strconv"&lt;/span&gt;
    &lt;span class="s"&gt;"strings"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://api.infrai.cc/v1/chat/completions"&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;chatRequest&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Model&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;    &lt;span class="s"&gt;`json:"model"`&lt;/span&gt;
    &lt;span class="n"&gt;Messages&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="s"&gt;`json:"messages"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Role&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"role"`&lt;/span&gt;
    &lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"content"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;chatResponse&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"id"`&lt;/span&gt;
    &lt;span class="n"&gt;Model&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"model"`&lt;/span&gt;
    &lt;span class="n"&gt;Choices&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="s"&gt;`json:"message"`&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s"&gt;`json:"choices"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;supportAnswer&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Answer&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"answer"`&lt;/span&gt;
    &lt;span class="n"&gt;CitationIDs&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"citation_ids"`&lt;/span&gt;
    &lt;span class="n"&gt;Escalate&lt;/span&gt;    &lt;span class="kt"&gt;bool&lt;/span&gt;     &lt;span class="s"&gt;`json:"escalate"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_API_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;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_API_KEY is required"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"kb-refund-7"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"kb-plan-12"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;`Use only these private knowledge-base records:
kb-refund-7: Refund reviews take five business days.
kb-plan-12: Plan changes take effect on the next renewal.

Question: When does my plan change take effect?
Return only JSON with answer (string), citation_ids (string array), and escalate (boolean).`&lt;/span&gt;

    &lt;span class="n"&gt;reqBody&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;chatRequest&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"auto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Messages&lt;/span&gt;&lt;span class="o"&gt;:&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="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Answer only from supplied records. Escalate when evidence is insufficient."&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&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;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;45&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&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="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&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;reqBody&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&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;Choices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"expected exactly one completion choice"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="n"&gt;supportAnswer&lt;/span&gt;
    &lt;span class="n"&gt;decoder&lt;/span&gt; &lt;span class="o"&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;NewDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&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;Choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&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;Message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;decoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisallowUnknownFields&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;decoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invalid answer JSON: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"answer must not be empty"&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CitationIDs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"citation %q was not retrieved"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Persist result.ID, result.Model, the KB revision, citations, and validation state before publishing.&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"validated request=%s model=%s answer=%q citations=%v escalate=%t&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&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;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CitationIDs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Escalate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;chatRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&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="n"&gt;err&lt;/span&gt; &lt;span class="o"&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;Marshal&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;chatResponse&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;40&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&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="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&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="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&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;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;chatResponse&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&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;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;chatResponse&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;responseBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&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;Body&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;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;readErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;chatResponse&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;readErr&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusTooManyRequests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parseErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Atoi&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;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Retry-After"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;parseErr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chatResponse&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="o"&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;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;300&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;chatResponse&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"chat request failed: status=%d body=%s"&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;StatusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;responseBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="n"&gt;chatResponse&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&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;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responseBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;chatResponse&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;chatResponse&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="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rate limit retry budget exhausted"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code stops at the trust boundary. A production handler should write an immutable attempt record and use an operation ID such as &lt;code&gt;tenant:conversation:message&lt;/code&gt; as the unique publication key. If two workers finish, one transition wins and the other records a duplicate attempt without sending another reply. Do not mistake a request ID for that business idempotency key: the former supports tracing, while the latter controls the side effect the customer sees.&lt;/p&gt;

&lt;p&gt;There is one intentionally strict edge here. &lt;code&gt;citation_ids&lt;/code&gt; is checked against retrieval output, not merely accepted because the model emitted valid JSON. Schema correctness without referential correctness is polished corruption.&lt;/p&gt;

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

&lt;p&gt;The rejected design imports three vendor clients into the support service and selects one with a branch. It looks direct, but the branch leaks into streaming events, error classification, usage accounting, test fixtures, secret rotation, and audit serialization. Each new model then becomes a migration across several operational contracts. For a junior SaaS team whose immediate job is grounded text support, that surface area is hard to justify.&lt;/p&gt;

&lt;p&gt;It remains the right design in a different system. Choose direct Anthropic, Google, or OpenAI integration when the product depends on a provider-specific request field, response event, regional control, or release schedule that a compatibility layer does not expose. Choose a specialist gateway when routing sophistication is the central requirement and its contract passes your evaluation. A common API is also not suitable when compliance requires a direct processor relationship or deployment control that the gateway cannot document; procurement and counsel must decide that boundary, because an API shape cannot establish regulatory suitability.&lt;/p&gt;

&lt;p&gt;The final decision rule is therefore modest. Use an OpenAI-compatible gateway to keep ordinary chat portable, keep retrieval validation and publication idempotency inside the application, and isolate the provider behind one adapter. Move to a direct integration when a measured product requirement crosses that adapter's portable subset. Don't abstract an unobserved future, but do preserve the audit record that will tell you when the future arrives.&lt;/p&gt;

&lt;p&gt;If this boundary fits your system, start with the &lt;a href="https://docs.infrai.cc/llms.txt" rel="noopener noreferrer"&gt;Infrai capability manifest&lt;/a&gt; and verify the live model catalog before selecting a default.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.infrai.cc/llms.txt" rel="noopener noreferrer"&gt;Infrai AI-readable capability manifest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/api-reference" rel="noopener noreferrer"&gt;OpenAI API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/api/overview" rel="noopener noreferrer"&gt;Anthropic API documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ai.google.dev/gemini-api/docs" rel="noopener noreferrer"&gt;Google Gemini API documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openrouter.ai/docs/api/reference/overview" rel="noopener noreferrer"&gt;OpenRouter API documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/batch" rel="noopener noreferrer"&gt;OpenAI Batch API guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>go</category>
      <category>ai</category>
      <category>saas</category>
    </item>
    <item>
      <title>Candidate Scoring: Compare 4 Cheap LLM API Gateways with One Key</title>
      <dc:creator>ottoneumann8425</dc:creator>
      <pubDate>Mon, 17 Aug 2026 17:18:01 +0000</pubDate>
      <link>https://dev.to/ottoneumann8425/candidate-scoring-compare-4-cheap-llm-api-gateways-with-one-key-4oa</link>
      <guid>https://dev.to/ottoneumann8425/candidate-scoring-compare-4-cheap-llm-api-gateways-with-one-key-4oa</guid>
      <description>&lt;p&gt;Short answer: choose an LLM API gateway only if it can attribute every candidate-scoring call to a tenant, expose the model and charge used, preserve a replay-safe audit record, and let the team compare models before a prompt reaches production. For an e-commerce hiring workflow, Infrai is a strong fit when one key and one bill matter more than provider-specific control: its native and OpenAI-compatible responses specify per-call cost, vendor, latency, cache-hit, and request identifiers, while its model, token-count, cost-estimate, cost-compare, and batch surfaces support preflight selection and offline scoring. Keep direct OpenAI, Anthropic Claude, or Google Gemini access when a single provider's contract or controls are the governing requirement.&lt;/p&gt;

&lt;p&gt;This is an architecture decision, not a cheapest-model contest. A low unit price cannot reconcile a tenant ledger, explain why a rubric run changed, or prove that a retry did not score the same candidate twice. The operative metric is attributable spend per completed, auditable scoring run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Governance begins with a tenant usage ledger
&lt;/h2&gt;

&lt;p&gt;The first invariant is &lt;strong&gt;tenant attribution&lt;/strong&gt;. Every scoring run needs a stable &lt;code&gt;tenant_id&lt;/code&gt;, &lt;code&gt;candidate_id&lt;/code&gt;, &lt;code&gt;rubric_version&lt;/code&gt;, and client-generated &lt;code&gt;run_id&lt;/code&gt;; the usage record needs the selected model, provider, token quantities, monetary cost, request identifier, and cache-hit state returned by the runtime. Aggregate dashboards are useful for finance, but they aren't sufficient for a disputed invoice or a candidate appeal because the unit of reconciliation is the scoring run.&lt;/p&gt;

&lt;p&gt;The second invariant is &lt;strong&gt;replay safety&lt;/strong&gt;. Treat a scoring request and its accounting entry as one logical operation even though they cross process and storage boundaries. The application should reserve &lt;code&gt;run_id&lt;/code&gt; before dispatch, use the same identifier on every retry, and enforce a unique constraint on &lt;code&gt;(tenant_id, run_id)&lt;/code&gt; in its usage ledger. Infrai specifies &lt;code&gt;Idempotency-Key&lt;/code&gt; as a platform convention, including a deterministic server-derived fallback and a 24-hour default deduplication window, but I would still keep the application constraint: transport deduplication and financial reconciliation solve related, not identical, problems.&lt;/p&gt;

&lt;p&gt;The third invariant is model-catalog validation. Check &lt;code&gt;/v1/ai/models&lt;/code&gt; before admitting work, filter on &lt;code&gt;available&lt;/code&gt;, and read the current input and output prices from that response; don't freeze a model identifier or price in source code. The catalog is also the point at which a team can reject a model that does not satisfy the job's modality or availability requirement.&lt;/p&gt;

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

&lt;p&gt;The fourth invariant is a bounded decision rule. For example, route extraction and rubric normalization to the least costly approved model whose output passes the schema and evaluation threshold, while reserving a more capable approved model for ambiguous evidence. Built-in token counting and cost estimation can reject an unexpectedly large resume or rubric before dispatch, and cost comparison can show reviewers the eligible alternatives. I'm not sure which quality threshold is defensible for your hiring policy; a labeled evaluation set, reviewed for protected-class proxies and scoring consistency, is what would resolve that uncertainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry reliability requires one immutable run identity
&lt;/h2&gt;

&lt;p&gt;Failure boundaries follow from those invariants. A timeout may leave the caller uncertain about the outcome, so it must retry with the same run identity. A 429 means back off, honor &lt;code&gt;Retry-After&lt;/code&gt;, and retain that identity. A model catalog change must stop admission rather than silently substitute an unapproved model. And a completed model response is not financially complete until its usage metadata is durably associated with the tenant ledger.&lt;/p&gt;

&lt;p&gt;Consider the full lifecycle for one hypothetical rubric run. The application allocates a &lt;code&gt;run_id&lt;/code&gt; while the candidate record and rubric version are still known, writes an admission row containing the token estimate and selected catalog entry, and only then dispatches work. If the caller loses the response, the retry carries the same identity; if the runtime accepts the work once, the application ledger accepts its accounting result once. When the response arrives, the worker appends actual cost, vendor, request ID, and cache-hit metadata rather than replacing the estimate, because the estimate explains the admission decision while the actual amount supports reconciliation. A later rubric revision creates a new run linked to the prior one instead of mutating history. This is deliberately stricter than a dashboard total: at month end, finance can group accepted usage by &lt;code&gt;tenant_id&lt;/code&gt;, while an auditor can traverse the exact candidate, rubric, model decision, returned metadata, and superseding run without guessing which retry produced the score.&lt;/p&gt;

&lt;h2&gt;
  
  
  The integration boundary is executable
&lt;/h2&gt;

&lt;p&gt;The following Go program performs the smallest useful preflight without inventing a request schema for cost comparison. It fetches the live model catalog, retries a 429 with &lt;code&gt;Retry-After&lt;/code&gt; or exponential delay, verifies availability, calculates an upper-bound amount from caller-supplied input and output token counts, and emits a tenant-scoped admission record. The actual scoring worker should carry the same &lt;code&gt;run_id&lt;/code&gt; and later append the runtime's returned cost and request metadata to an immutable ledger; it should not overwrite the estimate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/json"&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"io"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="s"&gt;"strconv"&lt;/span&gt;
    &lt;span class="s"&gt;"strings"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;                 &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="s"&gt;`json:"id"`&lt;/span&gt;
    &lt;span class="n"&gt;Available&lt;/span&gt;          &lt;span class="kt"&gt;bool&lt;/span&gt;    &lt;span class="s"&gt;`json:"available"`&lt;/span&gt;
    &lt;span class="n"&gt;PriceInputPerMTok&lt;/span&gt;  &lt;span class="kt"&gt;float64&lt;/span&gt; &lt;span class="s"&gt;`json:"price_input_per_mtok"`&lt;/span&gt;
    &lt;span class="n"&gt;PriceOutputPerMTok&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt; &lt;span class="s"&gt;`json:"price_output_per_mtok"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Catalog&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Object&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="s"&gt;`json:"object"`&lt;/span&gt;
    &lt;span class="n"&gt;Capability&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="s"&gt;`json:"capability"`&lt;/span&gt;
    &lt;span class="n"&gt;AvailableOnly&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;    &lt;span class="s"&gt;`json:"available_only"`&lt;/span&gt;
    &lt;span class="n"&gt;Count&lt;/span&gt;         &lt;span class="kt"&gt;int&lt;/span&gt;     &lt;span class="s"&gt;`json:"count"`&lt;/span&gt;
    &lt;span class="n"&gt;Data&lt;/span&gt;          &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="s"&gt;`json:"data"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Admission&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TenantID&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="s"&gt;`json:"tenant_id"`&lt;/span&gt;
    &lt;span class="n"&gt;CandidateID&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="s"&gt;`json:"candidate_id"`&lt;/span&gt;
    &lt;span class="n"&gt;RubricVersion&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="s"&gt;`json:"rubric_version"`&lt;/span&gt;
    &lt;span class="n"&gt;RunID&lt;/span&gt;         &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="s"&gt;`json:"run_id"`&lt;/span&gt;
    &lt;span class="n"&gt;Model&lt;/span&gt;         &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="s"&gt;`json:"model"`&lt;/span&gt;
    &lt;span class="n"&gt;EstimatedUSD&lt;/span&gt;  &lt;span class="kt"&gt;float64&lt;/span&gt; &lt;span class="s"&gt;`json:"estimated_usd"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"missing %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&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="n"&gt;v&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;getCatalog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt; &lt;span class="n"&gt;Catalog&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&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="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodGet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimRight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;"/ai/models"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&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;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LimitReader&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;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;20&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;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;readErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readErr&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusTooManyRequests&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="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Atoi&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;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Retry-After"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0&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="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;After&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="o"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&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;if&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;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="o"&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;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;300&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;catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"model catalog returned %d: %s"&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;StatusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&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="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&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;Unmarshal&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;catalog&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="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rate limit retry budget exhausted"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;baseURL&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AI_RUNTIME_BASE_URL"&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;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;modelID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MODEL_ID"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;inputTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INPUT_TOKENS"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;64&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;inputTokens&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"INPUT_TOKENS must be a non-negative integer"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;outputTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OUTPUT_TOKEN_LIMIT"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;64&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;outputTokens&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"OUTPUT_TOKEN_LIMIT must be a non-negative integer"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;getCatalog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;baseURL&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;modelID&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Available&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;estimate&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputTokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;_000_000&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PriceInputPerMTok&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
            &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputTokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;_000_000&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PriceOutputPerMTok&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Admission&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;TenantID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TENANT_ID"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;CandidateID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CANDIDATE_ID"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;RubricVersion&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RUBRIC_VERSION"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;RunID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RUN_ID"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EstimatedUSD&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;estimate&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;err&lt;/span&gt; &lt;span class="o"&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;NewEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Encode&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&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="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"requested model is not available"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&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;This estimate is deliberately conservative because the caller supplies the maximum output allowance. Don't book it as actual spend. Store it as an admission artifact, then reconcile against the returned per-call cost after the scoring response is accepted. If the final write races with a retry, the unique run constraint wins; an operator can investigate one explicit conflict instead of reconciling two plausible charges.&lt;/p&gt;

&lt;p&gt;One run, one charge.&lt;/p&gt;

&lt;p&gt;Batch processing belongs on the other side of the same control. Nightly candidate classification or bulk rubric rescoring can use the batch surface rather than holding live requests open, provided every item retains its tenant and run identity. Batch lowers operational cost for offline work, but it does not relax auditability, evaluation, or hiring-policy review.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can teams evaluate OpenAI, Claude, Gemini batch, and caching claims?
&lt;/h2&gt;

&lt;p&gt;Compare options against the accounting boundary, not against a screenshot of today's rate card. The table deliberately avoids price figures because they change, and because no credible choice follows from token price alone.&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;Key and billing boundary&lt;/th&gt;
&lt;th&gt;Tenant-cost consequence&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;Direct OpenAI&lt;/td&gt;
&lt;td&gt;OpenAI account and bill&lt;/td&gt;
&lt;td&gt;Application must join its own tenant/run identity to provider usage&lt;/td&gt;
&lt;td&gt;OpenAI-specific contracting or controls dominate portability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct Anthropic Claude&lt;/td&gt;
&lt;td&gt;Anthropic account and bill&lt;/td&gt;
&lt;td&gt;Same application-side allocation, scoped to Claude workloads&lt;/td&gt;
&lt;td&gt;Claude-specific contracting or controls are mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct Google Gemini&lt;/td&gt;
&lt;td&gt;Google account and bill&lt;/td&gt;
&lt;td&gt;Same application-side allocation, scoped to Gemini workloads&lt;/td&gt;
&lt;td&gt;Google-specific contracting or controls are mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;One key and one bill across the covered backend services&lt;/td&gt;
&lt;td&gt;Per-call cost, vendor, latency, cache-hit, and request metadata provide a consistent reconciliation envelope&lt;/td&gt;
&lt;td&gt;A small team needs quick model switching and one accounting boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai's material advantage here is consolidation: one credential and one invoice reduce key custody and month-end reconciliation across the supported services. Infrai also exposes a REST API over plain HTTP without an SDK, regardless of language or runtime; that keeps the admission and accounting boundary stable while a team changes approved models. Its public, self-describing discovery surface needs no key and describes 295 capabilities across 20 modules, including request and response schemas and runnable examples in 10 languages, so a reviewer can verify model readiness and generate the request shape instead of trusting prose.&lt;/p&gt;

&lt;p&gt;The catch is real. One gateway becomes a control-plane dependency, and its abstraction is not suitable when procurement requires a direct provider agreement, a provider-native feature, or independently negotiated data terms. EU and US deployment cannot be inferred from a generic region label; require written confirmation of processing location, subprocessors, retention, and cross-border transfer terms before sending candidate data. Likewise, a returned &lt;code&gt;cache_hit&lt;/code&gt; field is useful evidence, but it does not by itself prove that a particular prompt is cacheable or that caching satisfies a retention policy. Stick with a direct vendor when those provider-specific assurances decide the architecture.&lt;/p&gt;

&lt;p&gt;There are adjacent capability limits too: ASR is not available in the model catalog, real-time voice sessions are limited to the western region and are outside this design, there is no dedicated moderation endpoint, and image upscaling is limited to Lanc. None is central to text rubric scoring, but the boundaries matter if the workflow later expands to interview audio, safety review, or document imaging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration remains an explicit compliance escape hatch
&lt;/h2&gt;

&lt;p&gt;For this ADR, direct integration with all three providers is rejected because it creates three credential boundaries, three billing feeds, and provider-specific reconciliation logic before the team has solved the scoring policy itself. That is too much operational surface for a junior team whose primary decision axis is per-tenant cost visibility. It also makes a model swap an application change instead of a controlled configuration decision.&lt;/p&gt;

&lt;p&gt;Still, direct integration has a valid use case. Choose it when one provider is contractually mandated, when its native controls are part of the compliance case, or when the gateway's regional and data-processing evidence does not satisfy the candidate-data policy. The same goes for a self-managed routing layer: it can be the right answer for a team prepared to own availability, catalog synchronization, usage normalization, and invoice reconciliation. Ownership is the cost there — not the repository checkout.&lt;/p&gt;

&lt;p&gt;The final approval packet should contain the rubric version, labeled quality evaluation, model allowlist, model-catalog snapshot, tenant/run uniqueness rule, retention policy, regional and subprocessor evidence, and a reconciliation query that ties every accepted score to one actual usage record.&lt;/p&gt;

&lt;p&gt;No orphaned charge.&lt;/p&gt;

&lt;p&gt;No duplicate score.&lt;/p&gt;

&lt;p&gt;Those two tests catch more architectural wishful thinking than a long feature matrix.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;MDN, "Using server-sent events": &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;/ul&gt;

</description>
      <category>llm</category>
      <category>architecture</category>
      <category>costcontrol</category>
    </item>
    <item>
      <title>Catalog Speech-to-Text Control — Bounding Upload Deadlines, File Size, and Retry Backoff</title>
      <dc:creator>ottoneumann8425</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:13:39 +0000</pubDate>
      <link>https://dev.to/ottoneumann8425/catalog-speech-to-text-control-bounding-upload-deadlines-file-size-and-retry-backoff-4k96</link>
      <guid>https://dev.to/ottoneumann8425/catalog-speech-to-text-control-bounding-upload-deadlines-file-size-and-retry-backoff-4k96</guid>
      <description>&lt;p&gt;Short answer: treat a long speech-to-text submission as an idempotent ingestion transaction, not one oversized &lt;code&gt;fetch&lt;/code&gt; call: measure the encoded body, assign separate upload and processing deadlines, persist a stable operation key, and retry only from a replayable source after classifying the failure.&lt;/p&gt;

&lt;p&gt;For a gaming catalog, the input might be a publisher's long spoken walkthrough while the desired output is structured product copy: title candidates, supported modes, accessibility notes, and content descriptors. Provider portability matters because that enrichment pipeline should survive a model or API change without rewriting catalog reconciliation. The governing invariant is therefore stronger than "the request eventually returned": one source recording must produce one attributable enrichment decision, with enough evidence to reproduce or reverse it.&lt;/p&gt;

&lt;p&gt;A longer timeout alone doesn't establish that invariant. It can hide which phase failed, hold a socket while remote work continues, and make an operator's retry indistinguishable from a duplicate submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit invariants and failure boundaries
&lt;/h2&gt;

&lt;p&gt;The decision is to put a small transcription coordinator between the catalog workflow and any speech provider. Its contract has four states: &lt;code&gt;prepared&lt;/code&gt;, &lt;code&gt;submitted&lt;/code&gt;, &lt;code&gt;accepted&lt;/code&gt;, and &lt;code&gt;completed&lt;/code&gt;. A provider adapter may implement those states with one synchronous call or with an asynchronous job, but the catalog service sees the same operation record. That record contains a content digest, provider-neutral operation key, attempt number, timestamps, terminal disposition, and the immutable identity of the source object.&lt;/p&gt;

&lt;p&gt;The critical invariants are deliberately ledger-like. The digest and operation key bind every attempt to the same audio bytes. Only a terminal, schema-valid transcript can advance catalog enrichment. An accepted request is not a completed transcription. Every transition is appended to an audit trail; it isn't overwritten by the latest status. Finally, retry authority belongs to one coordinator, because retries at the browser, reverse proxy, application, and queue layers can multiply each other. Exactly once is a business invariant here, not a claim that the network delivers a packet exactly once: the coordinator can submit more than once after an ambiguous disconnect, yet it commits the resulting transcript once by enforcing a unique operation key and comparing the source digest. If an adapter exposes a provider operation identifier, store it as evidence, but don't make that identifier part of the portable catalog contract.&lt;/p&gt;

&lt;p&gt;The failure boundaries follow from the states. Before any bytes are sent, validation failures are terminal until the input changes. During upload, a local deadline or connection loss leaves submission ambiguous. After explicit acceptance, the client should poll or receive a callback rather than upload the same file again. During enrichment, transcript schema failure must not mutate the product record; it becomes a reviewable rejected artifact.&lt;/p&gt;

&lt;p&gt;One detail is easy to miss: &lt;code&gt;multipart/form-data&lt;/code&gt; adds boundaries and per-part headers, so the file's size is not necessarily the request body's size. RFC 7578 defines that encoding. Enforce both a source-object limit and an encoded-request limit at the adapter boundary, using the provider's current published limits as configuration rather than embedding them in shared domain code. Without those published limits, the exact ceiling is uncertain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes a large Node.js fetch multipart speech-to-text upload time out?
&lt;/h2&gt;

&lt;p&gt;A timeout report usually collapses several clocks into one label. The caller may stop waiting because an &lt;code&gt;AbortSignal&lt;/code&gt; fired; an intermediary may reject or close the request; the remote API may accept the bytes but take longer to process them; or the response may be lost after acceptance. Node.js exposes browser-compatible &lt;code&gt;fetch&lt;/code&gt;, and &lt;code&gt;AbortSignal.timeout(delay)&lt;/code&gt; creates a signal that aborts after the specified active time. That mechanism controls the caller's waiting budget. It does not prove that the remote side discarded work.&lt;/p&gt;

&lt;p&gt;That distinction changes the repair. If preflight sizing rejects the encoded body, increasing a deadline is irrelevant. If upload throughput is variable but the provider accepts streaming request bodies, the upload budget should be derived from an explicit operational policy and observed conservatively, while still capped. If remote processing outlives the connection, an asynchronous acceptance boundary is the sound design. If the outcome is ambiguous, query by the stored operation identifier when the provider supports that capability; otherwise, resubmission requires a stable idempotency mechanism or downstream deduplication.&lt;/p&gt;

&lt;p&gt;Do not guess.&lt;/p&gt;

&lt;p&gt;Instrumentation should record phase, not just elapsed time: preparation started, first submission attempted, acceptance observed, processing checked, transcript validated, and catalog commit completed. Record byte counts and digests, but never log multipart bodies, raw audio, authorization headers, or unrestricted transcript text. Speech can contain personal data, voice identifiers, unreleased game details, and contractual material; retention, access, deletion, and regional handling limits have to be set by the organization's applicable compliance policy. A trace that cannot explain a duplicate is inadequate, but an audit trail that leaks the payload is worse.&lt;/p&gt;

&lt;p&gt;For Node.js specifically, build a fresh &lt;code&gt;FormData&lt;/code&gt; and fresh readable source for each authorized attempt. A consumed stream isn't a replay plan. Tie the request to an abort signal, classify the resulting exception separately from an HTTP response, and keep the operation record outside process memory. A process restart should delay work, not erase whether a recording was already accepted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Portability matrix for acceptance evidence
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Boundary&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;th&gt;Main benefit&lt;/th&gt;
&lt;th&gt;Material limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One synchronous multipart request&lt;/td&gt;
&lt;td&gt;Small inputs with predictably short processing&lt;/td&gt;
&lt;td&gt;Few moving parts&lt;/td&gt;
&lt;td&gt;An ambiguous disconnect couples upload, processing, and response into one retry decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asynchronous submission plus status check&lt;/td&gt;
&lt;td&gt;Long recordings or variable processing time&lt;/td&gt;
&lt;td&gt;Acceptance is separated from completion&lt;/td&gt;
&lt;td&gt;Requires durable job state, polling or callbacks, and reconciliation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client-side segments plus aggregation&lt;/td&gt;
&lt;td&gt;Providers with documented segment limits, when semantic joins are acceptable&lt;/td&gt;
&lt;td&gt;Each transfer has a bounded blast radius&lt;/td&gt;
&lt;td&gt;Segment boundaries can damage context and timestamps; aggregation becomes part of correctness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object handoff by reference&lt;/td&gt;
&lt;td&gt;APIs that explicitly support fetching a protected object&lt;/td&gt;
&lt;td&gt;Application servers avoid relaying the full body&lt;/td&gt;
&lt;td&gt;Requires controlled object access, expiry, and a carefully audited trust boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The asynchronous boundary is the default for long catalog recordings because it exposes the state needed for reconciliation.&lt;/p&gt;

&lt;p&gt;The catch is operational weight: a team must own durable state, stale-job detection, callback authentication if callbacks are used, and a sweeper that resolves accepted-but-not-completed work. It is not suitable when the provider offers only a synchronous contract and inputs are already small enough to fit a tightly bounded request. In that case, retain the synchronous adapter and enforce a conservative size limit before starting the upload.&lt;/p&gt;

&lt;p&gt;Segmentation is also a conditional choice, not an automatic rescue. Stick with whole-recording submission when product names, speaker turns, or facts depend on distant context and the provider accepts the payload. Choose segmentation only when the provider documents the relevant limits and the catalog can tolerate an explicit merge policy. Store segment ordering and time ranges beside every partial transcript so that a later adapter can't silently reorder evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a Node.js speech-to-text client retry a large multipart audio upload?
&lt;/h2&gt;

&lt;p&gt;The retry loop should consume decisions from a classifier, not treat every failure as temporary. A &lt;code&gt;429 Too Many Requests&lt;/code&gt; response may include &lt;code&gt;Retry-After&lt;/code&gt;; RFC 6585 defines status 429, and RFC 9110 defines &lt;code&gt;Retry-After&lt;/code&gt; as either a delay in seconds or an HTTP date. Respect a valid server value within the caller's overall deadline. For a locally aborted or disconnected request, mark the result ambiguous and reconcile before resubmitting. Validation and authorization responses require a configuration or input change, not backoff.&lt;/p&gt;

&lt;p&gt;Use exponential backoff with randomized jitter and a hard attempt ceiling, but regard those values as deployment policy rather than universal constants. The delay must fit inside an end-to-end deadline that includes queueing and reconciliation. A retry scheduled beyond that deadline becomes an expired operation, visible to operators; it must not wake later and mutate a catalog entry whose review window has closed.&lt;/p&gt;

&lt;p&gt;The following Go core is intentionally independent of HTTP libraries and vendors. A Node.js adapter can map &lt;code&gt;fetch&lt;/code&gt; responses and abort errors into these dispositions while preserving the same state machine. The synthetic &lt;code&gt;game-042&lt;/code&gt; fixture makes the audit fields concrete without presenting benchmark results or a production incident.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;transcription&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"crypto/sha256"&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/hex"&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
    &lt;span class="s"&gt;"math/rand/v2"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Disposition&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Accepted&lt;/span&gt; &lt;span class="n"&gt;Disposition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;iota&lt;/span&gt;
    &lt;span class="n"&gt;Retryable&lt;/span&gt;
    &lt;span class="n"&gt;Ambiguous&lt;/span&gt;
    &lt;span class="n"&gt;Terminal&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;AttemptResult&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Disposition&lt;/span&gt; &lt;span class="n"&gt;Disposition&lt;/span&gt;
    &lt;span class="n"&gt;RemoteID&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;RetryAfter&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Operation&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Key&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;CatalogID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Digest&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Attempt&lt;/span&gt;   &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;Deadline&lt;/span&gt;  &lt;span class="n"&gt;time&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;catalogID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audio&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="n"&gt;time&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="n"&gt;Operation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sum256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Operation&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;catalogID&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CatalogID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;catalogID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Digest&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Deadline&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;deadline&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;func&lt;/span&gt; &lt;span class="n"&gt;NextDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="n"&gt;Operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="n"&gt;AttemptResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="n"&gt;time&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&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;Disposition&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Accepted&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Disposition&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Terminal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&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="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"terminal submission result"&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;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Disposition&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Ambiguous&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&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="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"reconcile acceptance before replay"&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;op&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&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="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"attempt ceiling reached"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;capDelay&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attempt&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;capDelay&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capDelay&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="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int64N&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&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;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RetryAfter&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;delay&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="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RetryAfter&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&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;Add&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deadline&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="m"&gt;0&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="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"retry exceeds operation deadline"&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="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NextDelay&lt;/code&gt; deliberately refuses to replay an ambiguous attempt. That is the point most generic retry middleware gets wrong. Reconciliation may discover a remote operation and move the record to &lt;code&gt;accepted&lt;/code&gt;; if it finds authoritative evidence that no operation was accepted, the coordinator can append that evidence and authorize a new attempt. When the provider has no lookup or idempotency facility, the adapter's limitation must be explicit, and the downstream unique key becomes the final protection against duplicate catalog commits.&lt;/p&gt;

&lt;p&gt;Test the state machine with a synthetic matrix, not a single happy-path recording: encoded body just below and just above the configured limit; abort before the first body byte; disconnect after the body; &lt;code&gt;429&lt;/code&gt; with delta-seconds, HTTP-date, malformed, and missing &lt;code&gt;Retry-After&lt;/code&gt;; process restart after acceptance; duplicate callback; transcript with a mismatched source digest; and deadline expiry while queued. Inject the clock and random source so the suite asserts decisions without sleeping. Separately test the Node.js adapter with a local controlled server that can stop reading, delay headers, and close after receiving a complete body. Those tests verify client behavior; they do not invent claims about any provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Governance limits for the simpler design
&lt;/h2&gt;

&lt;p&gt;The rejected design is a global "large audio timeout" combined with automatic whole-request retries. It is attractive because it changes little code, but it erases the boundary between transfer and remote execution. It also lets multiple infrastructure layers replay the same logical command without sharing an idempotency record. For catalog enrichment, where a transcript can trigger durable product changes, that ambiguity is unacceptable.&lt;/p&gt;

&lt;p&gt;There is a valid use case for the simpler design: an internal, synchronous transcription service with small bounded inputs, a documented maximum body size, one retry owner, and an idempotent server contract.&lt;/p&gt;

&lt;p&gt;Keep it there.&lt;/p&gt;

&lt;p&gt;Once recordings are long, processing duration varies, or provider portability is a requirement, use an adapter plus durable operation state and make acceptance independently observable.&lt;/p&gt;

&lt;p&gt;The final selection criterion is evidence, not nominal timeout length. A portable adapter must prove which bytes were submitted, which attempt was accepted, which transcript passed validation, and which catalog revision consumed it. Cost and latency still matter, but compare them only after every candidate can meet that correctness contract under aborts, duplicates, restarts, and late results.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/api/globals.html#fetch" rel="noopener noreferrer"&gt;https://nodejs.org/api/globals.html#fetch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/api/globals.html#static-method-abortsignaltimeoutdelay" rel="noopener noreferrer"&gt;https://nodejs.org/api/globals.html#static-method-abortsignaltimeoutdelay&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7578" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc7578&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6585#section-4" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc6585#section-4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-retry-after" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc9110.html#name-retry-after&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>node</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
