<?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: unifyport</title>
    <description>The latest articles on DEV Community by unifyport (@unifyport_cool).</description>
    <link>https://dev.to/unifyport_cool</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%2F4037236%2F4426e4ea-496c-49b5-a563-62d96b167f66.png</url>
      <title>DEV Community: unifyport</title>
      <link>https://dev.to/unifyport_cool</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/unifyport_cool"/>
    <language>en</language>
    <item>
      <title>LINE Service Messages vs Messaging API: Choose the Right Message Path</title>
      <dc:creator>unifyport</dc:creator>
      <pubDate>Fri, 07 Aug 2026 05:43:02 +0000</pubDate>
      <link>https://dev.to/unifyport/line-service-messages-vs-messaging-api-choose-the-right-message-path-2209</link>
      <guid>https://dev.to/unifyport/line-service-messages-vs-messaging-api-choose-the-right-message-path-2209</guid>
      <description>&lt;p&gt;LINE MINI App Service Messages and the LINE Messaging API can both deliver messages to users, but they solve different problems.&lt;/p&gt;

&lt;p&gt;A Service Message confirms or follows up on an action completed inside a LINE MINI App. A Messaging API message comes from a LINE Official Account and supports conversations, direct outreach, and audience messaging.&lt;/p&gt;

&lt;p&gt;They differ in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sender identity&lt;/li&gt;
&lt;li&gt;Trigger&lt;/li&gt;
&lt;li&gt;Recipient model&lt;/li&gt;
&lt;li&gt;Token lifecycle&lt;/li&gt;
&lt;li&gt;Message format&lt;/li&gt;
&lt;li&gt;Review requirements&lt;/li&gt;
&lt;li&gt;Chat room&lt;/li&gt;
&lt;li&gt;Pricing&lt;/li&gt;
&lt;li&gt;Content policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choosing the wrong path can result in rejected templates, unusable tokens, duplicate notifications, or an architecture that cannot support customer conversations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;Use a LINE MINI App Service Message when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user completed an action inside the MINI App&lt;/li&gt;
&lt;li&gt;The message confirms, reports, or reminds the user about that action&lt;/li&gt;
&lt;li&gt;The MINI App is verified&lt;/li&gt;
&lt;li&gt;The template has passed review&lt;/li&gt;
&lt;li&gt;You have a valid service notification token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the Messaging API when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The sender should be a LINE Official Account&lt;/li&gt;
&lt;li&gt;You need to reply to a user conversation&lt;/li&gt;
&lt;li&gt;You need push, multicast, narrowcast, or broadcast delivery&lt;/li&gt;
&lt;li&gt;You need flexible message objects&lt;/li&gt;
&lt;li&gt;You are operating under the Official Account recipient, quota, and pricing model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One API is not an upgraded version of the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare the two paths
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision point&lt;/th&gt;
&lt;th&gt;Service Message API&lt;/th&gt;
&lt;th&gt;Messaging API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Product&lt;/td&gt;
&lt;td&gt;LINE MINI App&lt;/td&gt;
&lt;td&gt;LINE Official Account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;Transactional action confirmation, result, or reminder&lt;/td&gt;
&lt;td&gt;Conversation, support, outreach, and audience delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;User action inside the MINI App&lt;/td&gt;
&lt;td&gt;User webhook event or application decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recipient&lt;/td&gt;
&lt;td&gt;User associated with a service notification token&lt;/td&gt;
&lt;td&gt;User, group, chat, audience, or Official Account friends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production requirement&lt;/td&gt;
&lt;td&gt;Verified MINI App and reviewed template&lt;/td&gt;
&lt;td&gt;Messaging API channel connected to an Official Account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message design&lt;/td&gt;
&lt;td&gt;Reviewed LINE-provided template&lt;/td&gt;
&lt;td&gt;Text, image, video, Flex, template, sticker, location, and other objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chat room&lt;/td&gt;
&lt;td&gt;Regional MINI App notice chat&lt;/td&gt;
&lt;td&gt;Chat with the Official Account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketing&lt;/td&gt;
&lt;td&gt;Prohibited&lt;/td&gt;
&lt;td&gt;Supported through eligible methods and applicable rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message limit&lt;/td&gt;
&lt;td&gt;Normally up to five per qualifying action&lt;/td&gt;
&lt;td&gt;Monthly allowance, endpoint limits, and account plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing&lt;/td&gt;
&lt;td&gt;Described by LINE as free&lt;/td&gt;
&lt;td&gt;Depends on market and Official Account plan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The official boundaries are documented in LINE's &lt;a href="https://developers.line.biz/en/docs/line-mini-app/develop/service-messages/" rel="noopener noreferrer"&gt;Service Message guide&lt;/a&gt; and &lt;a href="https://developers.line.biz/en/docs/messaging-api/sending-messages/" rel="noopener noreferrer"&gt;Messaging API sending guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service Messages begin with a MINI App action
&lt;/h2&gt;

&lt;p&gt;A Service Message should not exist without a specific action performed by the user inside the LINE MINI App.&lt;/p&gt;

&lt;p&gt;Appropriate examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reservation confirmation&lt;/li&gt;
&lt;li&gt;Order confirmation&lt;/li&gt;
&lt;li&gt;Check-in result&lt;/li&gt;
&lt;li&gt;Shipment completion&lt;/li&gt;
&lt;li&gt;Reservation reminder&lt;/li&gt;
&lt;li&gt;Reminder for a purchased ticket&lt;/li&gt;
&lt;li&gt;Queue-status update tied to a submitted request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The relationship should be explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User action
    ↓
Business transaction
    ↓
Service notification token
    ↓
Approved template
    ↓
Confirmation, result, or reminder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following are not valid Service Message use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;General promotions&lt;/li&gt;
&lt;li&gt;Discount campaigns&lt;/li&gt;
&lt;li&gt;Coupons&lt;/li&gt;
&lt;li&gt;Shopping rewards&lt;/li&gt;
&lt;li&gt;New-product announcements&lt;/li&gt;
&lt;li&gt;Unrelated event notifications&lt;/li&gt;
&lt;li&gt;Messages triggered by actions outside the MINI App&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verification does not remove these restrictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Messaging API messages begin with an Official Account
&lt;/h2&gt;

&lt;p&gt;The Messaging API provides these primary delivery methods:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reply&lt;/td&gt;
&lt;td&gt;Respond to a webhook event from a user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push&lt;/td&gt;
&lt;td&gt;Send to an eligible user, group, or multi-person chat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multicast&lt;/td&gt;
&lt;td&gt;Send to a specified list of user IDs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Narrowcast&lt;/td&gt;
&lt;td&gt;Send to an audience or demographic segment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broadcast&lt;/td&gt;
&lt;td&gt;Send to all friends of the Official Account&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It also supports a broader set of message objects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Text&lt;/li&gt;
&lt;li&gt;Text v2&lt;/li&gt;
&lt;li&gt;Image&lt;/li&gt;
&lt;li&gt;Video&lt;/li&gt;
&lt;li&gt;Audio&lt;/li&gt;
&lt;li&gt;Sticker&lt;/li&gt;
&lt;li&gt;Location&lt;/li&gt;
&lt;li&gt;Imagemap&lt;/li&gt;
&lt;li&gt;Template&lt;/li&gt;
&lt;li&gt;Flex Message&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the Messaging API appropriate for customer support, chatbots, campaigns, and Official Account communication.&lt;/p&gt;

&lt;p&gt;However, it follows the recipient, friendship, quota, rate-limit, and pricing rules of the Official Account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the token boundary
&lt;/h2&gt;

&lt;p&gt;The most common implementation error is treating all LINE tokens as interchangeable.&lt;/p&gt;

&lt;p&gt;They are not.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Credential or identifier&lt;/th&gt;
&lt;th&gt;Belongs to&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LIFF access token&lt;/td&gt;
&lt;td&gt;Current MINI App user session&lt;/td&gt;
&lt;td&gt;Helps issue the first service notification token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MINI App channel access token&lt;/td&gt;
&lt;td&gt;MINI App channel&lt;/td&gt;
&lt;td&gt;Authenticates Service Message API calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service notification token&lt;/td&gt;
&lt;td&gt;One MINI App user and action flow&lt;/td&gt;
&lt;td&gt;Sends reviewed Service Messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Messaging API channel access token&lt;/td&gt;
&lt;td&gt;Official Account channel&lt;/td&gt;
&lt;td&gt;Authenticates Messaging API requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reply token&lt;/td&gt;
&lt;td&gt;One eligible webhook event&lt;/td&gt;
&lt;td&gt;Sends a reply message once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LINE user ID&lt;/td&gt;
&lt;td&gt;User within a channel context&lt;/td&gt;
&lt;td&gt;Targets eligible Messaging API methods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audience ID&lt;/td&gt;
&lt;td&gt;Official Account audience&lt;/td&gt;
&lt;td&gt;Targets narrowcast operations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A Service notification token is not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A permanent user ID&lt;/li&gt;
&lt;li&gt;A Messaging API reply token&lt;/li&gt;
&lt;li&gt;An Official Account push target&lt;/li&gt;
&lt;li&gt;A reusable cross-user credential&lt;/li&gt;
&lt;li&gt;A general-purpose chat identity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep each token type in a separate model and storage path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Service Message token flow
&lt;/h2&gt;

&lt;p&gt;The first Service Message normally starts with a LIFF access token obtained during the user action.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MINI App
    ↓ liff.getAccessToken()
Backend
    ↓ POST /message/v3/notifier/token
Service notification token
    ↓ POST /message/v3/notifier/send?target=service
Service Message
    ↓
Renewed service notification token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LINE recommends stateless channel access tokens for LINE MINI App channels. Long-lived and v2.1 channel access tokens cannot be used for MINI App channels.&lt;/p&gt;

&lt;p&gt;The Service Message API returns state that must be preserved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ServiceNotificationState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;businessActionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userReference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;notificationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;remainingCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;Important characteristics include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The token is associated with one user&lt;/li&gt;
&lt;li&gt;It normally permits up to five messages for the approved action&lt;/li&gt;
&lt;li&gt;It expires one year after issuance&lt;/li&gt;
&lt;li&gt;The token value is renewed after a successful send&lt;/li&gt;
&lt;li&gt;The renewed token must be used for the next message&lt;/li&gt;
&lt;li&gt;One LIFF access token can issue only one Service notification token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not store the token in browser analytics or application logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persist the renewed token atomically
&lt;/h2&gt;

&lt;p&gt;Two workers must not send with the same Service notification token at the same time.&lt;/p&gt;

&lt;p&gt;A safe operation looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;sendActionUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;actionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirmation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reminder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;templateName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&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;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lockServiceNotificationState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actionId&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;idempotencyKey&lt;/span&gt; &lt;span class="o"&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actionId&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;templateName&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&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;tx&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;idempotencyKey&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;existing&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;existing&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remainingCount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;service_message_count_exhausted&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="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="nx"&gt;lineMiniApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendServiceMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;notificationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notificationToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;templateName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;templateName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&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;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateServiceNotificationState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;actionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;notificationToken&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;notificationToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;remainingCount&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;remainingCount&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;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDelivery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;line_service_message&lt;/span&gt;&lt;span class="dl"&gt;"&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="s2"&gt;sent&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact transaction implementation depends on your database and queue, but the invariants are the same:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lock the action state&lt;/li&gt;
&lt;li&gt;Check idempotency&lt;/li&gt;
&lt;li&gt;Send once&lt;/li&gt;
&lt;li&gt;Save the renewed token&lt;/li&gt;
&lt;li&gt;Save the new remaining count&lt;/li&gt;
&lt;li&gt;Commit together&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the request result is uncertain, reconcile it before retrying blindly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reply tokens have a different lifecycle
&lt;/h2&gt;

&lt;p&gt;Messaging API reply tokens arrive in eligible webhook events.&lt;/p&gt;

&lt;p&gt;They are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tied to one event&lt;/li&gt;
&lt;li&gt;Usable only once&lt;/li&gt;
&lt;li&gt;Intended to be used immediately&lt;/li&gt;
&lt;li&gt;Not interchangeable with Service notification tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A reply flow looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User sends message
       ↓
Official Account webhook
       ↓
Reply token
       ↓
POST /v2/bot/message/reply
       ↓
Reply in Official Account chat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the response is generated too late or the reply token has already been consumed, your application must evaluate whether an eligible push message is appropriate.&lt;/p&gt;

&lt;p&gt;Do not store a reply token as a permanent conversation address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat templates differently
&lt;/h2&gt;

&lt;p&gt;Service Messages use LINE-provided templates that must pass review.&lt;/p&gt;

&lt;p&gt;Your application supplies approved variables and permanent links:&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;"templateName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reservation_confirmation_en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reservation_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"R-20260807-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reservation_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-08 19:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"button_uri_1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reservation/detail?id=R-20260807-001"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The template must remain connected to the reviewed action.&lt;/p&gt;

&lt;p&gt;Messaging API message objects are more flexible:&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;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER_ID_PLACEHOLDER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"How can we help with your reservation?"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This flexibility does not mean every recipient is eligible for every send method. Recipient and friendship rules still apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not send the same message twice by default
&lt;/h2&gt;

&lt;p&gt;A product that uses both APIs can accidentally send duplicate updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reservation completed
       ├── Service Message
       └── Messaging API push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unless duplication has a deliberate product reason, select one delivery path per purpose.&lt;/p&gt;

&lt;p&gt;A routing policy can be explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MessagePurpose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transaction_confirmation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transaction_result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transaction_reminder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;support_reply&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;support_follow_up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;marketing_campaign&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;selectLineMessagePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MessagePurpose&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="s2"&gt;service_message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;messaging_api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transaction_confirmation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transaction_result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transaction_reminder&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;service_message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;support_reply&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;support_follow_up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;marketing_campaign&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;messaging_api&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final implementation must still check verification, template, recipient, consent, and quota requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Join both paths with your own business ID
&lt;/h2&gt;

&lt;p&gt;A Service notification token is not a customer-support identity.&lt;/p&gt;

&lt;p&gt;If a customer receives a reservation confirmation and later starts a support conversation, correlate both paths with your own transaction model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CustomerInteraction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;businessActionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reservationId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;lineMiniAppUserReference&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;officialAccountUserId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;supportConversationId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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;Do not assume identifiers from different LINE channels or products are globally interchangeable.&lt;/p&gt;

&lt;p&gt;Your application should own the relationship between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer&lt;/li&gt;
&lt;li&gt;Order or reservation&lt;/li&gt;
&lt;li&gt;MINI App action&lt;/li&gt;
&lt;li&gt;Service Message delivery&lt;/li&gt;
&lt;li&gt;Official Account conversation&lt;/li&gt;
&lt;li&gt;Support ticket&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recommended hybrid architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LINE MINI App action
        ↓
Transaction service
        ↓
Service Message worker
        ↓
Regional MINI App notice chat

Customer support message
        ↓
Official Account webhook
        ↓
Conversation service
        ↓
Messaging API reply

Both paths
        ↓
Shared customer/order database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Credentials&lt;/li&gt;
&lt;li&gt;Token storage&lt;/li&gt;
&lt;li&gt;Queues&lt;/li&gt;
&lt;li&gt;Delivery logs&lt;/li&gt;
&lt;li&gt;Retry policies&lt;/li&gt;
&lt;li&gt;Rate and quota monitoring&lt;/li&gt;
&lt;li&gt;Compliance rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Share only stable business context such as an internal order or reservation ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Using the wrong token
&lt;/h3&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unauthorized, invalid token, or recipient error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check whether the request used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A LIFF access token&lt;/li&gt;
&lt;li&gt;A MINI App channel access token&lt;/li&gt;
&lt;li&gt;A Service notification token&lt;/li&gt;
&lt;li&gt;A Messaging API channel access token&lt;/li&gt;
&lt;li&gt;A reply token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Token type should be part of your request tracing metadata, but never log the token value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Losing the renewed Service notification token
&lt;/h3&gt;

&lt;p&gt;Symptom:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First notification succeeds&lt;/li&gt;
&lt;li&gt;The next notification fails&lt;/li&gt;
&lt;li&gt;Stored &lt;code&gt;remainingCount&lt;/code&gt; does not match the latest response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cause:&lt;/p&gt;

&lt;p&gt;The application continued using the previous token value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reusing a token concurrently
&lt;/h3&gt;

&lt;p&gt;Symptom:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two workers process the same action&lt;/li&gt;
&lt;li&gt;One succeeds and one fails&lt;/li&gt;
&lt;li&gt;The next token state becomes unclear&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix:&lt;/p&gt;

&lt;p&gt;Use an action-level lock, idempotency key, and atomic token update.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Service Messages for marketing
&lt;/h3&gt;

&lt;p&gt;Symptom:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Template review fails&lt;/li&gt;
&lt;li&gt;Production use is restricted&lt;/li&gt;
&lt;li&gt;Message content no longer matches the approved action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix:&lt;/p&gt;

&lt;p&gt;Move promotional communication to an eligible Official Account method and follow its rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treating a push message as a guaranteed alternative
&lt;/h3&gt;

&lt;p&gt;Symptom:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Service Message path fails&lt;/li&gt;
&lt;li&gt;The application attempts a push message&lt;/li&gt;
&lt;li&gt;The user is not an eligible Messaging API recipient&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix:&lt;/p&gt;

&lt;p&gt;Evaluate recipient eligibility before designing a fallback. The two paths do not have the same reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical decision tree
&lt;/h2&gt;

&lt;p&gt;Ask these questions in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Did the user perform the triggering action inside the LINE MINI App?&lt;/li&gt;
&lt;li&gt;Is the message only a confirmation, action result, or reminder for that action?&lt;/li&gt;
&lt;li&gt;Is the MINI App verified?&lt;/li&gt;
&lt;li&gt;Has the exact template passed review?&lt;/li&gt;
&lt;li&gt;Is a valid Service notification token available?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If all answers are yes, use the Service Message API.&lt;/p&gt;

