<?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: Daniel Neata</title>
    <description>The latest articles on DEV Community by Daniel Neata (@danielmason77).</description>
    <link>https://dev.to/danielmason77</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%2F4184538%2F1ba4abb0-5ae5-4bcb-b8e2-bbe30bec3f44.png</url>
      <title>DEV Community: Daniel Neata</title>
      <link>https://dev.to/danielmason77</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danielmason77"/>
    <language>en</language>
    <item>
      <title>Failure Recovery for Publishing APIs: Reconcile Before Retrying</title>
      <dc:creator>Daniel Neata</dc:creator>
      <pubDate>Sat, 17 Oct 2026 14:02:54 +0000</pubDate>
      <link>https://dev.to/danielmason77/failure-recovery-for-publishing-apis-reconcile-before-retrying-3b</link>
      <guid>https://dev.to/danielmason77/failure-recovery-for-publishing-apis-reconcile-before-retrying-3b</guid>
      <description>&lt;h1&gt;
  
  
  Failure Recovery for Publishing APIs: Reconcile Before Retrying
&lt;/h1&gt;

&lt;p&gt;A network timeout does not tell you whether a publishing request failed. The remote service may have rejected the request, accepted it and lost the response, or created the record while your process was already disconnecting. Retrying immediately can turn one uncertain delivery into two public posts.&lt;/p&gt;

&lt;p&gt;This guide presents a limited recovery pattern for workers that publish approved content to remote APIs. The design combines stable operation keys, explicit delivery states and reconciliation against remote history. It is intended for systems that can search recent records or attach a client-supplied identifier. If the destination provides neither capability, the worker must remain conservative and escalate uncertain writes instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model uncertainty as a real state
&lt;/h2&gt;

&lt;p&gt;A delivery table often begins with pending, published and failed. That model is incomplete because a transport error can leave the remote result unknown.&lt;/p&gt;

&lt;p&gt;Add an uncertain state. Enter it when the request may have reached the destination but the response cannot be trusted. Do not automatically return an uncertain item to the normal retry queue.&lt;/p&gt;

&lt;p&gt;A useful record contains the internal content ID, destination, operation key, request start time, response status when known, remote ID, public URL and last reconciliation time. Store error summaries, but never store access tokens or complete authorization headers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a stable operation key
&lt;/h2&gt;

&lt;p&gt;Generate the operation key before the first network request and persist it with the delivery attempt. It must remain stable across process restarts and operator-approved retries.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;operationKey&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;revision&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="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contentId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The revision matters. A corrected article can be a legitimate new operation, while restarting the worker for the same revision should not create another delivery.&lt;/p&gt;

&lt;p&gt;If the API supports an idempotency header, send this key in the documented field. That is the strongest option because the destination can reject duplicate requests. Keep local reconciliation anyway; providers can have retention windows or endpoint-specific behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate selection from delivery
&lt;/h2&gt;

&lt;p&gt;Claim one approved item transactionally before making a remote call. The claim prevents two runners from selecting the same item at the same time.&lt;/p&gt;

&lt;p&gt;The worker should then use a small state machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beginAttempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;operationKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markPublished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;remoteId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;publicUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;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="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isDefiniteRejection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markRejected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;safeSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markUncertain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;safeSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A validated 400 response is normally a definite rejection. A timeout, connection reset or invalid response body after the request was sent is not. A 500 response may also be uncertain if the provider could have completed the write before returning the error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile before any retry
&lt;/h2&gt;

&lt;p&gt;A reconciliation worker reads uncertain attempts and searches the destination using the strongest available evidence.&lt;/p&gt;

&lt;p&gt;First, search by operation key when the platform exposes custom fields or request metadata. Otherwise, compare a bounded time window using destination account, normalized title or text, source URL and creation time. Require more than one weak attribute before declaring a match.&lt;/p&gt;

&lt;p&gt;Do not compare only the first few recent posts. Paginate until the search crosses the attempt time plus a documented safety margin. Stop at a hard page limit and report that history verification was incomplete.&lt;/p&gt;

