<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Unpoll</title>
    <subtitle>Signed HTTPS webhooks for the WhatsApp Agent API. Managed long polling, durable events, retries, and text replies.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://unpoll.cc/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://unpoll.cc"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-11-02T00:00:00+00:00</updated>
    <id>https://unpoll.cc/atom.xml</id>
    <entry xml:lang="en">
        <title>WhatsApp Agent API Webhooks: Receive Messages Without Running a Polling Loop</title>
        <published>2026-11-02T00:00:00+00:00</published>
        <updated>2026-11-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            Unpoll
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://unpoll.cc/guides/whatsapp-agent-api-webhooks/"/>
        <id>https://unpoll.cc/guides/whatsapp-agent-api-webhooks/</id>
        
        <content type="html" xml:base="https://unpoll.cc/guides/whatsapp-agent-api-webhooks/">&lt;p&gt;You want your WhatsApp agent to receive a message, run some application logic, and send a reply. An HTTP webhook would fit your application perfectly.&lt;/p&gt;
&lt;p&gt;But the &lt;strong&gt;WhatsApp Agent API receives updates through long polling&lt;/strong&gt;, not native webhook subscriptions. Your application has to request updates, track its position, and keep polling—even when nobody is sending messages.&lt;/p&gt;
&lt;p&gt;You can build and operate that infrastructure yourself. Or you can use &lt;strong&gt;Unpoll&lt;/strong&gt; to manage polling and deliver updates to your application as signed HTTPS webhooks.&lt;/p&gt;
&lt;p&gt;This guide explains the difference, how to connect an agent, and what your webhook endpoint needs to handle safely.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Which API is this about?&lt;/strong&gt; This guide covers the &lt;strong&gt;WhatsApp Agent API&lt;/strong&gt;, not the WhatsApp Business Cloud API. Their setup and event-delivery mechanisms are different.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;does-the-whatsapp-agent-api-support-webhooks&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#does-the-whatsapp-agent-api-support-webhooks&quot; aria-label=&quot;Anchor link for: does-the-whatsapp-agent-api-support-webhooks&quot;&gt;Does the WhatsApp Agent API support webhooks?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The WhatsApp Agent API exposes incoming messages and message receipts through:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;http&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#D73A49, #F97583);&quot;&gt;GET&lt;/span&gt;&lt;span&gt; https://api.whatsapp.com/agent/v1/updates&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a long-polling endpoint. A request stays open until updates arrive or its timeout expires.&lt;/p&gt;
&lt;p&gt;When updates arrive, the response includes a &lt;code&gt;next_offset&lt;/code&gt; cursor. Your next request must send that value back as &lt;code&gt;offset&lt;/code&gt;. When the provider returns an empty &lt;code&gt;204&lt;/code&gt; response, you keep the existing offset.&lt;/p&gt;
&lt;p&gt;That sounds like a small loop. The operational work appears when you need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Recover after a process crashes.&lt;/li&gt;
&lt;li&gt;Store updates before advancing the cursor.&lt;/li&gt;
&lt;li&gt;Prevent multiple workers from polling the same agent.&lt;/li&gt;
&lt;li&gt;Handle rate limits and provider failures.&lt;/li&gt;
&lt;li&gt;Deliver events while your application is temporarily unavailable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Webhooks don&#39;t remove the need for polling at the provider. They move that responsibility out of your application.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;how-unpoll-turns-polling-into-webhooks&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#how-unpoll-turns-polling-into-webhooks&quot; aria-label=&quot;Anchor link for: how-unpoll-turns-polling-into-webhooks&quot;&gt;How Unpoll turns polling into webhooks&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;With Unpoll, the incoming message path becomes:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;WhatsApp Agent API&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ↓ long polling&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Unpoll&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ↓ signed HTTPS POST&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Your webhook endpoint&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unpoll manages the polling loop, durably stores eligible new events before advancing the cursor, and schedules webhook delivery separately.&lt;/p&gt;
&lt;p&gt;That separation matters: a slow or failing webhook endpoint does not block ingestion.&lt;/p&gt;
&lt;p&gt;Your application still has responsibilities. It needs to verify requests, accept events durably, handle duplicates, and run your agent&#39;s business logic.&lt;/p&gt;
&lt;h2 id=&quot;connect-your-agent-and-receive-a-webhook&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#connect-your-agent-and-receive-a-webhook&quot; aria-label=&quot;Anchor link for: connect-your-agent-and-receive-a-webhook&quot;&gt;Connect your agent and receive a webhook&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before starting, you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Access to the WhatsApp Agent API and an agent token.&lt;/li&gt;
&lt;li&gt;An Unpoll account.&lt;/li&gt;
&lt;li&gt;A publicly reachable HTTPS endpoint.&lt;/li&gt;
&lt;li&gt;Somewhere to securely store your webhook signing secret.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The provider currently restricts an agent&#39;s outbound messages to its creator.&lt;/strong&gt; Use the creator&#39;s WhatsApp account for the walkthrough; this is not a setup for messaging arbitrary phone numbers.&lt;/p&gt;
&lt;h3 id=&quot;1-prepare-your-https-endpoint&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#1-prepare-your-https-endpoint&quot; aria-label=&quot;Anchor link for: 1-prepare-your-https-endpoint&quot;&gt;1. Prepare your HTTPS endpoint&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Choose an endpoint in your application, for example:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;https://your-server.example/webhooks/whatsapp&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It must accept JSON &lt;code&gt;POST&lt;/code&gt; requests. Unpoll requires a public HTTPS URL on port 443 and does not follow redirects.&lt;/p&gt;
&lt;p&gt;Prepare your handler to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read the bounded, untouched request body.&lt;/li&gt;
&lt;li&gt;Verify the timestamp and signature.&lt;/li&gt;
&lt;li&gt;Parse and validate the event.&lt;/li&gt;
&lt;li&gt;Atomically store the event and schedule any application work.&lt;/li&gt;
&lt;li&gt;Return a successful &lt;code&gt;2xx&lt;/code&gt; response.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Keep AI inference and other slow operations out of the acknowledgement path. Store the event or enqueue it durably first, then process it separately.&lt;/p&gt;
&lt;h3 id=&quot;2-connect-the-agent-in-unpoll&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#2-connect-the-agent-in-unpoll&quot; aria-label=&quot;Anchor link for: 2-connect-the-agent-in-unpoll&quot;&gt;2. Connect the agent in Unpoll&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Open the &lt;a rel=&quot;external&quot; href=&quot;https://app.unpoll.cc/agents/new&quot;&gt;Unpoll dashboard&lt;/a&gt;, enter your WhatsApp Agent API token, and set your webhook URL.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stop any other integration polling the same agent first.&lt;/strong&gt; The provider allows one active poll per agent; a newer poll replaces the previous one.&lt;/p&gt;
&lt;p&gt;When the webhook endpoint is created, Unpoll reveals its signing secret once. Save it in your server&#39;s secret configuration.&lt;/p&gt;
&lt;p&gt;There are three different credentials to keep separate:&lt;/p&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;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;WhatsApp Agent API token&lt;/td&gt;&lt;td&gt;Lets Unpoll communicate with the provider&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Webhook signing secret&lt;/td&gt;&lt;td&gt;Lets your application verify incoming Unpoll requests&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Unpoll agent API key&lt;/td&gt;&lt;td&gt;Lets your application send replies through Unpoll&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;None belongs in browser-side code.&lt;/p&gt;
&lt;h3 id=&quot;3-configure-signature-verification&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#3-configure-signature-verification&quot; aria-label=&quot;Anchor link for: 3-configure-signature-verification&quot;&gt;3. Configure signature verification&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Deploy your endpoint&#39;s signature verification using the signing secret. Then send a &lt;strong&gt;new&lt;/strong&gt; message to your agent from its creator&#39;s WhatsApp account.&lt;/p&gt;
&lt;p&gt;Wait until the endpoint is configured before sending the test message. Events ingested without an enabled webhook endpoint are stored but are not later delivered automatically.&lt;/p&gt;
&lt;p&gt;Unpoll starts with messages timestamped from when you connect the agent. Resuming a paused agent sets a new cutoff: messages timestamped before that resume are skipped rather than replayed. See &lt;a href=&quot;https://unpoll.cc/docs/limitations/&quot;&gt;known limitations&lt;/a&gt; before relying on backlog behavior.&lt;/p&gt;
&lt;h3 id=&quot;4-inspect-the-result&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#4-inspect-the-result&quot; aria-label=&quot;Anchor link for: 4-inspect-the-result&quot;&gt;4. Inspect the result&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Check the event and delivery attempt in the dashboard.&lt;/p&gt;
&lt;p&gt;A successful test should show:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;message.received&lt;/code&gt; event.&lt;/li&gt;
&lt;li&gt;A request arriving at your endpoint.&lt;/li&gt;
&lt;li&gt;Successful signature verification.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;2xx&lt;/code&gt; acknowledgement recorded for the delivery.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-does-the-webhook-payload-look-like&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-does-the-webhook-payload-look-like&quot; aria-label=&quot;Anchor link for: what-does-the-webhook-payload-look-like&quot;&gt;What does the webhook payload look like?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Each webhook contains one incoming message or one message-status update—not the provider&#39;s entire polling response.&lt;/p&gt;
&lt;p&gt;Here is an illustrative text-message event; the identifiers are examples:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;0199f0c6-2c3d-7e4f-8a50-61728394a5b6&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;version&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;2026-10-01&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;message.received&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;agent_id&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;0199f0c4-1b2c-7d3e-8f40-516273849abc&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;created_at&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;2026-10-19T09:10:02.345678Z&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;    &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;user:50972923564215&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;    &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;wamid.HBgONTA5NzI5MjM1NjQyMTUVEgASGBQzQTVGOUMyRTFCN0Q0MDY4QUYzMQA=&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;    &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;timestamp&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;1792401001&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;    &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;    &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;      &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;Hello agent&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The two &lt;code&gt;id&lt;/code&gt; fields serve different purposes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;id&lt;/code&gt;&lt;/strong&gt; identifies the Unpoll event. Use it to deduplicate webhook deliveries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;data.id&lt;/code&gt;&lt;/strong&gt; identifies the WhatsApp message. Use it when quoting a message or marking it as read.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The sender&#39;s &lt;code&gt;data.from&lt;/code&gt; is an opaque participant identifier. Store it and send it back unchanged; do not convert it into a phone number.&lt;/p&gt;
&lt;p&gt;Not every incoming message is text. Check &lt;code&gt;data.type&lt;/code&gt; before reading &lt;code&gt;data.text.body&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;verify-webhook-signatures-before-trusting-the-payload&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#verify-webhook-signatures-before-trusting-the-payload&quot; aria-label=&quot;Anchor link for: verify-webhook-signatures-before-trusting-the-payload&quot;&gt;Verify webhook signatures before trusting the payload&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Unpoll sends authentication headers including:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;X-Unpoll-Timestamp: &amp;lt;Unix timestamp in seconds&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;X-Unpoll-Signature: v1=&amp;lt;lowercase hex HMAC-SHA256&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The signing input is:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;timestamp + &amp;quot;.&amp;quot; + exact raw HTTP body bytes&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To verify a request:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Validate the timestamp and signature format, rejecting missing or duplicate authentication headers.&lt;/li&gt;
&lt;li&gt;Reject timestamps outside your allowed window; five minutes in either direction is the recommended starting point.&lt;/li&gt;
&lt;li&gt;Compute HMAC-SHA256 using the &lt;strong&gt;entire signing secret string&lt;/strong&gt;, including its &lt;code&gt;unpoll_whsec_&lt;/code&gt; prefix, as the UTF-8 key. Do not decode the secret.&lt;/li&gt;
&lt;li&gt;Decode the supplied signature, validate its length, and compare the MAC bytes in constant time.&lt;/li&gt;
&lt;li&gt;Only then parse the JSON and trust its contents.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Do not parse and reserialize JSON before verification. Even a whitespace change can alter the signed bytes. Never log the signing secret or full message body as part of debugging verification failures.&lt;/p&gt;
&lt;p&gt;Deduplicate using the &lt;strong&gt;verified body&#39;s &lt;code&gt;id&lt;/code&gt;&lt;/strong&gt;. The separate event and delivery ID headers are not covered by the signature.&lt;/p&gt;
&lt;p&gt;See the &lt;a href=&quot;https://unpoll.cc/docs/verify/&quot;&gt;signature verification documentation&lt;/a&gt; for language examples and the complete required checks.&lt;/p&gt;
&lt;h2 id=&quot;handle-duplicates-and-endpoint-failures&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#handle-duplicates-and-endpoint-failures&quot; aria-label=&quot;Anchor link for: handle-duplicates-and-endpoint-failures&quot;&gt;Handle duplicates and endpoint failures&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Unpoll webhook delivery is &lt;strong&gt;at least once&lt;/strong&gt;, not exactly once.&lt;/p&gt;
&lt;p&gt;For example, your application might store an event and return &lt;code&gt;204&lt;/code&gt;, but the response could be lost before Unpoll receives it. A retry can deliver the same event again.&lt;/p&gt;
&lt;p&gt;Use a durable uniqueness constraint on the event ID. Recording that ID and scheduling its work should happen atomically. A duplicate that was already accepted should receive a successful acknowledgement without scheduling the same work again.&lt;/p&gt;
&lt;p&gt;Response handling also matters:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Your endpoint&#39;s response&lt;/th&gt;&lt;th&gt;Unpoll&#39;s behavior&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;2xx&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Delivery acknowledged&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;408&lt;/code&gt;, &lt;code&gt;429&lt;/code&gt;, or &lt;code&gt;5xx&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Retried within the configured limits&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Connection error or timeout before response headers&lt;/td&gt;&lt;td&gt;Retried within the configured limits&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Other non-&lt;code&gt;2xx&lt;/code&gt;, including redirects&lt;/td&gt;&lt;td&gt;Delivery fails without automatic retry&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Return success only after accepting the event durably.&lt;/strong&gt; If temporary storage is unavailable, a &lt;code&gt;503&lt;/code&gt; is more appropriate than acknowledging work you will lose.&lt;/p&gt;
&lt;p&gt;Retries are bounded, not a promise to keep trying throughout an outage. The default budget is eight attempts within 24 hours; without &lt;code&gt;Retry-After&lt;/code&gt;, those attempts typically finish within a few minutes. Failed deliveries can be manually redelivered from the dashboard while they remain within retention. See the &lt;a href=&quot;https://unpoll.cc/docs/retries/&quot;&gt;retry documentation&lt;/a&gt; for the current policy.&lt;/p&gt;
&lt;p&gt;Events can also arrive out of order. Don&#39;t treat webhook arrival order as conversation order.&lt;/p&gt;
&lt;h2 id=&quot;send-a-text-reply&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#send-a-text-reply&quot; aria-label=&quot;Anchor link for: send-a-text-reply&quot;&gt;Send a text reply&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To reply, create an API key from the agent&#39;s &lt;strong&gt;API keys&lt;/strong&gt; tab.&lt;/p&gt;
&lt;p&gt;Set &lt;code&gt;AGENT_ID&lt;/code&gt; to the Unpoll agent ID and &lt;code&gt;UNPOLL_API_KEY&lt;/code&gt; to that key in your server environment. Replace the illustrative &lt;code&gt;to&lt;/code&gt; value below with &lt;code&gt;data.from&lt;/code&gt; from your verified incoming event:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #E1E4E8); background-color: light-dark(#FFFFFF, #24292E);&quot; &gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #B392F0);&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;https://api.unpoll.cc/v1/agents/&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;AGENT_ID&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;/messages&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;H&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;Authorization: Bearer &lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;UNPOLL_API_KEY&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;H&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;Content-Type: application/json&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;color: light-dark(#005CC5, #79B8FF);&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt; &amp;#39;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;    &amp;quot;to&amp;quot;: &amp;quot;user:50972923564215&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;    &amp;quot;text&amp;quot;: &amp;quot;Hello! Your message reached my agent.&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;  }&lt;/span&gt;&lt;span style=&quot;color: light-dark(#032F62, #9ECBFF);&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A successful response means the provider &lt;strong&gt;accepted&lt;/strong&gt; the message. Delivery and read receipts arrive separately as &lt;code&gt;message.status&lt;/code&gt; webhooks.&lt;/p&gt;
&lt;p&gt;Do not automatically retry a send whose outcome is unknown: the message may already have been sent, and retrying could duplicate it. Outbound sends and incoming webhook retries are different operations with different safety rules.&lt;/p&gt;
&lt;p&gt;Unpoll supports &lt;a href=&quot;https://unpoll.cc/docs/send/&quot;&gt;text replies&lt;/a&gt;, &lt;a href=&quot;https://unpoll.cc/docs/read/&quot;&gt;read receipts&lt;/a&gt;, and &lt;a href=&quot;https://unpoll.cc/docs/media/&quot;&gt;downloads of received media&lt;/a&gt;. It does not support outbound media sends.&lt;/p&gt;
&lt;h2 id=&quot;when-should-you-manage-polling-yourself&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#when-should-you-manage-polling-yourself&quot; aria-label=&quot;Anchor link for: when-should-you-manage-polling-yourself&quot;&gt;When should you manage polling yourself?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Managing your own poller can make sense if you want full control over ingestion and are prepared to operate persistent workers, cursor storage, recovery, and delivery infrastructure.&lt;/p&gt;
&lt;p&gt;Unpoll is an alternative when you want your application to start at &lt;strong&gt;“an authenticated event arrived”&lt;/strong&gt;, rather than &lt;strong&gt;“keep a polling system running.”&lt;/strong&gt; You still need a webhook endpoint and durable event handling, but you don&#39;t need your own long-running provider poller.&lt;/p&gt;
&lt;p&gt;You own the interesting part: deciding what your agent does with a message.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https://app.unpoll.cc/agents/new&quot;&gt;Connect your agent&lt;/a&gt;&lt;/strong&gt; or follow the &lt;a href=&quot;https://unpoll.cc/docs/quickstart/&quot;&gt;Unpoll quickstart&lt;/a&gt; to receive your first signed webhook.&lt;/p&gt;
</content>
        
    </entry>
</feed>