&lt;p&gt;Otherwise ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Should the sender be the LINE Official Account?&lt;/li&gt;
&lt;li&gt;Is this a reply to an eligible webhook event?&lt;/li&gt;
&lt;li&gt;Is the recipient eligible for push or audience delivery?&lt;/li&gt;
&lt;li&gt;Is the account within its message allowance?&lt;/li&gt;
&lt;li&gt;Which Messaging API method matches the recipient model?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then choose reply, push, multicast, narrowcast, or broadcast as appropriate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Message purpose is defined&lt;/li&gt;
&lt;li&gt;[ ] Triggering user action is identified&lt;/li&gt;
&lt;li&gt;[ ] Sender identity is correct&lt;/li&gt;
&lt;li&gt;[ ] Chat-room destination is understood&lt;/li&gt;
&lt;li&gt;[ ] Token type is explicit&lt;/li&gt;
&lt;li&gt;[ ] Service Message template is approved&lt;/li&gt;
&lt;li&gt;[ ] Promotional content is excluded from Service Messages&lt;/li&gt;
&lt;li&gt;[ ] Renewed notification tokens are stored atomically&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;remainingCount&lt;/code&gt; and expiry are persisted&lt;/li&gt;
&lt;li&gt;[ ] Action-level idempotency is implemented&lt;/li&gt;
&lt;li&gt;[ ] Reply tokens are not stored as permanent addresses&lt;/li&gt;
&lt;li&gt;[ ] Messaging API recipient eligibility is checked&lt;/li&gt;
&lt;li&gt;[ ] Monthly allowance and rate limits are monitored&lt;/li&gt;
&lt;li&gt;[ ] Duplicate cross-path delivery is prevented&lt;/li&gt;
&lt;li&gt;[ ] Both paths correlate through an internal business ID&lt;/li&gt;
&lt;li&gt;[ ] Credentials, queues, and retry policies remain separate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The correct question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which LINE API can send the most messages?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which product, sender identity, trigger, recipient model, and policy match this specific message?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Official references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/line-mini-app/develop/service-messages/" rel="noopener noreferrer"&gt;Sending LINE MINI App Service Messages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/line-mini-app/service/service-operation/" rel="noopener noreferrer"&gt;Service Message conditions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/reference/line-mini-app/" rel="noopener noreferrer"&gt;LINE MINI App API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/messaging-api/sending-messages/" rel="noopener noreferrer"&gt;Sending messages with the Messaging API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/messaging-api/pricing/" rel="noopener noreferrer"&gt;Messaging API pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.unifyport.ai/blog/line-service-messages-vs-messaging-api-mini-app/" rel="noopener noreferrer"&gt;UnifyPort&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was prepared with AI assistance for language and structure, then technically reviewed and verified by the author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>wenhooks</category>
      <category>architecture</category>
    </item>
    <item>
      <title>One LINE Official Account, Multiple Tools: Webhook and Token Architecture</title>
      <dc:creator>unifyport</dc:creator>
      <pubDate>Wed, 05 Aug 2026 06:02:41 +0000</pubDate>
      <link>https://dev.to/unifyport/one-line-official-account-multiple-tools-webhook-and-token-architecture-5bi8</link>
      <guid>https://dev.to/unifyport/one-line-official-account-multiple-tools-webhook-and-token-architecture-5bi8</guid>
      <description>&lt;p&gt;A completed WhatsApp Embedded Signup dialog does not mean that a customer is ready to send and receive messages.&lt;/p&gt;

&lt;p&gt;The browser flow may have finished while the backend still has one of these problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The result was attached to the wrong tenant&lt;/li&gt;
&lt;li&gt;The access token belongs to another app&lt;/li&gt;
&lt;li&gt;The wrong WhatsApp Business Account was selected&lt;/li&gt;
&lt;li&gt;The system user lacks the required access&lt;/li&gt;
&lt;li&gt;The phone number is not ready&lt;/li&gt;
&lt;li&gt;The app is not subscribed to WABA webhooks&lt;/li&gt;
&lt;li&gt;The first webhook cannot be routed to the customer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A SaaS application should display &lt;strong&gt;Connected&lt;/strong&gt; only after every backend gate has passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model onboarding as a state machine
&lt;/h2&gt;

&lt;p&gt;Avoid representing onboarding with one Boolean field such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;connected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use explicit states instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WhatsAppOnboardingStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;started&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;browser_finished&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token_validated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waba_resolved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;access_verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;phone_ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app_subscribed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webhook_pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verification_required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&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 safe transition path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;started
   ↓
browser_finished
   ↓
token_validated
   ↓
waba_resolved
   ↓
access_verified
   ↓
phone_ready
   ↓
app_subscribed
   ↓
webhook_pending
   ↓
active
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each transition should store evidence, not just a timestamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the acceptance gates
&lt;/h2&gt;

&lt;p&gt;Meta's &lt;a href="https://www.postman.com/meta/whatsapp-business-platform/documentation/du6gzjv/embedded-signup" rel="noopener noreferrer"&gt;official Embedded Signup collection&lt;/a&gt; separates the browser flow from the Graph API work required afterward.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;th&gt;Failure risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Session correlation&lt;/td&gt;
&lt;td&gt;Tenant ID, state, configuration ID&lt;/td&gt;
&lt;td&gt;Assets attached to the wrong customer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token validation&lt;/td&gt;
&lt;td&gt;App ID, scopes, expiry metadata&lt;/td&gt;
&lt;td&gt;Token exists but cannot manage the WABA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WABA resolution&lt;/td&gt;
&lt;td&gt;Exact WABA ID and business mapping&lt;/td&gt;
&lt;td&gt;First list result belongs to another customer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System-user access&lt;/td&gt;
&lt;td&gt;System-user ID and required task&lt;/td&gt;
&lt;td&gt;Later API operations fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phone readiness&lt;/td&gt;
&lt;td&gt;Phone-number ID and onboarding path&lt;/td&gt;
&lt;td&gt;WABA exists but messaging is unavailable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App subscription&lt;/td&gt;
&lt;td&gt;App appears in &lt;code&gt;subscribed_apps&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Meta receives messages but sends no webhook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery proof&lt;/td&gt;
&lt;td&gt;One correctly routed webhook&lt;/td&gt;
&lt;td&gt;Configuration passes without real delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A tenant should remain unavailable until all required gates pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Correlate the browser result with a server session
&lt;/h2&gt;

&lt;p&gt;Create the onboarding session on your server before opening Embedded Signup.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OnboardingSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;startedByUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;configurationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;onboardingPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cloud_api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;coexistence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;consumedAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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 &lt;code&gt;state&lt;/code&gt; value should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Random&lt;/li&gt;
&lt;li&gt;Single-use&lt;/li&gt;
&lt;li&gt;Bound to one tenant&lt;/li&gt;
&lt;li&gt;Bound to the initiating user&lt;/li&gt;
&lt;li&gt;Short-lived&lt;/li&gt;
&lt;li&gt;Validated on the server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the browser reports completion:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look up the server session&lt;/li&gt;
&lt;li&gt;Verify the state&lt;/li&gt;
&lt;li&gt;Reject expired sessions&lt;/li&gt;
&lt;li&gt;Reject already-consumed sessions&lt;/li&gt;
&lt;li&gt;Verify that the current user can modify the tenant&lt;/li&gt;
&lt;li&gt;Mark the result as consumed atomically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not trust a tenant ID submitted by the browser if it can be derived from the authenticated server session.&lt;/p&gt;

&lt;p&gt;If the Coexistence flow emits &lt;code&gt;FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING&lt;/code&gt;, treat it as evidence that the dialog finished—not evidence that the backend is active.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Keep credentials out of the browser
&lt;/h2&gt;

&lt;p&gt;Credential exchange and Graph API verification belong on the server.&lt;/p&gt;

&lt;p&gt;Do not place access tokens in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browser storage&lt;/li&gt;
&lt;li&gt;Query strings&lt;/li&gt;
&lt;li&gt;Client-side analytics&lt;/li&gt;
&lt;li&gt;Error-reporting breadcrumbs&lt;/li&gt;
&lt;li&gt;Support screenshots&lt;/li&gt;
&lt;li&gt;Application logs&lt;/li&gt;
&lt;li&gt;Database fields returned by public APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store the credential in a secret manager or encrypted server-side store.&lt;/p&gt;

&lt;p&gt;Persist only the audit metadata your application needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TokenAuditMetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tokenType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;grantedScopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;issuedAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;lastValidatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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 non-empty token is not proof that it belongs to the expected Meta app or has the permissions required for the selected WABA.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Resolve the intended WABA deterministically
&lt;/h2&gt;

&lt;p&gt;The official collection exposes this endpoint for retrieving client WABAs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /{business-id}/client_whatsapp_business_accounts
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A business can return multiple WABAs. Never select the first array element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Unsafe&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;waba&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;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Match against information captured for the current onboarding session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveWaba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;availableWabas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;expectedWabaId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;availableWabas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;expectedWabaId&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;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="s2"&gt;expected_waba_not_resolved&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="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store the relationship explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TenantWabaMapping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;metaBusinessId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;wabaId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sourceSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;verifiedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;If no exact match exists, move the tenant to &lt;code&gt;verification_required&lt;/code&gt;. Do not silently attach another WABA.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Verify system-user access
&lt;/h2&gt;

&lt;p&gt;Meta documents the following endpoint for checking assigned users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /{waba-id}/assigned_users?business={business-id}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The expected system user is present&lt;/li&gt;
&lt;li&gt;It belongs to the expected business&lt;/li&gt;
&lt;li&gt;It has the task required by your integration&lt;/li&gt;
&lt;li&gt;Your backend credential can perform the required WABA operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not treat any returned system user as sufficient.&lt;/p&gt;

&lt;p&gt;A system user added for a different operational role may not have the access your message or template workflow requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Branch by onboarding path
&lt;/h2&gt;

&lt;p&gt;Standard Cloud API onboarding and WhatsApp Business App Coexistence are not the same phone-number path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Standard Cloud API
&lt;/h3&gt;

&lt;p&gt;Depending on the current onboarding contract, the backend may need to register the phone number and verify that its status permits messaging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coexistence
&lt;/h3&gt;

&lt;p&gt;Coexistence uses a number already connected to the WhatsApp Business app.&lt;/p&gt;

&lt;p&gt;Do not automatically repeat the standard registration operation. Instead, validate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The number returned by the Coexistence flow&lt;/li&gt;
&lt;li&gt;Its current status&lt;/li&gt;
&lt;li&gt;Its WABA association&lt;/li&gt;
&lt;li&gt;The intended synchronization behavior&lt;/li&gt;
&lt;li&gt;The supported message-history boundary&lt;/li&gt;
&lt;li&gt;Which surface owns each business operation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the path in your tenant configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WhatsAppConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;wabaId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;phoneNumberId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cloud_api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;coexistence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verification_required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&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;This prevents later jobs from applying Cloud API assumptions to a Coexistence number.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Subscribe the app to the WABA
&lt;/h2&gt;

&lt;p&gt;A valid WABA and phone number do not prove that webhook delivery is configured.&lt;/p&gt;

&lt;p&gt;Meta's official collection uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /{waba-id}/subscribed_apps
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the result afterward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /{waba-id}/subscribed_apps
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected Meta app must appear in the returned subscription list.&lt;/p&gt;

&lt;p&gt;Treat subscription as a separate gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;verifyAppSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;whatsapp_business_api_data&lt;/span&gt;&lt;span class="p"&gt;?:&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="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;expectedAppId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscribed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&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="o"&gt;=&amp;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;whatsapp_business_api_data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;expectedAppId&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;subscribed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waba_app_subscription_missing&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registration can succeed while subscription fails. Subscription can also exist while the selected phone number is not operational.&lt;/p&gt;

&lt;p&gt;Do not combine these conditions into one status.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Prove one real webhook delivery
&lt;/h2&gt;

&lt;p&gt;Configuration reads are necessary but not sufficient.&lt;/p&gt;

&lt;p&gt;Before marking the tenant active, require one controlled webhook to reach a production-equivalent receiver.&lt;/p&gt;

&lt;p&gt;The acceptance path should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Controlled WhatsApp message
          ↓
Meta webhook delivery
          ↓
Signature verification
          ↓
WABA and phone-number lookup
          ↓
Tenant resolution
          ↓
Idempotency check
          ↓
Durable event storage
          ↓
Successful acknowledgement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a Meta webhook, verify the signature against the raw request body before parsing or routing it.&lt;/p&gt;

&lt;p&gt;An illustrative Node.js helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;createHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timingSafeEqual&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;verifyMetaSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;signatureHeader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;appSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;prefix&lt;/span&gt; &lt;span class="o"&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="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;signatureHeader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefix&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="kc"&gt;false&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;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHmac&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="nx"&gt;appSecret&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="nx"&gt;rawBody&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;signatureHeader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&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;Never log the app secret, token, or complete sensitive webhook payload while debugging verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Route by provider assets, not browser state
&lt;/h2&gt;

&lt;p&gt;The webhook receiver must resolve the tenant from trusted provider identifiers.&lt;/p&gt;

&lt;p&gt;Relevant identifiers can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WABA ID&lt;/li&gt;
&lt;li&gt;Phone-number ID&lt;/li&gt;
&lt;li&gt;Meta app ID&lt;/li&gt;
&lt;li&gt;Internal connection ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not route a production webhook using a tenant ID previously stored in browser state without verifying its asset mapping.&lt;/p&gt;

&lt;p&gt;A routing table can look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;whatsapp_connections&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;meta_app_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;waba_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;phone_number_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;onboarding_mode&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta_app_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;waba_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone_number_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;If the mapping is missing or ambiguous:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not guess&lt;/li&gt;
&lt;li&gt;Do not assign the event to the most recently onboarded tenant&lt;/li&gt;
&lt;li&gt;Store it in a restricted reconciliation queue&lt;/li&gt;
&lt;li&gt;Alert the integration owner&lt;/li&gt;
&lt;li&gt;Keep the tenant out of &lt;code&gt;active&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Make webhook processing idempotent
&lt;/h2&gt;

&lt;p&gt;Webhook delivery can be retried.&lt;/p&gt;

&lt;p&gt;Choose an idempotency key from stable provider fields appropriate to the event type. For inbound messages, the provider message ID is usually part of that identity.&lt;/p&gt;

&lt;p&gt;Store the event before running slow business logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verify signature
      ↓
Resolve tenant
      ↓
Insert event if absent
      ↓
Return success
      ↓
Process asynchronously
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A duplicate should not create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two conversations&lt;/li&gt;
&lt;li&gt;Two automated replies&lt;/li&gt;
&lt;li&gt;Two billing records&lt;/li&gt;
&lt;li&gt;Two workflow executions&lt;/li&gt;
&lt;li&gt;Two customer notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Operational readiness means that both the first delivery and a repeated delivery are safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Treat credit-line attachment as conditional
&lt;/h2&gt;

&lt;p&gt;The official Embedded Signup collection includes credit-line sharing for provider-paid arrangements.&lt;/p&gt;

&lt;p&gt;That does not make credit-line attachment a universal readiness gate.&lt;/p&gt;

&lt;p&gt;Apply it only when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your business owns the billing relationship with Meta&lt;/li&gt;
&lt;li&gt;Your partner model requires credit sharing&lt;/li&gt;
&lt;li&gt;The customer WABA is expected to use that credit line&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For customer-paid or other supported billing arrangements, document the appropriate billing check separately.&lt;/p&gt;

&lt;p&gt;Do not block every tenant on a credit-line operation that does not belong to its commercial model.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Separate platform approval from operational readiness
&lt;/h2&gt;

&lt;p&gt;This checklist does not prove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Meta business eligibility&lt;/li&gt;
&lt;li&gt;App Review approval&lt;/li&gt;
&lt;li&gt;Advanced Access approval&lt;/li&gt;
&lt;li&gt;Display-name approval&lt;/li&gt;
&lt;li&gt;Number quality&lt;/li&gt;
&lt;li&gt;Template approval&lt;/li&gt;
&lt;li&gt;Messaging-limit tier&lt;/li&gt;
&lt;li&gt;Policy compliance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are separate platform states.&lt;/p&gt;

&lt;p&gt;Maintain them independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WhatsAppReadiness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;embeddedSignup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;appReview&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;wabaAccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;phoneStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;webhookSubscription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;deliveryTest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;passed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&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;A successful test tenant also does not prove that every customer configuration will work. Keep the verification process repeatable for every onboarding session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended failure states
&lt;/h2&gt;

&lt;p&gt;Return actionable states instead of a generic connection error.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Next action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session_mismatch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Browser result cannot be correlated&lt;/td&gt;
&lt;td&gt;Restart onboarding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;token_invalid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Credential validation failed&lt;/td&gt;
&lt;td&gt;Repeat credential exchange&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;waba_not_resolved&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Expected WABA was not found&lt;/td&gt;
&lt;td&gt;Review business selection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system_user_missing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Required access is absent&lt;/td&gt;
&lt;td&gt;Assign or repair system user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;phone_not_ready&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Phone path is incomplete&lt;/td&gt;
&lt;td&gt;Follow path-specific recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subscription_missing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;App is not subscribed&lt;/td&gt;
&lt;td&gt;Subscribe and verify again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;webhook_not_received&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No delivery proof exists&lt;/td&gt;
&lt;td&gt;Inspect webhook and routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;routing_ambiguous&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Asset mapping matches multiple tenants&lt;/td&gt;
&lt;td&gt;Stop and reconcile mappings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These states make support, retry behavior, and audit logs much easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final post-onboarding checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Server-side onboarding session exists&lt;/li&gt;
&lt;li&gt;[ ] State is valid, single-use, and tenant-bound&lt;/li&gt;
&lt;li&gt;[ ] Browser result has been consumed exactly once&lt;/li&gt;
&lt;li&gt;[ ] Credentials remain server-side&lt;/li&gt;
&lt;li&gt;[ ] Token app, permissions, and expiry were validated&lt;/li&gt;
&lt;li&gt;[ ] Exact WABA was resolved&lt;/li&gt;
&lt;li&gt;[ ] WABA was not selected by array position&lt;/li&gt;
&lt;li&gt;[ ] System-user access was verified&lt;/li&gt;
&lt;li&gt;[ ] Standard and Coexistence paths are separated&lt;/li&gt;
&lt;li&gt;[ ] Phone-number ID is stored&lt;/li&gt;
&lt;li&gt;[ ] Phone status is ready for the selected path&lt;/li&gt;
&lt;li&gt;[ ] Expected app appears in &lt;code&gt;subscribed_apps&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Billing checks match the commercial model&lt;/li&gt;
&lt;li&gt;[ ] One real webhook reached the receiver&lt;/li&gt;
&lt;li&gt;[ ] Webhook signature was verified&lt;/li&gt;
&lt;li&gt;[ ] WABA and phone number resolved to one tenant&lt;/li&gt;
&lt;li&gt;[ ] Duplicate delivery is safe&lt;/li&gt;
&lt;li&gt;[ ] Failure states are recoverable&lt;/li&gt;
&lt;li&gt;[ ] Only then is the tenant marked &lt;code&gt;active&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser dialog finishing is a user-interface milestone.&lt;/p&gt;

&lt;p&gt;Operational readiness requires a verified chain from the server-side onboarding session to the correct WABA, system user, phone number, app subscription, tenant mapping, and real webhook delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.postman.com/meta/whatsapp-business-platform/documentation/du6gzjv/embedded-signup" rel="noopener noreferrer"&gt;Meta Embedded Signup collection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/version-4" rel="noopener noreferrer"&gt;Meta Embedded Signup v4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users" rel="noopener noreferrer"&gt;Onboarding WhatsApp Business app users&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.unifyport.ai/blog/whatsapp-embedded-signup-post-onboarding-verification-checklist/" rel="noopener noreferrer"&gt;UnifyPort&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was prepared with AI assistance for language and structure, then technically reviewed and verified by the author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webhooks</category>
      <category>architecture</category>
    </item>
    <item>
      <title>One LINE Official Account, Multiple Tools: Webhook and Token Architecture</title>
      <dc:creator>unifyport</dc:creator>
      <pubDate>Tue, 04 Aug 2026 03:10:04 +0000</pubDate>
      <link>https://dev.to/unifyport/one-line-official-account-multiple-tools-webhook-and-token-architecture-3p5l</link>
      <guid>https://dev.to/unifyport/one-line-official-account-multiple-tools-webhook-and-token-architecture-3p5l</guid>
      <description>&lt;p&gt;A single LINE Official Account can use multiple Messaging API tools.&lt;/p&gt;