&lt;p&gt;When exactly one credible match exists, save its remote ID and URL and mark the attempt published. When no match exists and the provider's consistency delay has passed, move the attempt to retry-approved rather than publishing immediately. When several matches exist, flag the attempt for review because a duplicate may already exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make retry a deliberate transition
&lt;/h2&gt;

&lt;p&gt;Only attempts in retry-approved may return to the delivery worker. Reuse the original operation key. Record who or what approved the retry and the evidence produced during reconciliation.&lt;/p&gt;

&lt;p&gt;Limit attempts with a small retry budget and exponential backoff. Backoff protects the provider, but it does not solve duplication by itself. The safety property comes from stable identity and reconciliation.&lt;/p&gt;

&lt;p&gt;For systems without remote search or idempotency support, the correct automatic action after an uncertain write may be no action. Manual inspection is slower than a retry, but safer than publishing repeated content under an account you cannot reconcile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the response before reporting success
&lt;/h2&gt;

&lt;p&gt;A successful HTTP status is not enough. Require the documented identifier field and validate its type. Prefer the public URL supplied by the API. If the service returns only an ID, perform a follow-up read and use the verified URL from that response.&lt;/p&gt;

&lt;p&gt;Keep publication counts tied to these receipts. Authentication checks, queue polling and dry runs should never increment a published counter.&lt;/p&gt;

&lt;p&gt;Return a structured result from every adapter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"published"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operationKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stable-key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"remoteId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"remote-record-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publicUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.test/posts/remote-record-id"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The URL is illustrative. A production adapter must use the actual destination response or a verified lookup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the difficult boundaries
&lt;/h2&gt;

&lt;p&gt;Use a fake adapter that records requests and can simulate a timeout after creating a remote record. The first run should leave the attempt uncertain. Reconciliation should discover the fake record, and the retry worker should make no second publish call.&lt;/p&gt;

&lt;p&gt;Add tests for a definite rejection, an uncertain attempt with no remote match, paginated history, multiple candidate matches and a process crash after the remote write but before the local commit.&lt;/p&gt;

&lt;p&gt;Also test two concurrent workers. Only one should acquire the claim. A reliable single-worker test does not prove that scheduled jobs cannot overlap in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operate with evidence
&lt;/h2&gt;

&lt;p&gt;A useful operational report separates four facts: the scheduler ran, a candidate was approved, the remote write was attempted, and a public record was verified. A green workflow badge proves only the first fact.&lt;/p&gt;

&lt;p&gt;Log the operation key, internal content ID, outcome and remote receipt. Keep credentials redacted. Alert when uncertain attempts exceed their reconciliation window or when remote history cannot be read completely.&lt;/p&gt;

&lt;p&gt;Recovery is not the same as repeating the request. The safe order is identify the intended operation, inspect the destination, reconcile the result and only then decide whether a retry is justified.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>webdev</category>
      <category>github</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>API Publishing Deduplication with Failure Recovery: Reconcile Before You Retry</title>
      <dc:creator>Daniel Neata</dc:creator>
      <pubDate>Fri, 16 Oct 2026 15:40:38 +0000</pubDate>
      <link>https://dev.to/danielmason77/api-publishing-deduplication-with-failure-recovery-reconcile-before-you-retry-2m28</link>
      <guid>https://dev.to/danielmason77/api-publishing-deduplication-with-failure-recovery-reconcile-before-you-retry-2m28</guid>
      <description>&lt;h1&gt;
  
  
  API Publishing Deduplication with Failure Recovery: Reconcile Before You Retry
&lt;/h1&gt;

&lt;p&gt;A publishing job sends a request. The connection closes before a response arrives. Should the job send the same request again?&lt;/p&gt;

&lt;p&gt;The answer depends on something the worker does not yet know: whether the remote platform accepted the first request. A network timeout describes the worker's observation. It does not prove that publication failed. Treating every timeout as permission to retry is an easy way to create duplicate posts.&lt;/p&gt;

&lt;p&gt;This guide describes a small worker design. It is an implementation pattern, not a claim that every publishing platform offers the same API features. Adapt the remote lookup and idempotency mechanism to the platform you actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate content identity from delivery attempts
&lt;/h2&gt;

