<?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>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>