&lt;p&gt;For example, one account might connect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A customer-support platform&lt;/li&gt;
&lt;li&gt;A campaign sender&lt;/li&gt;
&lt;li&gt;A rich-menu manager&lt;/li&gt;
&lt;li&gt;An analytics service&lt;/li&gt;
&lt;li&gt;An internal automation system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But these tools do not receive isolated LINE channels. They share one Messaging API channel, one webhook URL, channel access-token limits, API rate limits, and feature-specific quotas.&lt;/p&gt;

&lt;p&gt;That makes adding another tool an architecture change—not just another OAuth or API-key setup step.&lt;/p&gt;

&lt;p&gt;This guide explains how to share the channel without accidentally disabling an existing tool or losing inbound messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the shared boundary
&lt;/h2&gt;

&lt;p&gt;LINE's &lt;a href="https://developers.line.biz/en/tips/2026/07/23/using-multiple-api-tools/" rel="noopener noreferrer"&gt;official multiple-tools guidance&lt;/a&gt; confirms that multiple tools can call the Messaging API through one LINE Official Account.&lt;/p&gt;

&lt;p&gt;However, only one Messaging API channel can be linked to the account.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shared resource&lt;/th&gt;
&lt;th&gt;LINE constraint&lt;/th&gt;
&lt;th&gt;Operational risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Messaging API channel&lt;/td&gt;
&lt;td&gt;One channel per Official Account&lt;/td&gt;
&lt;td&gt;All tools share configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhook URL&lt;/td&gt;
&lt;td&gt;One URL per channel&lt;/td&gt;
&lt;td&gt;A new tool can replace the existing receiver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel access tokens&lt;/td&gt;
&lt;td&gt;Issuance limits vary by token type&lt;/td&gt;
&lt;td&gt;Rotation can disable another tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API rate limits&lt;/td&gt;
&lt;td&gt;Applied per endpoint and channel&lt;/td&gt;
&lt;td&gt;One tool can throttle another&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Messaging quota&lt;/td&gt;
&lt;td&gt;Shared by the account and plan&lt;/td&gt;
&lt;td&gt;Campaign traffic can affect support traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rich menus and audiences&lt;/td&gt;
&lt;td&gt;Channel-level limits&lt;/td&gt;
&lt;td&gt;Tools can overwrite or exhaust shared resources&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before connecting another tool, identify exactly which shared resources it needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create an integration inventory
&lt;/h2&gt;

&lt;p&gt;Maintain a manifest for every system using the channel.&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;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support-platform&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer-support-team&lt;/span&gt;
    &lt;span class="na"&gt;features&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;receive-webhooks&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;reply-messages&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;push-messages&lt;/span&gt;
    &lt;span class="na"&gt;token_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v2.1&lt;/span&gt;
    &lt;span class="na"&gt;owns_webhook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;campaign-service&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;marketing-operations&lt;/span&gt;
    &lt;span class="na"&gt;features&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;broadcast-messages&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;audience-management&lt;/span&gt;
    &lt;span class="na"&gt;token_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v2.1&lt;/span&gt;
    &lt;span class="na"&gt;owns_webhook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;rich-menu-manager&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;product-team&lt;/span&gt;
    &lt;span class="na"&gt;features&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rich-menu-management&lt;/span&gt;
    &lt;span class="na"&gt;token_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stateless&lt;/span&gt;
    &lt;span class="na"&gt;owns_webhook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each tool, record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Business owner&lt;/li&gt;
&lt;li&gt;Technical owner&lt;/li&gt;
&lt;li&gt;Messaging API endpoints&lt;/li&gt;
&lt;li&gt;Required webhook events&lt;/li&gt;
&lt;li&gt;Token type&lt;/li&gt;
&lt;li&gt;Token issuer&lt;/li&gt;
&lt;li&gt;Expiration and renewal procedure&lt;/li&gt;
&lt;li&gt;Rich-menu or audience ownership&lt;/li&gt;
&lt;li&gt;Expected request volume&lt;/li&gt;
&lt;li&gt;Emergency-disable procedure&lt;/li&gt;
&lt;li&gt;Rollback procedure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not connect a tool until these responsibilities are assigned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose channel access tokens carefully
&lt;/h2&gt;

&lt;p&gt;LINE currently supports four channel access-token types.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token type&lt;/th&gt;
&lt;th&gt;Validity&lt;/th&gt;
&lt;th&gt;Issuance limit per channel&lt;/th&gt;
&lt;th&gt;Limit behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Long-lived&lt;/td&gt;
&lt;td&gt;No fixed expiration&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Reissuing invalidates the active token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short-lived&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;Issuing beyond the limit revokes the oldest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2.1&lt;/td&gt;
&lt;td&gt;Up to 30 days&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;Additional issuance is rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateless&lt;/td&gt;
&lt;td&gt;15 minutes&lt;/td&gt;
&lt;td&gt;No stated count limit&lt;/td&gt;
&lt;td&gt;Cannot be revoked after issuance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The exact behavior is documented in LINE's &lt;a href="https://developers.line.biz/en/docs/basics/channel-access-token/" rel="noopener noreferrer"&gt;channel access-token guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The dangerous case is a shared long-lived token.&lt;/p&gt;

&lt;p&gt;If Tool A and Tool B both use the same long-lived token, reissuing it for Tool B immediately affects Tool A.&lt;/p&gt;

&lt;p&gt;A safer policy is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Issue credentials per team or tool&lt;/li&gt;
&lt;li&gt;Prefer expiring tokens when the vendor supports them&lt;/li&gt;
&lt;li&gt;Store tokens in a secret manager&lt;/li&gt;
&lt;li&gt;Record the token type and owner&lt;/li&gt;
&lt;li&gt;Automate renewal before expiration&lt;/li&gt;
&lt;li&gt;Test the new token before retiring the old one&lt;/li&gt;
&lt;li&gt;Revoke credentials when a tool is removed&lt;/li&gt;
&lt;li&gt;Never copy one team's production token into an unrelated system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not log token values, even during migration troubleshooting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep one webhook owner
&lt;/h2&gt;

&lt;p&gt;A Messaging API channel supports one webhook URL.&lt;/p&gt;

&lt;p&gt;LINE cannot directly deliver the same event to two independent URLs. If a new vendor changes the webhook URL, the current receiver stops receiving events.&lt;/p&gt;

&lt;p&gt;Use this decision table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;New tool requirement&lt;/th&gt;
&lt;th&gt;Recommended approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sends messages only&lt;/td&gt;
&lt;td&gt;Give it an appropriate token; do not change the webhook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manages rich menus only&lt;/td&gt;
&lt;td&gt;Leave the webhook owner unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replaces the current inbox&lt;/td&gt;
&lt;td&gt;Perform a controlled webhook migration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs a subset of inbound events&lt;/td&gt;
&lt;td&gt;Forward verified events from the existing receiver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple tools need every event&lt;/td&gt;
&lt;td&gt;Use one receiver and fan out internally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor requires direct ownership&lt;/td&gt;
&lt;td&gt;Choose one inbound owner or use another Official Account&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The stable architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LINE Platform
      ↓
Single webhook endpoint
      ↓
Signature verification
      ↓
Durable event inbox
      ↓
Internal event router
   ↙       ↓       ↘
Support  Analytics  Automation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verify the signature before parsing
&lt;/h2&gt;

&lt;p&gt;LINE signs webhook requests using the channel secret.&lt;/p&gt;

&lt;p&gt;Verification must use the raw request body. Do not parse and reserialize the JSON before calculating the signature.&lt;/p&gt;

&lt;p&gt;An illustrative Node.js receiver:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&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;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timingSafeEqual&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;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&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;channelSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LINE_CHANNEL_SECRET&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;channelSecret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LINE_CHANNEL_SECRET is required&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyLineSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHmac&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="nx"&gt;secret&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="nx"&gt;rawBody&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&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="nx"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/line/webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Buffer&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;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-line-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="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="nf"&gt;verifyLineSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;channelSecret&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&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;return&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;payload&lt;/span&gt; &lt;span class="o"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;eventInbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertIfAbsent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookEventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;occurredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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;Important boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify before processing&lt;/li&gt;
&lt;li&gt;Use a timing-safe comparison&lt;/li&gt;
&lt;li&gt;Preserve the raw body&lt;/li&gt;
&lt;li&gt;Do not log secrets or reply tokens&lt;/li&gt;
&lt;li&gt;Store the event before asynchronous fan-out&lt;/li&gt;
&lt;li&gt;Return a successful response promptly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LINE recommends asynchronous webhook processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deduplicate before fan-out
&lt;/h2&gt;

&lt;p&gt;Webhook redelivery can cause the same event to arrive more than once.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;webhookEventId&lt;/code&gt; as the idempotency key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;line_webhook_inbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;webhook_event_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;occurred_at&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&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;JSON&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;processing_status&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The receiver should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify the signature&lt;/li&gt;
&lt;li&gt;Insert the event with a unique &lt;code&gt;webhookEventId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Treat a duplicate-key result as an accepted duplicate&lt;/li&gt;
&lt;li&gt;Return &lt;code&gt;2xx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Let a worker process newly inserted events&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not let each downstream tool independently deduplicate the original webhook. Deduplication belongs at the shared receiving boundary.&lt;/p&gt;

&lt;p&gt;Webhook redelivery can also change arrival order. Use the event &lt;code&gt;timestamp&lt;/code&gt; to understand event sequence when order matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give reply-token ownership to one component
&lt;/h2&gt;

&lt;p&gt;Reply tokens create an additional concurrency problem.&lt;/p&gt;

&lt;p&gt;LINE reply tokens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can only be used once&lt;/li&gt;
&lt;li&gt;Should be used as soon as possible&lt;/li&gt;
&lt;li&gt;Must normally be used within one minute of receiving the webhook&lt;/li&gt;
&lt;li&gt;Cannot be safely consumed by multiple independent tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the same event is forwarded to a support system and an automation system, both systems must not attempt to use the reply token.&lt;/p&gt;

&lt;p&gt;Assign one reply coordinator.&lt;/p&gt;

&lt;p&gt;Downstream services can return a reply intent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ReplyIntent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;webhookEventId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;support&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;automation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="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 coordinator decides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the event should receive a reply&lt;/li&gt;
&lt;li&gt;Which tool has priority&lt;/li&gt;
&lt;li&gt;Whether a human response suppresses automation&lt;/li&gt;
&lt;li&gt;Whether multiple messages should be combined&lt;/li&gt;
&lt;li&gt;Whether the reply token has already been consumed&lt;/li&gt;
&lt;li&gt;Whether a later response must use a push message instead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This prevents duplicate or conflicting replies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate inbound and outbound ownership
&lt;/h2&gt;

&lt;p&gt;A tool that receives webhooks does not automatically need to own every outbound message.&lt;/p&gt;

&lt;p&gt;Define ownership by operation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Suggested owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Webhook signature verification&lt;/td&gt;
&lt;td&gt;Ingress service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durable event storage&lt;/td&gt;
&lt;td&gt;Ingress service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reply-token consumption&lt;/td&gt;
&lt;td&gt;Reply coordinator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human support replies&lt;/td&gt;
&lt;td&gt;Support system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduled campaigns&lt;/td&gt;
&lt;td&gt;Campaign platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rich menus&lt;/td&gt;
&lt;td&gt;Product or marketing owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audiences&lt;/td&gt;
&lt;td&gt;Marketing operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel token issuance&lt;/td&gt;
&lt;td&gt;Platform or security team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared quota monitoring&lt;/td&gt;
&lt;td&gt;Platform operations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One tool should not silently change resources owned by another.&lt;/p&gt;

&lt;p&gt;For example, a support-platform installation should not replace production rich menus unless rich-menu management is explicitly part of its approved scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Budget shared rate limits
&lt;/h2&gt;

&lt;p&gt;Messaging API rate limits are applied per API function and per channel, regardless of which tool or IP address sends the request.&lt;/p&gt;

&lt;p&gt;This means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tool A traffic
+ Tool B traffic
+ Tool C traffic
= Channel traffic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Monitor aggregate usage rather than isolated vendor dashboards.&lt;/p&gt;

&lt;p&gt;Track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests per endpoint&lt;/li&gt;
&lt;li&gt;&lt;code&gt;429 Too Many Requests&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Monthly message usage&lt;/li&gt;
&lt;li&gt;Broadcast volume&lt;/li&gt;
&lt;li&gt;Push-message volume&lt;/li&gt;
&lt;li&gt;Rich-menu count&lt;/li&gt;
&lt;li&gt;Audience count&lt;/li&gt;
&lt;li&gt;Statistics-unit usage&lt;/li&gt;
&lt;li&gt;Per-tool error rate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not blindly retry every &lt;code&gt;429&lt;/code&gt;. A coordinated retry policy should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Endpoint-aware backoff&lt;/li&gt;
&lt;li&gt;Jitter&lt;/li&gt;
&lt;li&gt;Maximum attempts&lt;/li&gt;
&lt;li&gt;Message-expiration rules&lt;/li&gt;
&lt;li&gt;Shared concurrency limits&lt;/li&gt;
&lt;li&gt;Alerting when one tool consumes unusual capacity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reserve capacity for critical support replies before launching a large campaign.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test unknown webhook events
&lt;/h2&gt;

&lt;p&gt;Adding or enabling a tool can change which features are used through the shared channel.&lt;/p&gt;

&lt;p&gt;Your current receiver must safely handle events it does not consume.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;routeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LineWebhookEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&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="nx"&gt;messageQueue&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;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;follow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unfollow&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="nx"&gt;accountQueue&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;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postback&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="nx"&gt;automationQueue&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;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nl"&gt;default&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;unknownEventQueue&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="na"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookEventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unknown event should be observable, but it should not crash the entire webhook request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration checklist for a new tool
&lt;/h2&gt;

&lt;p&gt;Before installation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Export the current webhook URL&lt;/li&gt;
&lt;li&gt;Identify the current token types&lt;/li&gt;
&lt;li&gt;List all token consumers&lt;/li&gt;
&lt;li&gt;Record current rich menus and audiences&lt;/li&gt;
&lt;li&gt;Measure normal API traffic&lt;/li&gt;
&lt;li&gt;Confirm whether the new tool needs inbound events&lt;/li&gt;
&lt;li&gt;Confirm which settings its installer changes&lt;/li&gt;
&lt;li&gt;Prepare the rollback configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During installation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a tool-specific credential&lt;/li&gt;
&lt;li&gt;Prevent unapproved webhook replacement&lt;/li&gt;
&lt;li&gt;Test in a controlled environment&lt;/li&gt;
&lt;li&gt;Send one inbound message&lt;/li&gt;
&lt;li&gt;Confirm exactly one durable event&lt;/li&gt;
&lt;li&gt;Confirm the correct downstream consumers receive it&lt;/li&gt;
&lt;li&gt;Verify only one component uses the reply token&lt;/li&gt;
&lt;li&gt;Test one supported outbound action&lt;/li&gt;
&lt;li&gt;Monitor rate-limit and authentication errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After installation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recheck the webhook URL&lt;/li&gt;
&lt;li&gt;Recheck existing tools&lt;/li&gt;
&lt;li&gt;Confirm campaigns and support replies still work&lt;/li&gt;
&lt;li&gt;Confirm rich menus were not replaced&lt;/li&gt;
&lt;li&gt;Review message usage&lt;/li&gt;
&lt;li&gt;Rotate temporary credentials&lt;/li&gt;
&lt;li&gt;Record the final configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prepare a rollback procedure
&lt;/h2&gt;

&lt;p&gt;A rollback should specify:&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;rollback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;previous_webhook_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;STORED_IN_SECURE_CONFIGURATION&lt;/span&gt;
  &lt;span class="na"&gt;affected_tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;support-platform&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;campaign-service&lt;/span&gt;
  &lt;span class="na"&gt;token_action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revoke-new-tool-token&lt;/span&gt;
  &lt;span class="na"&gt;verification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;send-controlled-user-message&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;confirm-single-webhook-record&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;confirm-support-reply&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;confirm-existing-rich-menu&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not make the first production failure the first time the rollback is tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final architecture checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every connected tool has an owner&lt;/li&gt;
&lt;li&gt;[ ] Messaging API features are inventoried per tool&lt;/li&gt;
&lt;li&gt;[ ] Token type and renewal owner are documented&lt;/li&gt;
&lt;li&gt;[ ] Tools do not share one long-lived token&lt;/li&gt;
&lt;li&gt;[ ] The single webhook owner is explicit&lt;/li&gt;
&lt;li&gt;[ ] Webhook signatures are verified against the raw body&lt;/li&gt;
&lt;li&gt;[ ] Events are stored before asynchronous fan-out&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;webhookEventId&lt;/code&gt; is used for deduplication&lt;/li&gt;
&lt;li&gt;[ ] Reply tokens have one coordinator&lt;/li&gt;
&lt;li&gt;[ ] Unknown webhook events remain observable&lt;/li&gt;
&lt;li&gt;[ ] Rich-menu and audience ownership is documented&lt;/li&gt;
&lt;li&gt;[ ] Rate limits are monitored at channel level&lt;/li&gt;
&lt;li&gt;[ ] Monthly message capacity is shared deliberately&lt;/li&gt;
&lt;li&gt;[ ] A controlled end-to-end test has passed&lt;/li&gt;
&lt;li&gt;[ ] The previous webhook configuration can be restored&lt;/li&gt;
&lt;li&gt;[ ] Removing a tool revokes its credentials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Multiple tools can safely share one LINE Official Account, but only when the shared channel is treated as production infrastructure.&lt;/p&gt;

&lt;p&gt;The most important rule is simple: receive once, verify once, store once, and coordinate everything that happens afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/tips/2026/07/23/using-multiple-api-tools/" rel="noopener noreferrer"&gt;Using the Messaging API from multiple tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/basics/channel-access-token/" rel="noopener noreferrer"&gt;Channel access-token types and limits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/messaging-api/receiving-messages/" rel="noopener noreferrer"&gt;Receiving webhook events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/messaging-api/verify-webhook-signature/" rel="noopener noreferrer"&gt;Webhook signature verification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/reference/messaging-api/#rate-limits" rel="noopener noreferrer"&gt;Messaging API rate limits&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.unifyport.ai/blog/line-messaging-api-multiple-tools-one-official-account/" rel="noopener noreferrer"&gt;UnifyPort&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was prepared with AI assistance for language and structure, then technically reviewed and verified by the author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webhooks</category>
      <category>architecture</category>
    </item>
    <item>
      <title>LINE MINI App Verification Review: A Pre-Submission Checklist That Prevents Rework</title>
      <dc:creator>unifyport</dc:creator>
      <pubDate>Mon, 03 Aug 2026 07:17:46 +0000</pubDate>
      <link>https://dev.to/unifyport/line-mini-app-verification-review-a-pre-submission-checklist-that-prevents-rework-3d3m</link>
      <guid>https://dev.to/unifyport/line-mini-app-verification-review-a-pre-submission-checklist-that-prevents-rework-3d3m</guid>
      <description>&lt;p&gt;A LINE MINI App does not automatically need verification before it can be published.&lt;/p&gt;