&lt;p&gt;Give each reviewed content item a stable internal identifier. Then create a separate delivery record for each destination account. The combination of content ID, platform and account identifies the intended publication; an attempt ID identifies one execution of the worker.&lt;/p&gt;

&lt;p&gt;Do not generate a new content ID merely because a request failed. That would make a retry look like a new publication. Likewise, do not use the current timestamp as the only identity: each execution would receive a different one.&lt;/p&gt;

&lt;p&gt;A minimal delivery record might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"guide-2026-10-16-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example-network"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accountId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"brand-account"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approved"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"externalId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastAttemptId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enforce uniqueness for that destination identity in the database. Application-level checks alone can race when two workers read the same queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claim work atomically
&lt;/h2&gt;

&lt;p&gt;The worker should change an eligible delivery from approved to claimed in one atomic operation. If another worker has already claimed it, stop processing that delivery. A read followed by an unrelated write leaves a gap in which both workers can believe they own the item.&lt;/p&gt;

&lt;p&gt;Keep the claim's timestamp and worker identifier. A crashed worker can leave a claim behind, so a recovery process needs to identify stale claims. However, an expired claim is not itself permission to publish again. The first worker might have reached the remote platform before it crashed.&lt;/p&gt;

&lt;p&gt;When recovering an expired claim, move it into reconciliation. Check remote state before creating another public post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model uncertainty as its own state
&lt;/h2&gt;

&lt;p&gt;Useful states include approved, claimed, published, rejected and uncertain. Define their transitions explicitly. In particular, do not collapse uncertain into rejected simply to make the retry queue easier to implement.&lt;/p&gt;

&lt;p&gt;Examples help distinguish them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A local validation failure before any network request is a known failure.&lt;/li&gt;
&lt;li&gt;A confirmed remote rejection can be recorded as rejected, following that API's documented response semantics.&lt;/li&gt;
&lt;li&gt;A timeout after sending a publication request is uncertain.&lt;/li&gt;
&lt;li&gt;A remote ID followed by a failed database update means the remote publication succeeded while local recording failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last case deserves special handling. Record the external ID in durable logs or another recovery store before acknowledging the job. Do not issue a second publication request just because the final database write needs retrying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile using the strongest available evidence
&lt;/h2&gt;

&lt;p&gt;If the platform provides an idempotency key for this exact operation, reuse a stable key according to its documented scope and retention rules. Do not assume an arbitrary request header creates idempotency.&lt;/p&gt;

&lt;p&gt;If you have an external ID, query that resource directly. Otherwise, use an authenticated listing for the correct account and compare the intended publication with remote records. A stable marker can help where supported. Exact text alone is weaker evidence: legitimate posts can share wording, and platforms can transform URLs or formatting.&lt;/p&gt;

&lt;p&gt;Read all pages needed to cover the relevant history. If pagination stops unexpectedly, authentication fails or a safety limit is reached, preserve the uncertain state. An incomplete search cannot establish absence. It is safer to surface that delivery for investigation than to silently duplicate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry the failed stage
&lt;/h2&gt;

&lt;p&gt;Structure the implementation around stages rather than wrapping the entire worker in a broad retry loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;recover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;store&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;externalId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;externalId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;confirmPublished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findDelivery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markUncertain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;confirmPublished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markForReviewedRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example deliberately does not publish. It resolves what is known and hands a confirmed absence to the retry policy. A production adapter must define what a complete search means and how it handles eventual visibility of newly created posts.&lt;/p&gt;

&lt;p&gt;Use bounded backoff for transient read failures. Avoid repeatedly querying a platform in a tight loop. Authentication problems need credential recovery, not more publication attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the failure between each pair of stages
&lt;/h2&gt;

&lt;p&gt;The most useful tests interrupt the worker at meaningful boundaries: after the claim, after the remote write, and before local confirmation. Simulate a remote service that creates the post but throws a timeout to the caller. Recovery should discover the existing publication and avoid a second write.&lt;/p&gt;

&lt;p&gt;Also test concurrent claims, multi-page history, an incomplete history response and a failed local confirmation. Count remote writes in the test, not just successful worker exits. A worker can exit successfully after publishing nothing, or fail after publishing something; neither exit code alone tells the full story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Report publication separately from job health
&lt;/h2&gt;