&lt;p&gt;The first question is whether your product actually depends on a verified-only capability. If it does, the review is more than a technical launch check: LINE evaluates identity consistency, policy compliance, channel configuration, and whether a reviewer can complete the service's real user flows.&lt;/p&gt;

&lt;p&gt;A preventable documentation mismatch can add another review cycle, so the best time to audit the submission is before opening the Review request tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide whether verification is necessary
&lt;/h2&gt;

&lt;p&gt;According to LINE's &lt;a href="https://developers.line.biz/en/docs/line-mini-app/discover/custom-features/" rel="noopener noreferrer"&gt;custom-features matrix&lt;/a&gt;, these capabilities require a verified MINI App:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Verification required&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Production service messages&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom Path&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home-screen shortcut&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Common Profile Quick-fill&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MINI App name and verified badge in the header&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official Account add-friend prompt&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom action button&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment-system integration&lt;/td&gt;
&lt;td&gt;No, but availability and separate reviews vary by region&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If none of the verified-only features are on your critical path, publishing the core experience as an unverified MINI App may be a reasonable first release.&lt;/p&gt;

&lt;p&gt;If verification is required, budget for the review before committing to a launch date.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Align the organization identity
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developers.line.biz/en/docs/line-mini-app/submit/submission-guide/" rel="noopener noreferrer"&gt;official submission guide&lt;/a&gt; requires the provider name to match the service provider. The company named in the privacy policy must also match the provider.&lt;/p&gt;

&lt;p&gt;Audit these fields together:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Value to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LINE Developers Console provider&lt;/td&gt;
&lt;td&gt;Registered provider name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel information&lt;/td&gt;
&lt;td&gt;Service provider name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy policy&lt;/td&gt;
&lt;td&gt;Company acquiring or processing user data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel description&lt;/td&gt;
&lt;td&gt;Company and service relationship&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Localized information&lt;/td&gt;
&lt;td&gt;The same identity in every supported language&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A simple internal review 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;"providerName"&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 Company Ltd."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"serviceProviderName"&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 Company Ltd."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"privacyPolicyCompany"&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 Company Ltd."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"matched"&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;Do not assume that a brand name, parent company, subsidiary, or trading name is automatically equivalent to the registered provider.&lt;/p&gt;

&lt;p&gt;If development and service operation are handled by different companies, describe that relationship clearly in the channel description and privacy policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Make the channel description testable
&lt;/h2&gt;

&lt;p&gt;A vague description tells the reviewer what the business is, but not what the MINI App actually does.&lt;/p&gt;

&lt;p&gt;Weak description:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is an online store.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Stronger description:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This LINE MINI App lets customers select a store, order products, complete payment, and present the resulting pickup code at the selected location.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The stronger version identifies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The primary user&lt;/li&gt;
&lt;li&gt;The core workflow&lt;/li&gt;
&lt;li&gt;Payment or reservation behavior&lt;/li&gt;
&lt;li&gt;The result generated by the MINI App&lt;/li&gt;
&lt;li&gt;What the reviewer should be able to test&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compare the description with the current UI. Names, buttons, navigation, and available features should match what you submit.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Verify the Review channel
&lt;/h2&gt;

&lt;p&gt;During verification, LINE evaluates the LIFF URL of the Review channel.&lt;/p&gt;

&lt;p&gt;Before requesting review, confirm that the Review and Published channels expose the same service:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same core features&lt;/li&gt;
&lt;li&gt;Same screen transitions&lt;/li&gt;
&lt;li&gt;Same product or reservation data&lt;/li&gt;
&lt;li&gt;Same authentication requirements&lt;/li&gt;
&lt;li&gt;Same payment behavior&lt;/li&gt;
&lt;li&gt;Same privacy and terms links&lt;/li&gt;
&lt;li&gt;Same error and completion states&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not leave the Review channel pointed at an old staging deployment.&lt;/p&gt;

&lt;p&gt;Create an environment record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Published endpoint: https://app.example.com/
Review endpoint:    https://review.example.com/
Release version:    2026.08.03
Test data version:  review-fixtures-v3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid including secrets in normal logs, screenshots, source control, or public documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Prepare complete test scenarios
&lt;/h2&gt;

&lt;p&gt;For services containing reservations, payments, or orders, LINE asks applicants to provide test scenarios in the review reference materials.&lt;/p&gt;

&lt;p&gt;A useful scenario should contain:&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;scenario&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Complete a store pickup order&lt;/span&gt;
&lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PROVIDED_IN_REVIEW_FORM&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TEMPORARY_REVIEW_CREDENTIAL&lt;/span&gt;
&lt;span class="na"&gt;preconditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Test store is available&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Sample product is in stock&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="s"&gt;Open the MINI App&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Select the test store&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Add the sample product&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Complete the test payment flow&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Open the order result&lt;/span&gt;
&lt;span class="na"&gt;expected_result&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Order confirmation is displayed&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Pickup code is generated&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Order appears in order history&lt;/span&gt;
&lt;span class="na"&gt;cleanup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Cancel or reset the test order&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prepare separate scenarios for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registration and login&lt;/li&gt;
&lt;li&gt;Reservation creation&lt;/li&gt;
&lt;li&gt;Reservation cancellation&lt;/li&gt;
&lt;li&gt;Successful payment&lt;/li&gt;
&lt;li&gt;Failed or cancelled payment&lt;/li&gt;
&lt;li&gt;Order completion&lt;/li&gt;
&lt;li&gt;Empty or unavailable inventory&lt;/li&gt;
&lt;li&gt;Permission denial&lt;/li&gt;
&lt;li&gt;Service-message delivery, if applicable&lt;/li&gt;
&lt;li&gt;Account deletion or data-management flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reviewer should not need to guess which product, store, date, or account can complete the flow.&lt;/p&gt;

&lt;p&gt;If Basic Authentication protects the Review channel, provide the temporary credentials in the designated review materials. Rotate or remove them after the review.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Check privacy and terms pages
&lt;/h2&gt;

&lt;p&gt;Review every public legal page from a logged-out browser.&lt;/p&gt;

&lt;p&gt;Confirm that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The URL is publicly reachable&lt;/li&gt;
&lt;li&gt;The company name matches the submitted provider&lt;/li&gt;
&lt;li&gt;The service name matches the MINI App&lt;/li&gt;
&lt;li&gt;Data collection and processing are described&lt;/li&gt;
&lt;li&gt;Contact information works&lt;/li&gt;
&lt;li&gt;The page is readable on mobile&lt;/li&gt;
&lt;li&gt;Redirects do not require an internal session&lt;/li&gt;
&lt;li&gt;Every configured localization points to an appropriate document&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The privacy-policy company is especially important because LINE explicitly compares it with the provider name.&lt;/p&gt;

&lt;p&gt;A Terms of use URL may be optional depending on the channel configuration, but if you configure one, treat it as review-controlled information. Changes to either the privacy-policy or terms URL after verification can require re-review.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Run a policy gate before the technical review
&lt;/h2&gt;

&lt;p&gt;A technically correct MINI App can still be rejected if the underlying business or content conflicts with the &lt;a href="https://terms2.line.me/LINE_MINI_App?lang=en" rel="noopener noreferrer"&gt;LINE MINI App Policy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Examples of restricted business categories include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gambling&lt;/li&gt;
&lt;li&gt;Dating and matchmaking&lt;/li&gt;
&lt;li&gt;Lending&lt;/li&gt;
&lt;li&gt;Fundraising and crowdfunding&lt;/li&gt;
&lt;li&gt;Multi-level marketing&lt;/li&gt;
&lt;li&gt;Tobacco and e-cigarettes&lt;/li&gt;
&lt;li&gt;Weapons and poisons&lt;/li&gt;
&lt;li&gt;Clinical trials&lt;/li&gt;
&lt;li&gt;Certain religious, political, adult-entertainment, and investment-related services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also review the actual content for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Intellectual-property infringement&lt;/li&gt;
&lt;li&gt;Misleading claims&lt;/li&gt;
&lt;li&gt;Spam&lt;/li&gt;
&lt;li&gt;Aggressive user inducement&lt;/li&gt;
&lt;li&gt;Unsafe or age-inappropriate material&lt;/li&gt;
&lt;li&gt;Unreliable product comparisons or rankings&lt;/li&gt;
&lt;li&gt;Features that move the main service entirely outside the MINI App&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat policy eligibility as a product requirement, not a final copy-editing task.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Review API scopes
&lt;/h2&gt;

&lt;p&gt;Request only the scopes required by the submitted workflow.&lt;/p&gt;

&lt;p&gt;For each scope, document:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Feature using it&lt;/th&gt;
&lt;th&gt;Screen&lt;/th&gt;
&lt;th&gt;Required?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Profile access&lt;/td&gt;
&lt;td&gt;Populate customer name&lt;/td&gt;
&lt;td&gt;Checkout&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenID&lt;/td&gt;
&lt;td&gt;Identify returning customer&lt;/td&gt;
&lt;td&gt;Login&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Additional scope&lt;/td&gt;
&lt;td&gt;Related feature&lt;/td&gt;
&lt;td&gt;Screen name&lt;/td&gt;
&lt;td&gt;Yes/No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Remove experimental scopes that are not part of the submitted service.&lt;/p&gt;

&lt;p&gt;After verification, changing scopes is one of the configuration changes that can require another review.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Treat service messages as a separate approval gate
&lt;/h2&gt;

&lt;p&gt;Verification makes the channel eligible for production service messages, but it does not automatically approve every message template.&lt;/p&gt;

&lt;p&gt;Production service messages require both:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A verified MINI App&lt;/li&gt;
&lt;li&gt;An approved service-message template&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Service messages must confirm or respond to an action performed by the user in the MINI App.&lt;/p&gt;

&lt;p&gt;Appropriate examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reservation confirmation&lt;/li&gt;
&lt;li&gt;Payment result&lt;/li&gt;
&lt;li&gt;Order status&lt;/li&gt;
&lt;li&gt;Appointment reminder&lt;/li&gt;
&lt;li&gt;Completion of a submitted request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Promotional content is not allowed, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discounts&lt;/li&gt;
&lt;li&gt;Shopping rewards&lt;/li&gt;
&lt;li&gt;New-product announcements&lt;/li&gt;
&lt;li&gt;Coupons&lt;/li&gt;
&lt;li&gt;General promotions&lt;/li&gt;
&lt;li&gt;Unrelated event announcements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Audit template variables, permanent links, languages, and the triggering user action before submitting the template review.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Freeze re-review-sensitive settings
&lt;/h2&gt;

&lt;p&gt;After verification, changes to many LINE Developers Console settings require re-review.&lt;/p&gt;

&lt;p&gt;These include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Channel icon&lt;/li&gt;
&lt;li&gt;Channel name&lt;/li&gt;
&lt;li&gt;Channel description&lt;/li&gt;
&lt;li&gt;Privacy policy URL&lt;/li&gt;
&lt;li&gt;Terms of use URL&lt;/li&gt;
&lt;li&gt;Localization&lt;/li&gt;
&lt;li&gt;Linked LINE Official Account&lt;/li&gt;
&lt;li&gt;Published Endpoint URL&lt;/li&gt;
&lt;li&gt;Scopes&lt;/li&gt;
&lt;li&gt;Add-friend option&lt;/li&gt;
&lt;li&gt;Service company information&lt;/li&gt;
&lt;li&gt;Provider information&lt;/li&gt;
&lt;li&gt;Service-message template information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete list is documented in LINE's &lt;a href="https://developers.line.biz/en/docs/line-mini-app/service/update-service/" rel="noopener noreferrer"&gt;re-review guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Before the first submission, freeze:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Channel identity
Legal URLs
Production endpoint
Required scopes
Official Account linkage
Localization
Service-message templates
Review test data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not mean the product can never change. It means review-controlled changes should be planned as release work with their own approval lead time.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Plan for the real review timeline
&lt;/h2&gt;

&lt;p&gt;LINE states that verification normally takes approximately one to two weeks.&lt;/p&gt;

&lt;p&gt;Applicants cannot specify the completion date. If the application is rejected, correcting the issue and requesting another review adds more time.&lt;/p&gt;

&lt;p&gt;A safer release plan is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internal submission audit
        ↓
First review request
        ↓
1–2 week review window
        ↓
Re-review buffer
        ↓
Production launch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the review process begins, the submitted information cannot be edited. A review request can only be cancelled before LINE starts reviewing it.&lt;/p&gt;

&lt;p&gt;If a campaign or seasonal launch depends on a verified-only capability, do not schedule the public launch immediately after the earliest possible approval date.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Keep verification separate from inbound messaging
&lt;/h2&gt;

&lt;p&gt;LINE MINI App verification answers this question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can this MINI App use verified-only platform capabilities?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It does not answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How will ordinary LINE customer conversations reach our support system?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Receiving customer messages, normalizing webhook events, routing conversations, and sending supported replies are separate architecture decisions.&lt;/p&gt;

&lt;p&gt;Do not assume that MINI App verification automatically provides a general customer-message inbox. Likewise, connecting an inbound messaging API does not grant a Custom Path, verified badge, or production service-message capability.&lt;/p&gt;

&lt;p&gt;If the product uses both systems, connect them deliberately through stable customer, reservation, or order identifiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final pre-submission checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Verification is required by a specific product feature&lt;/li&gt;
&lt;li&gt;[ ] Provider and service-provider names match&lt;/li&gt;
&lt;li&gt;[ ] Privacy-policy company matches the provider&lt;/li&gt;
&lt;li&gt;[ ] Localized identity information is consistent&lt;/li&gt;
&lt;li&gt;[ ] Channel description explains the real workflow&lt;/li&gt;
&lt;li&gt;[ ] Review and Published channels expose the same service&lt;/li&gt;
&lt;li&gt;[ ] Production endpoint has been frozen&lt;/li&gt;
&lt;li&gt;[ ] Privacy and terms pages are publicly accessible&lt;/li&gt;
&lt;li&gt;[ ] Every reservation, payment, and order flow has a test scenario&lt;/li&gt;
&lt;li&gt;[ ] Temporary review credentials work&lt;/li&gt;
&lt;li&gt;[ ] Test data can complete every submitted scenario&lt;/li&gt;
&lt;li&gt;[ ] Business category and content pass the policy audit&lt;/li&gt;
&lt;li&gt;[ ] API scopes match implemented features&lt;/li&gt;
&lt;li&gt;[ ] Service-message templates are reviewed separately&lt;/li&gt;
&lt;li&gt;[ ] Re-review-sensitive settings have owners&lt;/li&gt;
&lt;li&gt;[ ] Launch planning includes a re-review buffer&lt;/li&gt;
&lt;li&gt;[ ] Review credentials can be rotated after approval&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A successful verification submission is not just an application that launches. It is a review package in which the legal identity, configuration, product description, policies, and test environment all describe the same service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/line-mini-app/submit/submission-guide/" rel="noopener noreferrer"&gt;Submitting a LINE MINI App&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/line-mini-app/discover/custom-features/" rel="noopener noreferrer"&gt;Verified and unverified custom features&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://terms2.line.me/LINE_MINI_App?lang=en" rel="noopener noreferrer"&gt;LINE MINI App Policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/line-mini-app/service/update-service/" rel="noopener noreferrer"&gt;Re-review after updating a verified MINI App&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/line-mini-app/develop/service-messages/" rel="noopener noreferrer"&gt;Sending service messages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.unifyport.ai/blog/line-mini-app-verification-review-checklist/" rel="noopener noreferrer"&gt;UnifyPort&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was prepared with AI assistance for language and structure, then technically reviewed and verified by the author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Telegram Bot API 10.2 Migration Guide: Rich Messages, Ephemeral Edits, and Communities</title>
      <dc:creator>unifyport</dc:creator>
      <pubDate>Thu, 30 Jul 2026 09:04:09 +0000</pubDate>
      <link>https://dev.to/unifyport/telegram-bot-api-102-migration-guide-rich-messages-ephemeral-edits-and-communities-gfe</link>
      <guid>https://dev.to/unifyport/telegram-bot-api-102-migration-guide-rich-messages-ephemeral-edits-and-communities-gfe</guid>
      <description>&lt;p&gt;Telegram Bot API 10.2 looks like a small version bump, but it changes several parts of a production bot at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rich Messages gain typed blocks and embedded media&lt;/li&gt;
&lt;li&gt;Ephemeral messages gain a complete edit and delete lifecycle&lt;/li&gt;
&lt;li&gt;Communities introduce new topology events and metadata&lt;/li&gt;
&lt;li&gt;Mini Apps receive stricter origin protection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Telegram released Bot API 10.2 on July 14, 2026. The safest upgrade is not to enable every new feature immediately. First update your data model and event dispatcher, then test each outbound capability behind a feature flag.&lt;/p&gt;

&lt;p&gt;This guide turns the &lt;a href="https://core.telegram.org/bots/api-changelog" rel="noopener noreferrer"&gt;official Bot API 10.2 changelog&lt;/a&gt; into an implementation checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with an impact map
&lt;/h2&gt;

&lt;p&gt;Before changing code, separate the upgrade into four areas.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Main change&lt;/th&gt;
&lt;th&gt;Primary risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rich Messages&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;media&lt;/code&gt;, &lt;code&gt;blocks&lt;/code&gt;, and new &lt;code&gt;InputRichBlock*&lt;/code&gt; types&lt;/td&gt;
&lt;td&gt;Invalid payloads or incomplete SDK serialization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ephemeral messages&lt;/td&gt;
&lt;td&gt;Send, reply, edit, and delete lifecycle&lt;/td&gt;
&lt;td&gt;Losing the user-specific message identifiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Communities&lt;/td&gt;
&lt;td&gt;New lifecycle fields and &lt;code&gt;ChatFullInfo.community&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Dropping topology events in a default handler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mini Apps&lt;/td&gt;
&lt;td&gt;Cross-origin method protection&lt;/td&gt;
&lt;td&gt;Previously working cross-origin calls being rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You do not have to enable every feature during the same deployment.&lt;/p&gt;

&lt;p&gt;A reasonable rollout order is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upgrade the library and types&lt;/li&gt;
&lt;li&gt;Accept and store the new fields&lt;/li&gt;
&lt;li&gt;Add dispatcher coverage&lt;/li&gt;
&lt;li&gt;Run regression tests&lt;/li&gt;
&lt;li&gt;Enable outbound features separately&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Pin a compatible Bot API library
&lt;/h2&gt;

&lt;p&gt;Check whether your Telegram library has released a version compatible with Bot API 10.2.&lt;/p&gt;

&lt;p&gt;Before updating production, inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generated API types&lt;/li&gt;
&lt;li&gt;Serialization of optional fields&lt;/li&gt;
&lt;li&gt;Method names and parameter casing&lt;/li&gt;
&lt;li&gt;Webhook update types&lt;/li&gt;
&lt;li&gt;Retry and error behavior&lt;/li&gt;
&lt;li&gt;Support for unknown message fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not assume that upgrading the package automatically enables the new behavior. Some libraries may expose new types before fully supporting every method.&lt;/p&gt;

&lt;p&gt;Pin the selected version instead of using an open-ended dependency range:&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;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"your-telegram-library"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PINNED_COMPATIBLE_VERSION"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the upgrade in a branch and keep the previous dependency version available for rollback.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Audit Rich Message construction
&lt;/h2&gt;

&lt;p&gt;Bot API 10.2 adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;InputRichMessageMedia&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InputMediaVoiceNote&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;media&lt;/code&gt; on &lt;code&gt;InputRichMessage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;blocks&lt;/code&gt; on &lt;code&gt;InputRichMessage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A full set of &lt;code&gt;InputRichBlock*&lt;/code&gt; builders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;According to the &lt;a href="https://core.telegram.org/bots/api" rel="noopener noreferrer"&gt;Bot API reference&lt;/a&gt;, an &lt;code&gt;InputRichMessage&lt;/code&gt; must use exactly one of these content representations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;markdown&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;blocks&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you generate payloads dynamically, validate that rule before making the API call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateRichMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;html&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;media&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;representations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;representations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="s2"&gt;A rich message must contain exactly one of html, markdown, or blocks&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new &lt;code&gt;media&lt;/code&gt; field is used with media references embedded in HTML or Markdown, such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tg://photo?id=product_photo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review the following before enabling media:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Media identifiers are unique inside the payload&lt;/li&gt;
&lt;li&gt;Referenced media actually exists in the &lt;code&gt;media&lt;/code&gt; array&lt;/li&gt;
&lt;li&gt;The bot has permission to send that media type&lt;/li&gt;
&lt;li&gt;A plain-text fallback is available&lt;/li&gt;
&lt;li&gt;Your SDK preserves every new field during serialization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not silently convert an existing Markdown builder into a block builder during the dependency upgrade. Treat that as a separate feature change.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Preserve the ephemeral message identity
&lt;/h2&gt;

&lt;p&gt;Bot API 10.2 completes the ephemeral-message lifecycle with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;editEphemeralMessageText&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;editEphemeralMessageMedia&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;editEphemeralMessageCaption&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;editEphemeralMessageReplyMarkup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deleteEphemeralMessage&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also adds &lt;code&gt;receiver_user&lt;/code&gt; and &lt;code&gt;ephemeral_message_id&lt;/code&gt; to &lt;code&gt;Message&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A normal message ID is not enough to manage an ephemeral message. Store the complete routing identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EphemeralMessageReference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;receiverUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;ephemeralMessageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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 library call may look similar to this, although the exact method naming depends on your SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;editEphemeralMessageText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;chat_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;receiver_user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;receiverUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ephemeral_message_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ephemeralMessageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your request has been updated.&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;Also review reply construction. In Bot API 10.2, &lt;code&gt;ReplyParameters.message_id&lt;/code&gt; becomes optional when &lt;code&gt;ephemeral_message_id&lt;/code&gt; is present.&lt;/p&gt;

&lt;p&gt;Your implementation should test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sending to the intended user&lt;/li&gt;
&lt;li&gt;Editing text&lt;/li&gt;
&lt;li&gt;Editing media or captions&lt;/li&gt;
&lt;li&gt;Updating reply markup&lt;/li&gt;
&lt;li&gt;Deleting the message&lt;/li&gt;
&lt;li&gt;Rejecting edits from the wrong user context&lt;/li&gt;
&lt;li&gt;Handling an expired or unknown identifier&lt;/li&gt;
&lt;li&gt;Preventing a retry from creating a duplicate reply&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ephemeral messages are user-specific. Never treat them as a group broadcast mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Add Community events to the dispatcher
&lt;/h2&gt;

&lt;p&gt;Communities link supergroups, channels, and bots around a shared topic or audience.&lt;/p&gt;

&lt;p&gt;Bot API 10.2 introduces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Community&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CommunityChatAdded&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CommunityChatRemoved&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;message.community_chat_added&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;message.community_chat_removed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ChatFullInfo.community&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A handler that only checks &lt;code&gt;message.text&lt;/code&gt; may silently discard these service messages.&lt;/p&gt;

&lt;p&gt;Add explicit branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TelegramMessage&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;community_chat_added&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;recordCommunityChatAdded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;community_chat_removed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;recordCommunityChatRemoved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;handleTextMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="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="nf"&gt;handleUnknownMessageType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not use the Community ID as a replacement for the original chat ID.&lt;/p&gt;

&lt;p&gt;A Community describes topology. Messages still need to be stored and routed according to their original chat identity.&lt;/p&gt;

&lt;p&gt;A practical metadata model can retain both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CommunityChatRelation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;communityId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;relationStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;removed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;observedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;When topology changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Persist the lifecycle event&lt;/li&gt;
&lt;li&gt;Update the relation state&lt;/li&gt;
&lt;li&gt;Reconcile current information with &lt;code&gt;getChat&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Keep an audit record&lt;/li&gt;
&lt;li&gt;Continue routing messages by their original chat ID&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. Handle the additional update type
&lt;/h2&gt;

&lt;p&gt;Bot API 10.2 also adds &lt;code&gt;BotSubscriptionUpdated&lt;/code&gt; and the &lt;code&gt;subscription&lt;/code&gt; field on &lt;code&gt;Update&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Even if your bot does not currently use payment subscriptions, make sure the webhook decoder does not reject an update merely because this field is present.&lt;/p&gt;

&lt;p&gt;The safe pattern is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parse known top-level fields&lt;/li&gt;
&lt;li&gt;Preserve unknown fields where possible&lt;/li&gt;
&lt;li&gt;Log the event category without sensitive payload data&lt;/li&gt;
&lt;li&gt;Route unsupported updates to an observable fallback&lt;/li&gt;
&lt;li&gt;Avoid failing the entire webhook request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Returning a non-success response for every unknown update can create unnecessary retries.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Recheck Mini App origins
&lt;/h2&gt;

&lt;p&gt;Telegram automatically enabled stricter Mini App origin protection on July 20, 2026, unless the bot opted out through BotFather.&lt;/p&gt;

&lt;p&gt;Audit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The configured Mini App domain&lt;/li&gt;
&lt;li&gt;Redirect destinations&lt;/li&gt;
&lt;li&gt;Embedded authentication pages&lt;/li&gt;
&lt;li&gt;Payment or support pages&lt;/li&gt;
&lt;li&gt;Calls made after navigation&lt;/li&gt;
&lt;li&gt;Third-party content opened inside the Mini App&lt;/li&gt;
&lt;li&gt;Development and staging domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not solve an origin failure by broadly disabling protection without understanding which page initiated the call.&lt;/p&gt;

&lt;p&gt;Test the production domain and each allowed non-production environment separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Avoid an unnecessary inbound rewrite
&lt;/h2&gt;

&lt;p&gt;The new &lt;code&gt;InputRichMessage&lt;/code&gt; builders primarily affect the outbound path.&lt;/p&gt;

&lt;p&gt;They do not mean that every ordinary incoming user message must suddenly be parsed as a tree of rich blocks.&lt;/p&gt;

&lt;p&gt;Your inbound work should focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recognizing the new Community service-message fields&lt;/li&gt;
&lt;li&gt;Accepting &lt;code&gt;subscription&lt;/code&gt; updates&lt;/li&gt;
&lt;li&gt;Preserving new optional fields&lt;/li&gt;
&lt;li&gt;Keeping an observable unknown-event fallback&lt;/li&gt;
&lt;li&gt;Maintaining the existing text, media, and callback handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a compatibility update, not a reason to replace a working inbound normalization pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Deploy behind separate feature flags
&lt;/h2&gt;

&lt;p&gt;Use independent flags instead of one global “Bot API 10.2” switch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;telegramFeatures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;richMessageBlocks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;richMessageMedia&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ephemeralEdits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;communityTopology&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you accept Community events immediately while delaying new outbound formatting.&lt;/p&gt;

&lt;p&gt;A safe rollout looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Upgrade dependency
        ↓
Accept new webhook fields
        ↓
Deploy with outbound flags disabled
        ↓
Verify normal inbound traffic
        ↓
Enable one outbound feature internally
        ↓
Monitor errors and payloads
        ↓
Expand gradually
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Monitor at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Telegram API error codes&lt;/li&gt;
&lt;li&gt;Serialization failures&lt;/li&gt;
&lt;li&gt;Unknown update counts&lt;/li&gt;
&lt;li&gt;Community topology changes&lt;/li&gt;
&lt;li&gt;Ephemeral edit/delete failures&lt;/li&gt;
&lt;li&gt;Rich-message fallback usage&lt;/li&gt;
&lt;li&gt;Webhook retry volume&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Build a rollback path
&lt;/h2&gt;

&lt;p&gt;Before production rollout, record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Previous library version&lt;/li&gt;
&lt;li&gt;New database fields&lt;/li&gt;
&lt;li&gt;Feature-flag defaults&lt;/li&gt;
&lt;li&gt;Payload format differences&lt;/li&gt;
&lt;li&gt;Reversible and irreversible migrations&lt;/li&gt;
&lt;li&gt;Rollback owner&lt;/li&gt;
&lt;li&gt;Monitoring thresholds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If new fields are optional, prefer additive database changes. Do not make the previous application version unable to read records created during the rollout.&lt;/p&gt;

&lt;p&gt;The rollback should disable new sends while still accepting already-delivered webhook fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final upgrade checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Bot API library version is pinned&lt;/li&gt;
&lt;li&gt;[ ] New SDK types and serialization have been inspected&lt;/li&gt;
&lt;li&gt;[ ] Existing inbound regression tests pass&lt;/li&gt;
&lt;li&gt;[ ] Exactly one Rich Message representation is enforced&lt;/li&gt;
&lt;li&gt;[ ] Rich Message media references are validated&lt;/li&gt;
&lt;li&gt;[ ] Plain-text fallbacks are available&lt;/li&gt;
&lt;li&gt;[ ] Ephemeral message identifiers are persisted&lt;/li&gt;
&lt;li&gt;[ ] Ephemeral edit and delete failure paths are tested&lt;/li&gt;
&lt;li&gt;[ ] Community lifecycle events have explicit handler branches&lt;/li&gt;
&lt;li&gt;[ ] Community topology is stored separately from chat routing&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;subscription&lt;/code&gt; updates do not break webhook decoding&lt;/li&gt;
&lt;li&gt;[ ] Mini App production and staging origins have been checked&lt;/li&gt;
&lt;li&gt;[ ] Outbound features use separate flags&lt;/li&gt;
&lt;li&gt;[ ] Unknown updates remain observable&lt;/li&gt;
&lt;li&gt;[ ] Dependency and feature rollback steps are documented&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bot API 10.2 is manageable when it is treated as several small migrations instead of one large feature launch.&lt;/p&gt;

&lt;p&gt;Update the compatibility layer first. Enable Rich Messages, ephemeral editing, and Community-aware behavior only after the underlying fields, routing rules, and rollback path are ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://core.telegram.org/bots/api-changelog" rel="noopener noreferrer"&gt;Telegram Bot API 10.2 changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://core.telegram.org/bots/api" rel="noopener noreferrer"&gt;Telegram Bot API reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.unifyport.ai/blog/telegram-bot-api-10-2-upgrade-checklist/" rel="noopener noreferrer"&gt;UnifyPort&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was prepared with AI assistance for language and structure, then technically reviewed and verified by the author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Before You Submit a LINE MINI App Service Message Template: A Review Checklist</title>
      <dc:creator>unifyport</dc:creator>
      <pubDate>Tue, 28 Jul 2026 08:16:49 +0000</pubDate>
      <link>https://dev.to/unifyport_cool/before-you-submit-a-line-mini-app-service-message-template-a-review-checklist-4611</link>
      <guid>https://dev.to/unifyport_cool/before-you-submit-a-line-mini-app-service-message-template-a-review-checklist-4611</guid>
      <description>&lt;p&gt;Passing a simulator test does not mean a LINE MINI App service message is ready for production.&lt;/p&gt;

&lt;p&gt;A production service message must pass three separate gates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your LINE MINI App must be verified.&lt;/li&gt;
&lt;li&gt;The service message template must pass review.&lt;/li&gt;
&lt;li&gt;Your application must supply a valid service notification token at runtime.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This checklist helps you catch review and implementation problems before they delay your release.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Understand the three production gates
&lt;/h2&gt;

&lt;p&gt;Treat verification, template review, and runtime delivery as separate controls.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;What it checks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MINI App verification&lt;/td&gt;
&lt;td&gt;Whether the service can use service messages in production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Template review&lt;/td&gt;
&lt;td&gt;Whether the message content and use case comply with LINE requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime token&lt;/td&gt;
&lt;td&gt;Whether the message can be sent for the correct user action&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An unverified MINI App can test service messages only through a Developing channel with users who have an Admin or Tester role.&lt;/p&gt;

&lt;p&gt;Even after a template passes review, delivery can still fail if the service notification token is missing, expired, already used incorrectly, or unrelated to the current user action.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Know the template states
&lt;/h2&gt;

&lt;p&gt;The template status in the LINE Developers Console is an operational signal, not just a label.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DEVELOPING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Review has not been requested. The template is available only for development testing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Under review&lt;/td&gt;
&lt;td&gt;The submitted template is being evaluated. Some template operations are restricted.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PUBLISHING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The template passed review and can be used in production after the MINI App is verified.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;While a template is under review, you can inspect it and run simulator tests, but you cannot freely add templates, delete them, or edit their Use Case.&lt;/p&gt;

&lt;p&gt;Plan your complete template set before requesting review.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Start from the user action
&lt;/h2&gt;

&lt;p&gt;A service message should confirm, report the result of, or remind the user about an action they performed in the MINI App.&lt;/p&gt;

&lt;p&gt;Good use cases include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirming a reservation&lt;/li&gt;
&lt;li&gt;Reporting a payment result&lt;/li&gt;
&lt;li&gt;Sending a delivery update&lt;/li&gt;
&lt;li&gt;Reminding a user about an upcoming appointment&lt;/li&gt;
&lt;li&gt;Reporting that a submitted request has completed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A weak use case would be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Send customers information about available products.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A stronger version would be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After a customer completes a delivery booking, send a confirmation containing the booking number, delivery date, and status page link.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second version identifies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The triggering action&lt;/li&gt;
&lt;li&gt;The recipient&lt;/li&gt;
&lt;li&gt;The delivery timing&lt;/li&gt;
&lt;li&gt;The message purpose&lt;/li&gt;
&lt;li&gt;The expected content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Promotional messages, discounts, coupons, shopping rewards, new-product announcements, and general event announcements are not appropriate service-message use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Document the entire message sequence
&lt;/h2&gt;

&lt;p&gt;Do not review each message in isolation.&lt;/p&gt;

&lt;p&gt;Document the sequence from the triggering action to the final service message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User completes an action
        ↓
MINI App receives a service notification token
        ↓
Backend stores the token securely
        ↓
Backend selects an approved template
        ↓
Service message is sent
        ↓
Renewed token and remaining count are processed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each message, record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Triggering user action&lt;/li&gt;
&lt;li&gt;Delivery timing&lt;/li&gt;
&lt;li&gt;Template name&lt;/li&gt;
&lt;li&gt;Language&lt;/li&gt;
&lt;li&gt;Variable source&lt;/li&gt;
&lt;li&gt;Destination link&lt;/li&gt;
&lt;li&gt;Expected next user action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LINE normally permits up to five service messages for one user action, although the reviewed limit may differ by use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Choose the template and language early
&lt;/h2&gt;

&lt;p&gt;LINE provides predefined service message templates. A channel can register up to 20 templates.&lt;/p&gt;

&lt;p&gt;Supported template languages include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Japanese&lt;/li&gt;
&lt;li&gt;English&lt;/li&gt;
&lt;li&gt;Traditional Chinese&lt;/li&gt;
&lt;li&gt;Thai&lt;/li&gt;
&lt;li&gt;Indonesian&lt;/li&gt;
&lt;li&gt;Korean&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose the required language variants before review. Adding or changing template information later can require another review.&lt;/p&gt;

&lt;p&gt;For API requests, use the exact template name displayed in the LINE Developers Console. The effective name includes the template name and its BCP 47 language tag.&lt;/p&gt;

&lt;p&gt;Do not reconstruct or translate the identifier in application code.&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 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;"templateName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"delivery_result_en"&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;Treat &lt;code&gt;templateName&lt;/code&gt; as an approved configuration value rather than a label generated at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Validate every variable
&lt;/h2&gt;

&lt;p&gt;For every variable, confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Its value comes from the action described in the Use Case&lt;/li&gt;
&lt;li&gt;It is available when the message is sent&lt;/li&gt;
&lt;li&gt;It cannot contain unexpected HTML or formatting&lt;/li&gt;
&lt;li&gt;Its fallback behavior is defined&lt;/li&gt;
&lt;li&gt;Its maximum length is enforced before the API call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LINE applies different recommendations and limits depending on the variable type.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable type&lt;/th&gt;
&lt;th&gt;Recommended&lt;/th&gt;
&lt;th&gt;Soft limit&lt;/th&gt;
&lt;th&gt;Hard limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Detailed&lt;/td&gt;
&lt;td&gt;10 grapheme clusters&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simple&lt;/td&gt;
&lt;td&gt;32 grapheme clusters&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A grapheme cluster is closer to a user-perceived character than a raw byte or code point. Emoji and combined Unicode characters make this distinction important.&lt;/p&gt;

&lt;p&gt;Your preflight validation should therefore use Unicode-aware length checks.&lt;/p&gt;

&lt;p&gt;If a template contains no variables, send an empty &lt;code&gt;params&lt;/code&gt; object rather than inventing placeholder values:&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;"templateName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"booking_confirmation_en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Review button behavior
&lt;/h2&gt;

&lt;p&gt;For templates containing buttons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first button is required&lt;/li&gt;
&lt;li&gt;Additional buttons may be optional&lt;/li&gt;
&lt;li&gt;Each button must use a permanent link&lt;/li&gt;
&lt;li&gt;The destination must be available after the message is delivered&lt;/li&gt;
&lt;li&gt;The link should take the user directly to the relevant result or next action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid temporary URLs that expire before the user opens the message.&lt;/p&gt;

&lt;p&gt;Also test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the link opens the expected MINI App page&lt;/li&gt;
&lt;li&gt;Whether authentication state is handled correctly&lt;/li&gt;
&lt;li&gt;Whether an invalid or completed record has a safe fallback&lt;/li&gt;
&lt;li&gt;Whether the page still works when opened later from message history&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Use the simulator as a content test
&lt;/h2&gt;

&lt;p&gt;Before submission, preview every language variant in the simulator.&lt;/p&gt;