&lt;p&gt;For each run, report selected, published, rejected, uncertain and skipped counts. Include the external ID and public URL when available. A green scheduler badge means the job completed according to its exit policy. It should never be presented as proof that a new post exists.&lt;/p&gt;

&lt;p&gt;Keep credentials out of logs, and retain enough non-secret delivery metadata to reconcile an interrupted attempt. This approach makes recovery a controlled state transition instead of a guess. It also makes a quiet day explainable: an empty reviewed queue is a content-supply issue, while an uncertain publication is a delivery issue. Each needs a different fix.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>webdev</category>
      <category>github</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>API Authentication Across Multiple Platforms: A Practical Implementation Guide</title>
      <dc:creator>Daniel Neata</dc:creator>
      <pubDate>Thu, 15 Oct 2026 14:16:53 +0000</pubDate>
      <link>https://dev.to/danielmason77/api-authentication-across-multiple-platforms-a-practical-implementation-guide-4i0f</link>
      <guid>https://dev.to/danielmason77/api-authentication-across-multiple-platforms-a-practical-implementation-guide-4i0f</guid>
      <description>&lt;h1&gt;
  
  
  API Authentication Across Multiple Platforms: A Practical Implementation Guide
&lt;/h1&gt;

&lt;p&gt;Publishing to several platforms from one GitHub Actions repository sounds simple: store tokens, call APIs, and schedule a job. The difficult part is not sending the request. It is proving that a post was created exactly once, recovering when authentication expires, and keeping credentials out of logs. This guide walks through a small Node.js publisher design suitable for Bluesky, Blogger, and other OAuth-backed APIs. The examples are illustrative; endpoint contracts differ by provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate credentials by destination
&lt;/h2&gt;

&lt;p&gt;Treat every platform as its own security boundary. A Bluesky app password is not an OAuth refresh token, and a Blogger refresh token cannot be handled as a short-lived access token. Use individual GitHub Actions secrets for each credential. Give each token the minimum scopes needed to create posts. Do not embed tokens in article content, URLs, build artifacts, or debug output.&lt;/p&gt;

&lt;p&gt;Use environment variables only at the step that needs them. Avoid printing the complete environment. If an API returns an authentication error, log the HTTP status and a redacted provider error code rather than the raw response body, which can occasionally contain sensitive values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build one timeout-aware request helper
&lt;/h2&gt;