&lt;p&gt;Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long customer names&lt;/li&gt;
&lt;li&gt;Long order or reservation identifiers&lt;/li&gt;
&lt;li&gt;Emoji and combined characters&lt;/li&gt;
&lt;li&gt;Empty optional values&lt;/li&gt;
&lt;li&gt;Date and time formats&lt;/li&gt;
&lt;li&gt;Currency formats&lt;/li&gt;
&lt;li&gt;Button-label truncation&lt;/li&gt;
&lt;li&gt;Permanent-link destinations&lt;/li&gt;
&lt;li&gt;Differences between language variants&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The simulator helps validate layout and content. It does not prove that the template is approved or that production token handling works.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Freeze the review package
&lt;/h2&gt;

&lt;p&gt;Before requesting review, prepare one review package containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Final template list&lt;/li&gt;
&lt;li&gt;Languages&lt;/li&gt;
&lt;li&gt;Use Case&lt;/li&gt;
&lt;li&gt;Message sequence&lt;/li&gt;
&lt;li&gt;Variable definitions&lt;/li&gt;
&lt;li&gt;Example values&lt;/li&gt;
&lt;li&gt;Permanent links&lt;/li&gt;
&lt;li&gt;Screenshots or simulator results&lt;/li&gt;
&lt;li&gt;Runtime template identifiers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A compact internal 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;"templateName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"delivery_result_en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"approvedUseCase"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Send the delivery result after the user completes a booking"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"actionType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"delivery_booking"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"variables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"delivery_status"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permanentLink"&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://miniapp.line.me/example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PUBLISHING"&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;Freezing this information reduces the risk that the reviewed template differs from the implementation shipped by another team.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Prepare the production handoff
&lt;/h2&gt;

&lt;p&gt;Before enabling production traffic, verify all of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The MINI App is verified&lt;/li&gt;
&lt;li&gt;The required template status is &lt;code&gt;PUBLISHING&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The backend uses the exact reviewed &lt;code&gt;templateName&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Each token is associated with the correct user action&lt;/li&gt;
&lt;li&gt;Tokens are not exposed in application logs&lt;/li&gt;
&lt;li&gt;Renewed tokens are stored correctly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;remainingCount&lt;/code&gt; is checked when planning follow-up messages&lt;/li&gt;
&lt;li&gt;Retries cannot accidentally duplicate a service message&lt;/li&gt;
&lt;li&gt;Failures are observable without logging sensitive token values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Template approval and runtime token management solve different problems. Both must be tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Control changes after approval
&lt;/h2&gt;

&lt;p&gt;After the LINE MINI App is verified, changes to service message template information require re-review.&lt;/p&gt;

&lt;p&gt;Before modifying a reviewed template, compare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Triggering action&lt;/li&gt;
&lt;li&gt;Delivery timing&lt;/li&gt;
&lt;li&gt;Message purpose&lt;/li&gt;
&lt;li&gt;Text content&lt;/li&gt;
&lt;li&gt;Variables&lt;/li&gt;
&lt;li&gt;Language variants&lt;/li&gt;
&lt;li&gt;Button labels&lt;/li&gt;
&lt;li&gt;Permanent links&lt;/li&gt;
&lt;li&gt;Message sequence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not treat a text or URL adjustment as automatically safe just because the overall business flow has not changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final pre-submission checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] The Use Case begins with a specific user action&lt;/li&gt;
&lt;li&gt;[ ] The message is transactional, not promotional&lt;/li&gt;
&lt;li&gt;[ ] The full message sequence is documented&lt;/li&gt;
&lt;li&gt;[ ] The channel remains within the 20-template limit&lt;/li&gt;
&lt;li&gt;[ ] Every required language variant is ready&lt;/li&gt;
&lt;li&gt;[ ] Every variable has a source, limit, and fallback&lt;/li&gt;
&lt;li&gt;[ ] Unicode-aware length validation is implemented&lt;/li&gt;
&lt;li&gt;[ ] The first button and every permanent link have been tested&lt;/li&gt;
&lt;li&gt;[ ] Long and empty values have been tested in the simulator&lt;/li&gt;
&lt;li&gt;[ ] Runtime code uses the exact console template name&lt;/li&gt;
&lt;li&gt;[ ] The review package matches the implementation&lt;/li&gt;
&lt;li&gt;[ ] Production token storage, renewal, and retry behavior have been tested&lt;/li&gt;
&lt;li&gt;[ ] The team understands which future changes require re-review&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Official references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/line-mini-app/develop/service-messages/" rel="noopener noreferrer"&gt;Sending service messages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/line-mini-app/service/service-operation/" rel="noopener noreferrer"&gt;Running your LINE MINI App service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/line-mini-app/service/update-service/" rel="noopener noreferrer"&gt;Updating a verified LINE MINI App&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.unifyport.ai/blog/line-mini-app-service-message-template-review-checklist/" rel="noopener noreferrer"&gt;UnifyPort&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was prepared with AI assistance for language and structure, then technically reviewed and verified by the author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Stop Queued WhatsApp Replies After the 24-Hour Window Expires</title>
      <dc:creator>unifyport</dc:creator>
      <pubDate>Mon, 27 Jul 2026 02:51:54 +0000</pubDate>
      <link>https://dev.to/unifyport_cool/how-to-stop-queued-whatsapp-replies-after-the-24-hour-window-expires-flp</link>
      <guid>https://dev.to/unifyport_cool/how-to-stop-queued-whatsapp-replies-after-the-24-hour-window-expires-flp</guid>
      <description>&lt;p&gt;A WhatsApp reply can be valid when an agent writes it and invalid when a queue worker finally sends it.&lt;/p&gt;

&lt;p&gt;Approval queues, retries, outages, and delayed jobs can all push a free-form reply beyond WhatsApp's 24-hour customer service window. Checking the window only in the UI is therefore not enough—the sender needs a server-side guard immediately before transport.&lt;/p&gt;

&lt;p&gt;This tutorial builds that guard with explicit timestamps, idempotent inbound processing, and versioned conversation state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The queue race
&lt;/h2&gt;

&lt;p&gt;Consider this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:00 — Customer sends a message
10:05 — Agent writes a reply
10:10 — Reply is approved
09:59 next day — Job is still waiting
10:01 next day — Worker claims the job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reply was valid when it was written and approved, but the customer service window has expired by the time the worker attempts to send it.&lt;/p&gt;

&lt;p&gt;The worker must block the non-template send and return an explicit routing result. It must not trust a decision made earlier in the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model timestamps instead of one boolean
&lt;/h2&gt;

&lt;p&gt;Avoid storing a single value such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isFreeConversation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That value does not explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the window expires&lt;/li&gt;
&lt;li&gt;Which user message opened it&lt;/li&gt;
&lt;li&gt;Whether the event was duplicated&lt;/li&gt;
&lt;li&gt;Whether a newer event already changed the state&lt;/li&gt;
&lt;li&gt;Whether a separate free-entry period is active&lt;/li&gt;
&lt;li&gt;Whether a queued worker is using stale state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store explicit state instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WhatsAppWindowState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;serviceWindowExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;freeEntryExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;lastUserMessageAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;lastUserMessageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;stateVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;Each field has a separate job.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;serviceWindowExpiresAt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controls whether a non-template service reply is permitted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;freeEntryExpiresAt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Records the separate 72-hour free-entry billing period&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lastUserMessageAt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prevents an older event from moving the window backward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lastUserMessageId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Provides idempotency and audit evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stateVersion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Detects stale workers and concurrent state changes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 72-hour free-entry period is not an extended customer service window. Keep the two clocks separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluate permission separately from pricing
&lt;/h2&gt;

&lt;p&gt;The send guard should answer whether the selected transport is permitted.&lt;/p&gt;

&lt;p&gt;Do not hard-code market rates or future pricing rules into the permission state machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SendDecision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;non_template_service&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;serviceWindowExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;stateVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;freeEntryActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved_template_or_human_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;service_window_expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no_user_message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;stateVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;freeEntryActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&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;evaluateWhatsAppSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WhatsAppWindowState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;SendDecision&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;serviceWindowOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWindowExpiresAt&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWindowExpiresAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&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;freeEntryActive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;freeEntryExpiresAt&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;freeEntryExpiresAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&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;serviceWindowOpen&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;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved_template_or_human_review&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWindowExpiresAt&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
          &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no_user_message&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="s2"&gt;service_window_expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stateVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stateVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;freeEntryActive&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;non_template_service&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;serviceWindowExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWindowExpiresAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stateVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stateVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;freeEntryActive&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;Notice the strict comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;serviceWindowExpiresAt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the exact expiry timestamp, treat the window as closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update the window only from verified user messages
&lt;/h2&gt;

&lt;p&gt;Not every webhook should extend the service window.&lt;/p&gt;

&lt;p&gt;These events must not reset it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delivery receipts&lt;/li&gt;
&lt;li&gt;Read receipts&lt;/li&gt;
&lt;li&gt;Message status changes&lt;/li&gt;
&lt;li&gt;Agent drafts&lt;/li&gt;
&lt;li&gt;Internal notes&lt;/li&gt;
&lt;li&gt;Retry events&lt;/li&gt;
&lt;li&gt;Business-sent message echoes&lt;/li&gt;
&lt;li&gt;Duplicate webhook deliveries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A safe inbound sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify the provider webhook.&lt;/li&gt;
&lt;li&gt;Parse the verified payload.&lt;/li&gt;
&lt;li&gt;Confirm that the event represents a user-sent inbound message.&lt;/li&gt;
&lt;li&gt;Deduplicate using the stable message or event ID.&lt;/li&gt;
&lt;li&gt;Reject stale state changes from older events.&lt;/li&gt;
&lt;li&gt;Set the expiry to the accepted message timestamp plus 24 hours.&lt;/li&gt;
&lt;li&gt;Increment &lt;code&gt;stateVersion&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Save the source event for audit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A simplified updater:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;InboundUserMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;occurredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyInboundUserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WhatsAppWindowState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InboundUserMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;WhatsAppWindowState&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastUserMessageId&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;state&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastUserMessageAt&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;occurredAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastUserMessageAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&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;return&lt;/span&gt; &lt;span class="nx"&gt;state&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastUserMessageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastUserMessageAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;occurredAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;serviceWindowExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;occurredAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;stateVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stateVersion&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="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;Do not add 24 hours to the previous expiry.&lt;/p&gt;

&lt;p&gt;If the user sends a message at 09:00 and another at 12:00, the new expiry is 12:00 the next day—not one extra day after the previous expiry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recheck the state when claiming a queued reply
&lt;/h2&gt;

&lt;p&gt;The UI may display the current window, but the queue worker owns the final decision.&lt;/p&gt;

&lt;p&gt;When claiming a job:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lock the reply job.&lt;/li&gt;
&lt;li&gt;Load the latest conversation state.&lt;/li&gt;
&lt;li&gt;Evaluate the state using the current server time.&lt;/li&gt;
&lt;li&gt;Record the decision and &lt;code&gt;stateVersion&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Mark the job for the selected route.&lt;/li&gt;
&lt;li&gt;Commit the short transaction.&lt;/li&gt;
&lt;li&gt;Recheck the captured deadline immediately before transport.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;claimQueuedReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="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;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&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;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lockReplyJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&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;state&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;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lockConversationState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conversationId&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;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;queued&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;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;already_claimed&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;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluateWhatsAppSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&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;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed&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;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateReplyJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&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="s2"&gt;needs_rerouting&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="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;evaluatedStateVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stateVersion&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;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateReplyJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&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="s2"&gt;ready_to_send&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;evaluatedStateVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stateVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sendBefore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWindowExpiresAt&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;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;Do not keep a database transaction open across the external WhatsApp API request.&lt;/p&gt;

&lt;p&gt;Instead, persist the decision and deadline, then let the transport layer perform a final time check before sending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertBeforeTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;sendBefore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="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;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;sendBefore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;service_window_expired&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the job waits again after being marked ready, it must return through the guard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not silently convert expired text into a template
&lt;/h2&gt;

&lt;p&gt;When the 24-hour window closes, the state machine should return a routing decision—not rewrite content.&lt;/p&gt;

&lt;p&gt;Safe outcomes include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;non_template_service
approved_template
human_review
blocked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unsafe behavior includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatically converting arbitrary text into a template&lt;/li&gt;
&lt;li&gt;Truncating a reply until it fits a template&lt;/li&gt;
&lt;li&gt;Selecting a template based only on keyword similarity&lt;/li&gt;
&lt;li&gt;Reusing template variables with a different business purpose&lt;/li&gt;
&lt;li&gt;Sending first and reviewing the decision afterward&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Templates have their own approved wording, category, variables, and business intent. Those contracts belong to a separate decision step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the one-second boundaries
&lt;/h2&gt;

&lt;p&gt;Use a fixed clock in automated tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;evaluateWhatsAppSend&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="o"&gt;=&amp;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;expiry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-07-28T10:00:00.000Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WhatsAppWindowState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;serviceWindowExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;freeEntryExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastUserMessageAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-07-27T10:00:00.000Z&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="na"&gt;lastUserMessageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;msg_123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stateVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allows a reply one millisecond before expiry&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="o"&gt;=&amp;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;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluateWhatsAppSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-07-28T09:59:59.999Z&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks a reply at the exact expiry time&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="o"&gt;=&amp;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;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluateWhatsAppSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-07-28T10:00:00.000Z&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;Also test these scenarios:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Expected result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reply at &lt;code&gt;23:59:59&lt;/code&gt; after the user message&lt;/td&gt;
&lt;td&gt;Non-template reply allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reply at exactly 24 hours&lt;/td&gt;
&lt;td&gt;Window closed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New user message before expiry&lt;/td&gt;
&lt;td&gt;Expiry resets from the new message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reply approved before expiry but claimed afterward&lt;/td&gt;
&lt;td&gt;Non-template transport blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate inbound webhook&lt;/td&gt;
&lt;td&gt;State remains unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Older event arrives after a newer event&lt;/td&gt;
&lt;td&gt;Expiry does not move backward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two workers claim the same reply&lt;/td&gt;
&lt;td&gt;Only one worker commits the claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job is delayed after being marked ready&lt;/td&gt;
&lt;td&gt;Transport checks the deadline again&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Keep an audit trail
&lt;/h2&gt;

&lt;p&gt;For every send decision, record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SendAuditRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;replyJobId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;evaluatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;evaluatedStateVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;serviceWindowExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;freeEntryExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;selectedPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;non_template_service&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved_template&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;human_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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 makes it possible to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which user message opened the window?&lt;/li&gt;
&lt;li&gt;Which state version did the worker evaluate?&lt;/li&gt;
&lt;li&gt;Was the job already expired when it was claimed?&lt;/li&gt;
&lt;li&gt;Did another worker process the same reply?&lt;/li&gt;
&lt;li&gt;Was the message sent as free-form text or through a template?&lt;/li&gt;
&lt;li&gt;Why was the job routed to human review?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementation checklist
&lt;/h2&gt;

&lt;p&gt;Before release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Verify webhook signatures before changing state&lt;/li&gt;
&lt;li&gt;[ ] Deduplicate inbound user messages&lt;/li&gt;
&lt;li&gt;[ ] Ignore receipts, echoes, and internal events&lt;/li&gt;
&lt;li&gt;[ ] Normalize provider timestamps to UTC&lt;/li&gt;
&lt;li&gt;[ ] Store explicit expiry timestamps&lt;/li&gt;
&lt;li&gt;[ ] Keep the 24-hour and 72-hour clocks separate&lt;/li&gt;
&lt;li&gt;[ ] Increment a monotonic &lt;code&gt;stateVersion&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Re-evaluate state when claiming a queued reply&lt;/li&gt;
&lt;li&gt;[ ] Check the deadline again immediately before transport&lt;/li&gt;
&lt;li&gt;[ ] Treat the exact expiry timestamp as closed&lt;/li&gt;
&lt;li&gt;[ ] Prevent concurrent workers from claiming one reply&lt;/li&gt;
&lt;li&gt;[ ] Route expired drafts instead of rewriting them&lt;/li&gt;
&lt;li&gt;[ ] Keep permission and pricing calculations separate&lt;/li&gt;
&lt;li&gt;[ ] Store a complete send-decision audit record&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;A WhatsApp reply is not authorized forever just because it was valid when an agent drafted it.&lt;/p&gt;

&lt;p&gt;The reliable boundary is the moment the worker attempts transport.&lt;/p&gt;

&lt;p&gt;A safe implementation should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update the window only from verified, deduplicated user messages.&lt;/li&gt;
&lt;li&gt;Store explicit timestamps and a monotonic state version.&lt;/li&gt;
&lt;li&gt;Re-evaluate the state when a queued reply is claimed.&lt;/li&gt;
&lt;li&gt;Check the deadline again before the external API request.&lt;/li&gt;
&lt;li&gt;Block expired free-form replies.&lt;/li&gt;
&lt;li&gt;Route them to an approved-template decision or human review.&lt;/li&gt;
&lt;li&gt;Keep charging calculations outside the permission state machine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The UI can explain the window, but the server-side sender must enforce it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official reference
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://whatsappbusiness.com/products/platform-pricing/" rel="noopener noreferrer"&gt;WhatsApp Business Platform pricing and customer service window&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.unifyport.ai/blog/whatsapp-customer-service-window-state-machine-october-2026/" rel="noopener noreferrer"&gt;UnifyPort&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was prepared with AI assistance for language and structure, then technically reviewed and verified by the author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>whatsapp</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
    <item>
      <title>Third-Party AI Assistants on WhatsApp in the EU: What Changed in 2026</title>
      <dc:creator>unifyport</dc:creator>
      <pubDate>Thu, 23 Jul 2026 05:55:23 +0000</pubDate>
      <link>https://dev.to/unifyport/third-party-ai-assistants-on-whatsapp-in-the-eu-what-changed-in-2026-3gec</link>
      <guid>https://dev.to/unifyport/third-party-ai-assistants-on-whatsapp-in-the-eu-what-changed-in-2026-3gec</guid>
      <description>&lt;p&gt;On June 9, 2026, the European Commission ordered Meta to restore free WhatsApp access for competing general-purpose AI assistants while its antitrust investigation continues.&lt;/p&gt;

&lt;p&gt;That sounds simple, but the headline is easy to overread.&lt;/p&gt;

&lt;p&gt;The measure does not mean that every third-party assistant is automatically available on every WhatsApp account. It is regional, temporary, and focused on a specific distribution path: competing general-purpose AI assistants using the WhatsApp Business API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;Can third-party AI assistants use WhatsApp in Europe again?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yes, under the interim conditions ordered by the European Commission—but actual availability still depends on the provider, region, onboarding status, and account eligibility.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The order requires Meta to restore the access conditions that applied before October 15, 2025, including free access, and to maintain them until the Commission reaches a final decision.&lt;/p&gt;

&lt;p&gt;It does not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create Business API credentials for a provider&lt;/li&gt;
&lt;li&gt;Complete Meta app review&lt;/li&gt;
&lt;li&gt;Approve individual WhatsApp accounts&lt;/li&gt;
&lt;li&gt;Guarantee worldwide access&lt;/li&gt;
&lt;li&gt;Apply automatically to every type of business chatbot&lt;/li&gt;
&lt;li&gt;Represent a final finding that Meta violated competition law&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The timeline
&lt;/h2&gt;

&lt;p&gt;The distinction between policy, access, fees, and interim enforcement becomes clearer when viewed as a timeline.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Practical meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;October 15, 2025&lt;/td&gt;
&lt;td&gt;Meta adopted a policy restricting competing general-purpose AI assistants from using the WhatsApp Business API&lt;/td&gt;
&lt;td&gt;Rival assistants lost the previous distribution path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;December 2025&lt;/td&gt;
&lt;td&gt;The European Commission opened an antitrust investigation&lt;/td&gt;
&lt;td&gt;Meta's policy entered formal competition review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 4, 2026&lt;/td&gt;
&lt;td&gt;Meta readmitted third-party general-purpose assistants but introduced a fee&lt;/td&gt;
&lt;td&gt;Access returned, but the Commission preliminarily considered the fee equivalent to the earlier restriction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;June 9, 2026&lt;/td&gt;
&lt;td&gt;The Commission imposed interim measures&lt;/td&gt;
&lt;td&gt;Meta was ordered to restore the previous free-access conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Five working days later&lt;/td&gt;
&lt;td&gt;Compliance deadline specified by the Commission&lt;/td&gt;
&lt;td&gt;Meta was required to implement the temporary remedy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final decision&lt;/td&gt;
&lt;td&gt;Still pending&lt;/td&gt;
&lt;td&gt;The underlying antitrust investigation remains open&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An interim measure protects competition while an investigation continues. It changes what Meta must do now, but it does not predetermine the final legal outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which type of “WhatsApp AI” are we discussing?
&lt;/h2&gt;

&lt;p&gt;The phrase “WhatsApp AI” can describe several unrelated products and architectures.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product or integration&lt;/th&gt;
&lt;th&gt;Covered directly by the EU measure?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A competing general-purpose AI assistant distributed through the WhatsApp Business API&lt;/td&gt;
&lt;td&gt;Yes—this is the core subject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A business-specific support bot answering questions about one company&lt;/td&gt;
&lt;td&gt;Not necessarily; its terms and permissions may be different&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meta AI&lt;/td&gt;
&lt;td&gt;No; this is Meta's own assistant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meta Business Agent&lt;/td&gt;
&lt;td&gt;No; this is another Meta product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An internal workflow that sends inbound customer messages to an AI model&lt;/td&gt;
&lt;td&gt;This is a separate integration decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A third-party assistant outside the European market&lt;/td&gt;
&lt;td&gt;No automatic conclusion should be drawn&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This distinction matters because the legal measure concerns access to a distribution channel. It does not define the technical availability of every chatbot, account, or region.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why might an assistant still be unavailable?
&lt;/h2&gt;

&lt;p&gt;Even after the interim measure, a third-party assistant may still be unavailable for several reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The provider has not completed restored Business API onboarding.&lt;/li&gt;
&lt;li&gt;The provider has not reopened registration in the target region.&lt;/li&gt;
&lt;li&gt;The user or business account is outside the relevant rollout.&lt;/li&gt;
&lt;li&gt;The account does not meet Meta's current eligibility requirements.&lt;/li&gt;
&lt;li&gt;The integration belongs to a different product category.&lt;/li&gt;
&lt;li&gt;App review, business verification, or permissions are incomplete.&lt;/li&gt;
&lt;li&gt;The provider has restored its public assistant but not the related webhook or operational controls.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A legal order does not automatically create credentials, deploy code, configure webhooks, or approve an account.&lt;/p&gt;

&lt;h2&gt;
  
  
  A verification checklist for engineering teams
&lt;/h2&gt;

&lt;p&gt;Before changing a production integration, verify each layer independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Confirm the region
&lt;/h3&gt;

&lt;p&gt;Record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where the AI provider is established&lt;/li&gt;
&lt;li&gt;Where the service is being offered&lt;/li&gt;
&lt;li&gt;Where the WhatsApp user or business account is located&lt;/li&gt;
&lt;li&gt;Whether the provider describes the rollout as EU, EEA, or global&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not treat a European competition remedy as a global product announcement.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Name the product precisely
&lt;/h3&gt;

&lt;p&gt;Determine whether the integration is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A general-purpose AI assistant&lt;/li&gt;
&lt;li&gt;A business-specific customer-support agent&lt;/li&gt;
&lt;li&gt;Meta AI&lt;/li&gt;
&lt;li&gt;Meta Business Agent&lt;/li&gt;
&lt;li&gt;A Cloud API application&lt;/li&gt;
&lt;li&gt;A BSP-managed integration&lt;/li&gt;
&lt;li&gt;An internal message-processing workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These products do not necessarily use the same access or approval path.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Ask the provider for current operational status
&lt;/h3&gt;

&lt;p&gt;Confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether Business API access has been restored&lt;/li&gt;
&lt;li&gt;Whether new onboarding is open&lt;/li&gt;
&lt;li&gt;Which regions are active&lt;/li&gt;
&lt;li&gt;Which accounts are eligible&lt;/li&gt;
&lt;li&gt;Whether pricing has changed&lt;/li&gt;
&lt;li&gt;Whether the provider has published a migration notice&lt;/li&gt;
&lt;li&gt;Whether existing users need to reconnect&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Commission's decision is authoritative for the competition measure. The provider remains the authoritative source for its own rollout.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Retest the complete workflow
&lt;/h3&gt;

&lt;p&gt;Do not stop after confirming that an assistant profile is visible.&lt;/p&gt;

&lt;p&gt;Test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User consent&lt;/li&gt;
&lt;li&gt;Incoming messages&lt;/li&gt;
&lt;li&gt;Webhook delivery&lt;/li&gt;
&lt;li&gt;Conversation state&lt;/li&gt;
&lt;li&gt;Model invocation&lt;/li&gt;
&lt;li&gt;Human handoff&lt;/li&gt;
&lt;li&gt;Reply delivery&lt;/li&gt;
&lt;li&gt;Logging and retention&lt;/li&gt;
&lt;li&gt;Account disconnect and recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A working profile page does not prove that the operational integration is healthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the architecture reversible
&lt;/h2&gt;

&lt;p&gt;Platform policies, regional rollouts, fees, and account rules can change independently.&lt;/p&gt;

&lt;p&gt;A more resilient architecture separates the major stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WhatsApp message
       |
       v
Message intake
       |
       v
Consent and policy checks
       |
       v
Conversation storage
       |
       +------------------+
       |                  |
       v                  v
AI model              Human queue
       |                  |
       +--------+---------+
                |
                v
          Reply delivery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation provides several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The message history does not belong exclusively to one model.&lt;/li&gt;
&lt;li&gt;A human agent can take over without losing context.&lt;/li&gt;
&lt;li&gt;The AI provider can be replaced without rebuilding message intake.&lt;/li&gt;
&lt;li&gt;Regional policies can be applied before model invocation.&lt;/li&gt;
&lt;li&gt;Reply delivery can be disabled independently during an incident.&lt;/li&gt;
&lt;li&gt;Audit logs can distinguish automated and human responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The legal right of a provider to access WhatsApp and the technical design of a company's support workflow are related, but they are not the same decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common interpretation mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  “The WhatsApp AI ban has been permanently cancelled”
&lt;/h3&gt;

&lt;p&gt;Not confirmed.&lt;/p&gt;

&lt;p&gt;The Commission imposed interim measures while the investigation continues. A final decision has not yet been reached.&lt;/p&gt;

&lt;h3&gt;
  
  
  “Every third-party assistant should now work”
&lt;/h3&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;The provider still needs operational access, regional rollout, appropriate credentials, and eligible accounts.&lt;/p&gt;

&lt;h3&gt;
  
  
  “The measure applies worldwide”
&lt;/h3&gt;

&lt;p&gt;Do not assume that.&lt;/p&gt;

&lt;p&gt;The decision addresses competition in the European market and should not be treated as a global availability guarantee.&lt;/p&gt;

&lt;h3&gt;
  
  
  “Meta AI availability proves that competitors are available”
&lt;/h3&gt;

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

&lt;p&gt;Meta AI is a Meta product. Its rollout does not demonstrate that a competing provider has completed Business API onboarding.&lt;/p&gt;

&lt;h3&gt;
  
  
  “A customer-support chatbot is automatically covered”
&lt;/h3&gt;

&lt;p&gt;Not always.&lt;/p&gt;

&lt;p&gt;The measure focuses on competing general-purpose AI assistants. A business-specific support agent may be governed by different product terms and permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What teams should document
&lt;/h2&gt;

&lt;p&gt;For every affected integration, record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provider name&lt;/li&gt;
&lt;li&gt;Product category&lt;/li&gt;
&lt;li&gt;WhatsApp access method&lt;/li&gt;
&lt;li&gt;Target region&lt;/li&gt;
&lt;li&gt;Business verification state&lt;/li&gt;
&lt;li&gt;App-review state&lt;/li&gt;
&lt;li&gt;Required permissions&lt;/li&gt;
&lt;li&gt;Webhook configuration&lt;/li&gt;
&lt;li&gt;Human handoff behavior&lt;/li&gt;
&lt;li&gt;Current pricing terms&lt;/li&gt;
&lt;li&gt;Provider confirmation date&lt;/li&gt;
&lt;li&gt;Last successful end-to-end test&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a verifiable operational record instead of relying on a news headline or assumptions about account eligibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;The 2026 EU interim measures are important, but their scope is narrower than “third-party WhatsApp AI is available again.”&lt;/p&gt;

&lt;p&gt;The accurate interpretation is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The European Commission ordered Meta to restore free WhatsApp Business API access for competing general-purpose AI assistants.&lt;/li&gt;
&lt;li&gt;The restored conditions apply while the antitrust investigation continues.&lt;/li&gt;
&lt;li&gt;The measure is not a final judgment.&lt;/li&gt;
&lt;li&gt;Actual availability still depends on provider rollout, onboarding, region, and account eligibility.&lt;/li&gt;
&lt;li&gt;Engineering teams should retest the entire message workflow before changing production routing.&lt;/li&gt;
&lt;li&gt;Message intake, AI reasoning, human handoff, and reply delivery should remain separable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Treat the Commission's announcement as a policy and access change—not as proof that a particular assistant or account is production-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ec.europa.eu/commission/presscorner/detail/en/ip_26_1276" rel="noopener noreferrer"&gt;European Commission: interim measures on Meta and rival AI assistants on WhatsApp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://competition-policy.ec.europa.eu/antitrust-and-cartels/latest-news_en" rel="noopener noreferrer"&gt;European Commission competition policy news&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.unifyport.ai/blog/whatsapp-third-party-ai-eu-interim-measures-2026/" rel="noopener noreferrer"&gt;UnifyPort&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article provides a technical and product interpretation, not legal advice. It was prepared with AI assistance for language and structure, then reviewed and fact-checked against the cited official sources.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>whatsapp</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Recover Missed LINE MINI App Purchase Webhooks with a 7-Day Reconciliation Job</title>
      <dc:creator>unifyport</dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:02:38 +0000</pubDate>
      <link>https://dev.to/unifyport/recover-missed-line-mini-app-purchase-webhooks-with-a-7-day-reconciliation-job-4k7c</link>
      <guid>https://dev.to/unifyport/recover-missed-line-mini-app-purchase-webhooks-with-a-7-day-reconciliation-job-4k7c</guid>
      <description>&lt;p&gt;A successful LINE MINI App purchase reservation does not mean that the customer completed the payment.&lt;/p&gt;

&lt;p&gt;The final source of truth is the &lt;code&gt;purchaseComplete&lt;/code&gt; webhook. But what happens when your endpoint is unavailable, a deployment breaks webhook processing, or the event reaches your server but the business transaction fails?&lt;/p&gt;

&lt;p&gt;LINE provides an event history API that can recover purchase webhooks from the previous seven days. The recovery process still needs careful pagination, reconciliation, and idempotency to avoid granting the same item twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reservation success and purchase completion are different events
&lt;/h2&gt;

&lt;p&gt;LINE MINI App in-app purchases use a multi-step flow.&lt;/p&gt;

&lt;p&gt;Your server first reserves the purchase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST https://api.line.me/iap/v1/product/reserve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LINE returns an &lt;code&gt;orderId&lt;/code&gt;, but the customer can still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Close the MINI App&lt;/li&gt;
&lt;li&gt;Cancel the app-store payment&lt;/li&gt;
&lt;li&gt;Lose network connectivity&lt;/li&gt;
&lt;li&gt;Fail to finish the payment flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The digital item or entitlement should therefore be granted only after processing a &lt;code&gt;purchaseComplete&lt;/code&gt; event.&lt;/p&gt;

&lt;p&gt;These are four separate questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Evidence to trust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Did the reservation succeed?&lt;/td&gt;
&lt;td&gt;Reserve response, saved &lt;code&gt;orderId&lt;/code&gt;, and &lt;code&gt;x-line-request-id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did the purchase complete?&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;purchaseComplete&lt;/code&gt; event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did the live webhook reach your endpoint?&lt;/td&gt;
&lt;td&gt;Raw request log and webhook-processing record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Was the entitlement granted exactly once?&lt;/td&gt;
&lt;td&gt;An idempotency record keyed by &lt;code&gt;orderId&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Store recovery data when reserving the purchase
&lt;/h2&gt;

&lt;p&gt;A recovery job cannot reconcile an order if the reservation was never recorded.&lt;/p&gt;

&lt;p&gt;Store at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your internal checkout ID&lt;/li&gt;
&lt;li&gt;LINE's &lt;code&gt;orderId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;x-line-request-id&lt;/code&gt; response header&lt;/li&gt;
&lt;li&gt;Reservation timestamp&lt;/li&gt;
&lt;li&gt;Expected product or entitlement&lt;/li&gt;
&lt;li&gt;Current purchase state&lt;/li&gt;
&lt;li&gt;Whether a &lt;code&gt;purchaseComplete&lt;/code&gt; event has already been applied&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alert when a reservation remains unresolved beyond the expected checkout duration.&lt;/p&gt;

&lt;p&gt;Do not mark it as paid automatically, and do not wait until the seventh day to investigate. The event history API only covers the preceding seven days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query a fixed recovery window
&lt;/h2&gt;

&lt;p&gt;Use the official event history endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--get&lt;/span&gt; &lt;span class="s2"&gt;"https://api.line.me/iap/v1/webhook/events"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LINE_CHANNEL_ACCESS_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"startEpochSeconds=1784678400"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"endEpochSeconds=1784700000"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"pageSize=100"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"status=FAILED"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timestamps above are example values for a window on July 22, 2026. Generate UTC epoch seconds from the actual incident start and end times.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;status&lt;/code&gt; filter describes webhook delivery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FAILED&lt;/code&gt;: LINE could not successfully deliver the webhook&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SUCCESS&lt;/code&gt;: LINE successfully delivered it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not describe whether the customer's payment succeeded or failed.&lt;/p&gt;

&lt;p&gt;When the problem occurred after your endpoint accepted the request, querying only &lt;code&gt;status=FAILED&lt;/code&gt; may not be sufficient. In that case, omit the filter and reconcile every event within the incident window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep pagination parameters stable
&lt;/h2&gt;