&lt;p&gt;Network requests can hang or fail after the remote service has accepted a write. Always set a timeout, distinguish read operations from writes, and do not automatically retry a timed-out create request. Node.js 20+ provides AbortSignal.timeout, which keeps the example dependency-free.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;apiRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

      &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt;

    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AUTH_REQUIRED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RATE_LIMITED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`API_HTTP_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

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

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

&lt;/div&gt;



&lt;p&gt;This helper deliberately avoids logging the request URL because some legacy APIs accept credentials in query parameters. Prefer Authorization headers whenever supported. Rate-limit responses should be handled according to the provider's Retry-After guidance, with a bounded retry policy for safe reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep authentication lifecycle separate from publication
&lt;/h2&gt;

&lt;p&gt;For OAuth providers, access tokens typically expire before refresh tokens. A refresh operation is a credential-management step, not a reason to retry a post blindly. Refresh securely, then inspect remote publication history before deciding whether a previous attempt needs to be repeated. For app-password systems, create a session for each short-lived job and discard the session afterward.&lt;/p&gt;

&lt;p&gt;Record the destination, account identifier, and token expiry where appropriate, but never the token itself. If authentication fails, stop the workflow with a nonzero exit code. A green workflow that silently skipped a broken publisher is a misleading operational signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the queue claim atomic
&lt;/h2&gt;

&lt;p&gt;Two GitHub scheduled jobs may overlap because cron runs can be delayed. The database should offer an atomic transition from approved to claimed, so that only one worker owns a post. A plain GET followed by POST is not sufficient protection against a race condition. If the queue is empty, an optional curated fallback may run, but only after it confirms the intended account and scans for previously published content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design publication as a state machine
&lt;/h2&gt;

&lt;p&gt;Use explicit states: approved, claimed, published, and failed. Save the remote ID and URL after the provider confirms creation. A remote ID is stronger evidence than a successful process exit; a publicly accessible post URL is stronger still. A network timeout during a create request is an &lt;em&gt;uncertain outcome&lt;/em&gt;, not proof of failure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;publishClaimed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;known&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByFingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;known&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remoteId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;known&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NO_REMOTE_ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remoteId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// A write may have succeeded before the connection failed.&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TimeoutError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbortError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uncertain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CHECK_REMOTE_BEFORE_RETRY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

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

    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

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

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

&lt;/div&gt;



&lt;p&gt;The provider adapter must implement findByFingerprint using the platform's actual read API. This example is a design pattern, not a drop-in implementation: some providers do not support a fingerprint query. In those cases, scan the account's recent posts, store a durable local publication ledger, or use an idempotency key if the API supports one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle pagination and duplicate detection honestly
&lt;/h2&gt;

&lt;p&gt;Fetching the first 100 posts is not a complete history check for a long-lived account. Follow pagination until you have covered the entire relevant window, or stop publishing if you cannot establish that a candidate is unused. A fixed list of ten fallback messages will eventually be exhausted. Treat exhaustion as a normal no-op and generate new editorially reviewed content separately; do not silently cycle the same copy.&lt;/p&gt;

&lt;p&gt;Use a stable fingerprint derived from normalized text and destination URL. Preserve the original text for exact comparisons when platforms expose it. A cryptographic hash can identify your own content, but a remote API cannot search by that hash unless you also store a ledger or add an allowed marker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure GitHub Actions to fail visibly
&lt;/h2&gt;

&lt;p&gt;Run syntax checks and dry-run authentication on code changes. Reserve live publication for a scheduled or explicitly authorized job. GitHub cron is best-effort: it can start late, so evaluate publishing windows in the intended timezone and account for daylight saving time. Avoid broad windows that let multiple delayed runs publish the same fallback post without a shared lock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Safe publisher&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

      &lt;span class="na"&gt;dry_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;

        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;

        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;24'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node --check publisher.js&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run publisher&lt;/span&gt;

        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

          &lt;span class="na"&gt;PLATFORM_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PLATFORM_TOKEN }}&lt;/span&gt;

          &lt;span class="na"&gt;DRY_RUN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.dry_run }}&lt;/span&gt;

        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node publisher.js&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Never interpret a successful syntax check or dry run as a published post. For live runs, emit structured, non-sensitive fields such as platform, slot, claimed item ID, remote post ID, public URL, and final outcome. The monitoring process should flag missing IDs and repeated empty queues without inventing publication success.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the failure paths before enabling automation
&lt;/h2&gt;

&lt;p&gt;Test with an invalid token, an empty queue, a 429 response, a delayed cron execution, and a simulated timeout immediately after a write. Also test the case where the remote post exists but the local database update fails. In that case, reconcile the remote ID instead of posting again. Keep dry runs side-effect-free, including avoiding state changes that would consume queued content.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical operating checklist
&lt;/h2&gt;

&lt;p&gt;Before enabling a new destination, verify the account identity, permissions, token rotation process, duplicate detection, timeout behavior, scheduled timezone, and evidence logs. Confirm that an error produces a failed job and that a genuinely empty queue produces an explicit no-op. Review published links periodically, because an API response can succeed even if a platform later moderates or removes content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Reliable multi-platform publishing depends on state, not just schedules. Keep secrets isolated, make queue claims atomic, reconcile uncertain writes, and require remote evidence before calling a publication successful. Those controls are less glamorous than adding another social network, but they are what make unattended automation maintainable.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/danielmason77/danielmason-threads-publisher/blob/main/content-library/dev-community/api-authentication-across-multiple-platforms-a-practical-implementation-guide.md" rel="noopener noreferrer"&gt;https://github.com/danielmason77/danielmason-threads-publisher/blob/main/content-library/dev-community/api-authentication-across-multiple-platforms-a-practical-implementation-guide.md&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>webdev</category>
      <category>github</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