&lt;p&gt;The history endpoint returns at most 100 records per page and may include a &lt;code&gt;nextCursor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For every subsequent page, keep these values unchanged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;startEpochSeconds&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;endEpochSeconds&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pageSize&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the &lt;code&gt;cursor&lt;/code&gt; should change.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.line.me/iap/v1/webhook/events&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fixedQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;startEpochSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1784678400&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;endEpochSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1784700000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;100&lt;/span&gt;&lt;span class="dl"&gt;"&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="s2"&gt;FAILED&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;do&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;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fixedQuery&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;cursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cursor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cursor&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&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;query&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="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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LINE_CHANNEL_ACCESS_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="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="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;`LINE event history request failed: &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="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;page&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &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;record&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&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;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;purchaseComplete&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;await&lt;/span&gt; &lt;span class="nf"&gt;applyPurchaseOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&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="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextCursor&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Changing the time range or filters during pagination can create overlaps or gaps in the recovery result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the same handler for live and recovered events
&lt;/h2&gt;

&lt;p&gt;Do not create a separate entitlement implementation exclusively for recovered events.&lt;/p&gt;

&lt;p&gt;Both paths should call the same business handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Live webhook ────────┐
                     ├── applyPurchaseOnce(orderId, event)
Event history API ───┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler should atomically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Insert an idempotency record keyed by &lt;code&gt;orderId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Stop if that key already exists&lt;/li&gt;
&lt;li&gt;Validate the expected product and order state&lt;/li&gt;
&lt;li&gt;Grant the entitlement&lt;/li&gt;
&lt;li&gt;Mark the purchase as completed&lt;/li&gt;
&lt;li&gt;Commit everything in one transaction&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the live webhook was already applied, the recovered event should become a no-op.&lt;/p&gt;

&lt;p&gt;Conceptually:&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;applyPurchaseOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&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;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inserted&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;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertIdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&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;inserted&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="s2"&gt;already_applied&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;orderId&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;await&lt;/span&gt; &lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantEntitlement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productId&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;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markPurchaseCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&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="s2"&gt;applied&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;orderId&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact database API will differ, but the idempotency record and entitlement update must share the same transaction boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile four sources of evidence
&lt;/h2&gt;

&lt;p&gt;For a fixed incident window, compare:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reserved &lt;code&gt;orderId&lt;/code&gt; values that remain unresolved&lt;/li&gt;
&lt;li&gt;Live webhook request and processing logs&lt;/li&gt;
&lt;li&gt;Event history records returned by LINE&lt;/li&gt;
&lt;li&gt;Idempotency and entitlement records in your database&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Classify every reservation as one of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Completed and applied&lt;/li&gt;
&lt;li&gt;Completed but already applied&lt;/li&gt;
&lt;li&gt;Incomplete or canceled&lt;/li&gt;
&lt;li&gt;Missing and requiring manual investigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Record the following information in the incident report:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exact UTC start and end timestamps&lt;/li&gt;
&lt;li&gt;Applied filters&lt;/li&gt;
&lt;li&gt;Number of pages processed&lt;/li&gt;
&lt;li&gt;Number of events examined&lt;/li&gt;
&lt;li&gt;Recovered &lt;code&gt;orderId&lt;/code&gt; values&lt;/li&gt;
&lt;li&gt;Duplicate events skipped&lt;/li&gt;
&lt;li&gt;Last successful recovery time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Continue verifying live webhook signatures
&lt;/h2&gt;

&lt;p&gt;The history API uses a channel access token. It does not recreate the original HTTP webhook request and should not be expected to contain its original signature header.&lt;/p&gt;

&lt;p&gt;For live deliveries, continue verifying LINE's &lt;code&gt;x-line-signature&lt;/code&gt; against the raw request body using the channel secret.&lt;/p&gt;

&lt;p&gt;Do not parse and serialize the JSON again before verification. Signature validation must use the exact raw bytes received by the endpoint.&lt;/p&gt;

&lt;p&gt;Recovered history events should be marked with a source such as:&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;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"line_event_history"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&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-order-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;This makes audit logs distinguishable without introducing a second entitlement path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important limitations
&lt;/h2&gt;

&lt;p&gt;The event history API is a recovery source, not a replacement for live webhook monitoring.&lt;/p&gt;

&lt;p&gt;Keep these boundaries in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The lookback period is seven days, not a permanent ledger&lt;/li&gt;
&lt;li&gt;Each page returns at most 100 records&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status=FAILED&lt;/code&gt; reports delivery failure, not purchase failure&lt;/li&gt;
&lt;li&gt;A successfully delivered webhook may still fail inside your application&lt;/li&gt;
&lt;li&gt;Recovered events may duplicate events already processed live&lt;/li&gt;
&lt;li&gt;Refund history may follow a different API lifecycle&lt;/li&gt;
&lt;li&gt;Your own reservation and entitlement records must be retained longer than seven days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Schedule reconciliation frequently enough that a weekend outage cannot age out of the recovery window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovery checklist
&lt;/h2&gt;

&lt;p&gt;Before running the job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Confirm the exact UTC incident window&lt;/li&gt;
&lt;li&gt;[ ] Save the original query parameters&lt;/li&gt;
&lt;li&gt;[ ] Confirm the channel access token is available server-side&lt;/li&gt;
&lt;li&gt;[ ] Back up or snapshot the unresolved reservation list&lt;/li&gt;
&lt;li&gt;[ ] Verify that &lt;code&gt;applyPurchaseOnce&lt;/code&gt; is idempotent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While processing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Keep filters unchanged across pages&lt;/li&gt;
&lt;li&gt;[ ] Change only &lt;code&gt;cursor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Process only expected event types&lt;/li&gt;
&lt;li&gt;[ ] Deduplicate by &lt;code&gt;orderId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Record recovered and skipped orders&lt;/li&gt;
&lt;li&gt;[ ] Stop on authentication or schema errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After processing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Reconcile every reservation in scope&lt;/li&gt;
&lt;li&gt;[ ] Confirm entitlements were granted exactly once&lt;/li&gt;
&lt;li&gt;[ ] Record the last successful recovery checkpoint&lt;/li&gt;
&lt;li&gt;[ ] Investigate records that remain unclassified&lt;/li&gt;
&lt;li&gt;[ ] Confirm live webhook monitoring is healthy again&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;A successful purchase reservation is not proof of payment, and a missed live webhook does not need to become a permanent lost order.&lt;/p&gt;

&lt;p&gt;A reliable recovery workflow should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Query LINE's event history before the seven-day boundary&lt;/li&gt;
&lt;li&gt;Keep the incident window fixed during pagination&lt;/li&gt;
&lt;li&gt;Replay &lt;code&gt;purchaseComplete&lt;/code&gt; events through the live business handler&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;orderId&lt;/code&gt; as the idempotency key&lt;/li&gt;
&lt;li&gt;Reconcile reservations, deliveries, and entitlements before closing the incident&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The history endpoint helps recover the event. Your own durable records and idempotent transaction determine whether recovery is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/reference/line-mini-app/" rel="noopener noreferrer"&gt;LINE MINI App API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/line-mini-app/in-app-purchase/implement-in-app-purchase/" rel="noopener noreferrer"&gt;Implement LINE MINI App in-app purchases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/line-mini-app/in-app-purchase/iap-guidelines/" rel="noopener noreferrer"&gt;LINE MINI App in-app purchase development guidelines&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.unifyport.ai/blog/line-mini-app-purchase-webhook-recovery-runbook/" rel="noopener noreferrer"&gt;UnifyPort&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was prepared with AI assistance for language and structure, then technically reviewed and verified by the author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How LINE MINI App Service Notification Tokens Work (and Why You Must Rotate Them)</title>
      <dc:creator>unifyport</dc:creator>
      <pubDate>Tue, 21 Jul 2026 08:36:35 +0000</pubDate>
      <link>https://dev.to/unifyport/how-line-mini-app-service-notification-tokens-work-and-why-you-must-rotate-them-2cgg</link>
      <guid>https://dev.to/unifyport/how-line-mini-app-service-notification-tokens-work-and-why-you-must-rotate-them-2cgg</guid>
      <description>&lt;p&gt;LINE MINI App service messages are not ordinary push notifications.&lt;/p&gt;

&lt;p&gt;They use a user-bound, action-specific notification token that changes after every successful send. If your application keeps using the original token, later reminders can fail even when the template and channel credentials are correct.&lt;/p&gt;

&lt;p&gt;This guide explains the complete flow: issuing the token, sending an approved template, saving the renewed token, and handling retries safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before implementing the API flow, make sure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your LINE MINI App is verified for production use.&lt;/li&gt;
&lt;li&gt;The service-message template has been approved.&lt;/li&gt;
&lt;li&gt;The message confirms or responds to an action completed by the user.&lt;/li&gt;
&lt;li&gt;The channel access token is stored only on your server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unverified MINI Apps can test service messages in a Developing channel, but they cannot send production service messages from a Published channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the three tokens
&lt;/h2&gt;

&lt;p&gt;Three different credentials are involved in the flow.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Credential&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Important lifecycle rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LIFF access token&lt;/td&gt;
&lt;td&gt;Identifies the current LINE user&lt;/td&gt;
&lt;td&gt;Exchange it shortly after the user completes the action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel access token&lt;/td&gt;
&lt;td&gt;Authorizes server-side LINE API calls&lt;/td&gt;
&lt;td&gt;Never expose it to the browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service notification token&lt;/td&gt;
&lt;td&gt;Authorizes notifications for one user action&lt;/td&gt;
&lt;td&gt;Replace it after every successful send&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A LIFF access token can be obtained in the MINI App with:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;liffAccessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;liff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send this value to your backend over HTTPS after the reservation, purchase, check-in, or other approved action succeeds.&lt;/p&gt;

&lt;p&gt;Do not write the LIFF access token or service notification token to application logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Issue a service notification token
&lt;/h2&gt;

&lt;p&gt;Your backend exchanges the LIFF access token for a service notification token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.line.me/message/v3/notifier/token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LINE_CHANNEL_ACCESS_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;liffAccessToken&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LIFF_ACCESS_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful response looks 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;"notificationToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"34c11a03-b726-49e3-8ce0-949387a9f531"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiresIn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;31536000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"remainingCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xD06R2407210008"&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;Store the following values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;notificationToken&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;expiresIn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;remainingCount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sessionId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Your internal order, reservation, or action ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The token is bound to one user and one action session. It is not a reusable user identifier.&lt;/p&gt;

&lt;p&gt;One LIFF access token can issue only one service notification token, so perform the exchange once and persist the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Send an approved template
&lt;/h2&gt;

&lt;p&gt;Use the service notification token with the official send endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.line.me/message/v3/notifier/send?target=service"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LINE_CHANNEL_ACCESS_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "templateName": "thankyou_msg_en",
    "params": {
      "date": "2026-07-21",
      "username": "Brown &amp;amp; Cony"
    },
    "notificationToken": "34c11a03-b726-49e3-8ce0-949387a9f531"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details are easy to miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;target=service&lt;/code&gt; is required.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;templateName&lt;/code&gt; must exactly match an approved template.&lt;/li&gt;
&lt;li&gt;The keys inside &lt;code&gt;params&lt;/code&gt; must match the variables defined by that template.&lt;/li&gt;
&lt;li&gt;If the template has no variables, &lt;code&gt;params&lt;/code&gt; is still required and should be &lt;code&gt;{}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Save the renewed token
&lt;/h2&gt;

&lt;p&gt;This is the most important implementation rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After every successful send, replace the stored notification token with the new token returned by LINE.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The response contains an updated token and counters:&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;"notificationToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"renewed-token-value"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiresIn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;31536000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"remainingCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xD06R2407210008"&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;Do not schedule another reminder until the renewed token has been saved successfully.&lt;/p&gt;

&lt;p&gt;A safe sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lock or version the action record.&lt;/li&gt;
&lt;li&gt;Send the approved service-message template.&lt;/li&gt;
&lt;li&gt;Receive the successful response.&lt;/li&gt;
&lt;li&gt;Replace the stored notification token.&lt;/li&gt;
&lt;li&gt;Update &lt;code&gt;remainingCount&lt;/code&gt; and &lt;code&gt;expiresIn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Commit the database transaction.&lt;/li&gt;
&lt;li&gt;Schedule the next reminder.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This prevents two workers from sending with the same token concurrently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example storage model
&lt;/h2&gt;

&lt;p&gt;A minimal action record could 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;"actionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order_12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xD06R2407210008"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notificationToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"encrypted-token-value"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"remainingCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiresAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2027-07-21T10:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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;Encrypt the notification token at rest and use the &lt;code&gt;version&lt;/code&gt; field—or an equivalent database lock—to prevent concurrent updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling errors and retries
&lt;/h2&gt;

&lt;p&gt;Treat notification tokens as rotating credentials, not permanent addresses.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP 400
&lt;/h3&gt;

&lt;p&gt;Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request body format&lt;/li&gt;
&lt;li&gt;Template name&lt;/li&gt;
&lt;li&gt;Template variables&lt;/li&gt;
&lt;li&gt;Recipient state&lt;/li&gt;
&lt;li&gt;Current notification token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not repeatedly retry the same invalid request.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP 401
&lt;/h3&gt;

&lt;p&gt;Refresh or verify the server-side channel credential.&lt;/p&gt;

&lt;p&gt;If the LIFF access token or service notification token is invalid, start a new eligible user-action flow instead of replaying it indefinitely.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP 403
&lt;/h3&gt;

&lt;p&gt;Confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The channel is authorized for the operation&lt;/li&gt;
&lt;li&gt;The MINI App has the required status&lt;/li&gt;
&lt;li&gt;The template exists and is approved&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Concurrent sends
&lt;/h3&gt;

&lt;p&gt;Never allow two workers to spend the same notification token simultaneously. Use a database lock, compare-and-swap update, or version check around the send operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Message limits
&lt;/h2&gt;

&lt;p&gt;A newly issued token is normally valid for up to one year and starts with a limited number of sends.&lt;/p&gt;

&lt;p&gt;The reviewed use case may have a different limit, so your application should always treat the latest &lt;code&gt;remainingCount&lt;/code&gt; returned by LINE as the runtime source of truth.&lt;/p&gt;

&lt;p&gt;Service messages must remain tied to the original user action. They are designed for cases such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reservation confirmations&lt;/li&gt;
&lt;li&gt;Order results&lt;/li&gt;
&lt;li&gt;Shipping updates&lt;/li&gt;
&lt;li&gt;Reminders related to the original action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are not intended for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advertising&lt;/li&gt;
&lt;li&gt;Coupons&lt;/li&gt;
&lt;li&gt;Product promotions&lt;/li&gt;
&lt;li&gt;General announcements&lt;/li&gt;
&lt;li&gt;Free-form customer support conversations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementation checklist
&lt;/h2&gt;

&lt;p&gt;Before releasing the integration, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] The LIFF access token is collected after an eligible user action&lt;/li&gt;
&lt;li&gt;[ ] Token exchange happens on the server&lt;/li&gt;
&lt;li&gt;[ ] Channel credentials are never exposed to the browser&lt;/li&gt;
&lt;li&gt;[ ] Tokens are not written to application logs&lt;/li&gt;
&lt;li&gt;[ ] The exact approved template name is used&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;target=service&lt;/code&gt; is included&lt;/li&gt;
&lt;li&gt;[ ] The renewed notification token is saved after every send&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;remainingCount&lt;/code&gt; is checked before scheduling another message&lt;/li&gt;
&lt;li&gt;[ ] Concurrent sends are prevented&lt;/li&gt;
&lt;li&gt;[ ] Failed requests use bounded, reason-specific retry logic&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The API calls themselves are straightforward. The difficult part is managing the notification token correctly.&lt;/p&gt;

&lt;p&gt;Think of it as a rotating credential associated with one user action:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Obtain a fresh LIFF access token.&lt;/li&gt;
&lt;li&gt;Exchange it on the server.&lt;/li&gt;
&lt;li&gt;Send an approved template.&lt;/li&gt;
&lt;li&gt;Save the renewed notification token.&lt;/li&gt;
&lt;li&gt;Use the latest &lt;code&gt;remainingCount&lt;/code&gt; for future decisions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you get the rotation and concurrency rules right, follow-up service messages become much more predictable.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/line-mini-app/develop/service-messages/" rel="noopener noreferrer"&gt;Sending service messages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/reference/line-mini-app/" rel="noopener noreferrer"&gt;LINE MINI App API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/docs/line-mini-app/service/service-operation/" rel="noopener noreferrer"&gt;Service operation and message limits&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.unifyport.ai/blog/line-mini-app-service-message-notification-token/" rel="noopener noreferrer"&gt;UnifyPort&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was prepared with AI assistance for language and structure, then technically reviewed and verified by the author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Five Things Every Production Webhook Receiver Should Handle</title>
      <dc:creator>unifyport</dc:creator>
      <pubDate>Mon, 20 Jul 2026 07:39:11 +0000</pubDate>
      <link>https://dev.to/unifyport_cool/five-things-every-production-webhook-receiver-should-handle-6jk</link>
      <guid>https://dev.to/unifyport_cool/five-things-every-production-webhook-receiver-should-handle-6jk</guid>
      <description>&lt;p&gt;Webhooks look simple at first.&lt;/p&gt;

&lt;p&gt;Your application exposes an HTTP endpoint, another service sends a JSON payload, and your code processes the event.&lt;/p&gt;

&lt;p&gt;But a webhook receiver that works in a local demo is very different from one that can safely handle production traffic.&lt;/p&gt;

&lt;p&gt;Here are five things every production webhook receiver should handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Verify every webhook request
&lt;/h2&gt;

&lt;p&gt;A public webhook endpoint can receive requests from anyone on the internet.&lt;/p&gt;

&lt;p&gt;Your application should verify that each request really came from the expected service before processing its contents.&lt;/p&gt;

&lt;p&gt;A common approach is HMAC signature verification:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The sender signs the raw request body with a shared secret.&lt;/li&gt;
&lt;li&gt;The signature is included in an HTTP header.&lt;/li&gt;
&lt;li&gt;Your receiver calculates the expected signature.&lt;/li&gt;
&lt;li&gt;The two signatures are compared using a timing-safe comparison.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Always verify the signature before parsing or processing the event.&lt;/p&gt;

&lt;p&gt;It is also important to verify the raw request body. Parsing and serializing JSON again can change the bytes and cause a valid signature check to fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Make event processing idempotent
&lt;/h2&gt;

&lt;p&gt;Webhook providers may deliver the same event more than once.&lt;/p&gt;

&lt;p&gt;This can happen when your server times out, returns an error, or successfully processes an event but fails to return a response quickly enough.&lt;/p&gt;

&lt;p&gt;Every event should have a unique identifier. Store that identifier before performing an action that must happen only once.&lt;/p&gt;

&lt;p&gt;Without idempotency, one customer message could create multiple support tickets, notifications, or automated replies.&lt;/p&gt;

&lt;p&gt;For a small application, event identifiers can be stored in a database table. Larger systems may use Redis or another shared idempotency store.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Respond quickly and process asynchronously
&lt;/h2&gt;

&lt;p&gt;A webhook endpoint should not perform long-running work inside the HTTP request.&lt;/p&gt;

&lt;p&gt;Instead, the receiver should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify the request.&lt;/li&gt;
&lt;li&gt;Validate the event.&lt;/li&gt;
&lt;li&gt;Store it or add it to a queue.&lt;/li&gt;
&lt;li&gt;Return a successful response.&lt;/li&gt;
&lt;li&gt;Process the event asynchronously.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This prevents temporary problems in a CRM, AI service, database, or notification system from blocking webhook delivery.&lt;/p&gt;

&lt;p&gt;It also makes retry behavior easier to control.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Store the original event
&lt;/h2&gt;

&lt;p&gt;Do not treat a Slack notification, CRM record, or application log as the only copy of an inbound event.&lt;/p&gt;

&lt;p&gt;Store the original webhook event before transforming it.&lt;/p&gt;

&lt;p&gt;This gives your team a reliable record for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debugging delivery problems&lt;/li&gt;
&lt;li&gt;Reprocessing failed events&lt;/li&gt;
&lt;li&gt;Investigating customer reports&lt;/li&gt;
&lt;li&gt;Auditing automated actions&lt;/li&gt;
&lt;li&gt;Comparing old and new event schemas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sensitive message contents should only be stored when necessary and should follow your privacy and retention policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Monitor delivery and processing failures
&lt;/h2&gt;

&lt;p&gt;A production webhook system needs more than application logs.&lt;/p&gt;

&lt;p&gt;Useful metrics include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of received events&lt;/li&gt;
&lt;li&gt;Invalid signature attempts&lt;/li&gt;
&lt;li&gt;Duplicate deliveries&lt;/li&gt;
&lt;li&gt;Processing duration&lt;/li&gt;
&lt;li&gt;Queue depth&lt;/li&gt;
&lt;li&gt;Downstream failures&lt;/li&gt;
&lt;li&gt;Events waiting for retry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Logs should include identifiers such as the event ID, delivery ID, provider, account ID, and response status.&lt;/p&gt;

&lt;p&gt;Avoid writing API keys, signing secrets, access tokens, or unnecessary personal message contents to logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple production architecture
&lt;/h2&gt;

&lt;p&gt;A reliable webhook pipeline can remain simple:&lt;/p&gt;

&lt;p&gt;Webhook delivery&lt;br&gt;&lt;br&gt;
→ Signature verification&lt;br&gt;&lt;br&gt;
→ Event validation&lt;br&gt;&lt;br&gt;
→ Persistent storage&lt;br&gt;&lt;br&gt;
→ Processing queue&lt;br&gt;&lt;br&gt;
→ CRM, support system, AI agent, or notification service&lt;/p&gt;

&lt;p&gt;The same structure can be used for payments, source control events, messaging platforms, and many other event-driven integrations.&lt;/p&gt;

&lt;p&gt;The difficult part is rarely receiving the first request. The difficult part is making sure every valid event is processed safely, only once, and remains observable when another system fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying this to messaging platforms
&lt;/h2&gt;

&lt;p&gt;These principles become especially important when receiving customer messages.&lt;/p&gt;

&lt;p&gt;A support team may receive messages from Telegram, WhatsApp, LINE, Zalo, TikTok, and X. Each platform has different authentication methods and capabilities, but the receiving system still needs the same security, idempotency, storage, and monitoring boundaries.&lt;/p&gt;

&lt;p&gt;At UnifyPort, we are building one API and a standard webhook event layer for these messaging platforms.&lt;/p&gt;

&lt;p&gt;You can learn more about the event model and webhook design in the UnifyPort documentation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.unifyport.ai/docs/" rel="noopener noreferrer"&gt;https://www.unifyport.ai/docs/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>backend</category>
      <category>security</category>
    </item>
  </channel>
</rss>
