{"slug": "how-to-trigger-ai-agents-from-webhooks-signature-verification-retries-and", "title": "How to Trigger AI Agents from Webhooks: Signature Verification, Retries, and Idempotency", "summary": "A developer has published a technical guide on safely triggering AI agents from webhooks, covering HMAC-SHA256 signature verification, replay protection, and idempotency keys for retry deduplication. The writeup warns that webhook-triggered agents turn bad payloads from data-quality bugs into real-world actions, and shows how to capture the raw request body before framework parsing so signature checks don't fail on re-serialized JSON. It also notes per-provider header differences across Slack, GitHub, HubSpot, and Notion, and points to Corsair's plugin catalog as a way to avoid that parsing work.", "body_md": "[Message me in Slack when Dev finally uploads the Q4 financials. That one line is why webhooks matter for AI agents. Without them, an agent can only act when someone asks it to, or when it is polled on a schedule. Webhooks are what let an agent react the moment something actually happens somewhere else.](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fren11bx5teod83bty8wl.png)\n\nThat reactiveness is also what makes webhooks riskier for agent systems than for a typical backend. A webhook that corrupts a database row is a bug. A webhook that triggers an agent to send an email or update a customer record is now a real world action, and a bad one is harder to walk back. Worse, the agent's own actions can generate new webhook events, creating the conditions for the system to react to itself.\n\nThis post covers the three things that have to be right before an agent is allowed to act on an incoming event: verifying the signature actually came from the provider, blocking replayed events, and deduplicating retries with idempotency keys, along with when to fall back to polling. It does not cover the self triggered loop problem in depth. That gets its own post, though it is touched on briefly below.\n\nWhen a webhook only updates a row in a database, a bad payload is a data quality problem. When a webhook can trigger an AI agent to send an email, post to Slack, or update a customer record, a bad payload is an action problem. That difference is why signature verification is not optional for agent triggered workflows the way it might feel optional for a low stakes internal tool.\n\nThe standard approach is HMAC SHA256. The provider and your server share a secret key. The provider computes a hash of the request body, often combined with a timestamp as covered in the next section, using that key, and sends the result in a header. Your server recomputes the same hash and compares the two.\n\nThat comparison has to run through a constant time function, such as `crypto.timingSafeEqual` in Node.js, `hmac.compare_digest` in Python, or `Rack::Utils.secure_compare` in Ruby. A plain `==` check can leak timing information that lets an attacker guess the correct signature byte by byte across repeated requests.\n\nThis is where most implementations break. Frameworks like Express parse the incoming body into a JavaScript object before your handler ever sees it. If you then call `JSON.stringify()` on that object to recompute the hash, you are hashing a different set of bytes than the provider sent, because of whitespace, key ordering, and encoding differences. The signature check fails even though the payload is genuine.\n\nThe fix is to capture the raw body before anything parses it:\n\n``` js\napp.use(express.json({\n  verify: (req, res, buf) => {\n    req.rawBody = buf;\n  }\n}));\n\napp.post('/webhooks', (req, res) => {\n  const isValid = verifyHmacSignature(req.rawBody, req.headers['x-signature']);\n  if (!isValid) return res.status(401).send('Invalid signature');\n  res.status(200).send('OK');\n});\n```\n\nEvery provider also names its header and formats its hash differently, which adds friction once an agent is wired into a dozen apps instead of one:\n\n`X-Slack-Signature` and the timestamp in `X-Slack-Request-Timestamp`\n`X-Hub-Signature-256`, keeping an older `X-Hub-Signature` only for backward compatibility`X-HubSpot-Signature-v3` alongside `X-HubSpot-Request-Timestamp`\n`X-Notion-Signature`\nNone of these details are hard on their own, but they add up fast across a real integration stack. A plugin catalog like [Corsair's](https://docs.corsair.dev/guides/plugins) that ships signature verification built into each integration removes this per provider parsing work entirely.\n\nA valid signature only proves a request once came from the provider. It does not prove the request is being sent for the first time. An attacker who intercepts a genuine, signed payload, perhaps from an insecure network or a compromised log, can resend that exact request later and your server will verify it successfully, because the signature itself is still correct.\n\nThis is a replay attack, and the defense is a timestamp bound directly into the signature itself.\n\nMost providers send a timestamp header, such as `X-Slack-Request-Timestamp` or `X-HubSpot-Request-Timestamp`, alongside the signature. Your server checks that the timestamp falls within a tight freshness window, typically 5 minutes or 300 seconds, and rejects anything older.\n\nChecking the header alone is not enough, since an attacker who controls the request could simply rewrite the timestamp to look fresh. To prevent that, the provider folds the timestamp into the string that gets hashed, not just into a separate header. Slack's format is `v0:` plus the timestamp plus the raw body; HubSpot concatenates the method, URI, raw body, and timestamp. Because the timestamp sits inside the hashed string, changing it invalidates the signature along with it.\n\nFor an agent triggered workflow, this matters more than it would for a typical webhook consumer. A replayed event that updates a timestamp field in a database is low stakes. A replayed event that causes an agent to resend an email or post a duplicate Slack message is a real world consequence a user will actually notice.\n\nWebhook providers guarantee at least once delivery, not exactly once. Network blips, slow responses, and timeouts all cause providers to resend events your server already received. For a typical application this means an occasional duplicate database write. For an agent, it means the same instruction, such as send this email or post this update, arriving twice and being capable of running twice.\n\nRespond with a 2xx status as soon as the signature is verified, before any agent logic runs. Providers enforce short response windows (GitHub expects a response within 10 seconds) and will treat a slow handler as a failed delivery, triggering a retry even though your server is still working on it. The practical pattern is to verify, enqueue the event for background processing, and return 202 Accepted immediately.\n\nA 5xx response or a timeout tells the provider the failure was temporary, so it backs off and tries again later. A 4xx response tells the provider the request itself was invalid, for example a failed signature check, and that retrying the identical payload will not help. Mixing these up causes either silent data loss or retry storms, so the distinction is worth getting right deliberately rather than by accident.\n\nIf a delivery keeps failing past a handful of attempts, most systems apply exponential backoff with jitter to space retries out and avoid hammering a downed endpoint all at once. After a capped number of attempts, typically 5 to 10, the event is moved to a dead letter queue instead of retrying forever, so it can be inspected and replayed manually rather than silently lost.\n\nThe actual deduplication mechanism is an idempotency key: typically the event or delivery ID the provider already includes in the payload. Store that ID under a unique constraint, alongside a fingerprint of the request and its processing status. This is especially important for multi tenant webhooks, where the constraint has to be scoped per tenant as well as per event ID, so two customers that happen to share a similar event ID never collide.\n\nWhen a new event arrives, attempt an atomic insert. If it succeeds, you own the event and process it. If it fails because the key already exists, the event is a duplicate, so you skip the agent action and still return a success status, since an error response would only cause the provider to keep retrying.\n\nKeys do not need to live forever. A 24 hour retention window matches what most providers use for their own retry windows, and a 72 hour window gives a buffer against a bug that ships on a Friday and gets caught the following Monday.\n\nDeduplication answers whether an event has already been seen. It does not answer whether the task that event kicked off actually finished, which is a separate durability problem for longer running agent workflows.\n\nWebhooks are not a complete substitute for polling, and treating them as one leads to quiet gaps in what an agent knows. A few situations call for polling even in a webhook first design.\n\nReconciliation: webhook delivery depends on network transit, and events do get delayed or dropped. Running a periodic reconciliation poll against a provider's history endpoint catches anything a webhook missed.\n\nHistorical backfill: webhook subscriptions only cover events going forward from the moment they are configured. A newly connected account has no event history to react to, so the only way to establish a starting state is to poll a list or history endpoint once at setup.\n\nProviders without webhook support, or with rate capped channels: not every API offers webhooks, and some that do cap how many events they will send per second, silently dropping the rest during a spike. Periodic polling is the fallback for both.\n\nSynchronous reasoning: webhooks are inherently asynchronous. If an agent needs to check current state in the middle of a reasoning step, rather than reacting to a future push, it needs a synchronous read, not a notification that may arrive later.\n\nIn practice, most reliable integrations end up running both: webhooks for fast reaction, and a background poll as the safety net underneath. When deciding which providers in a stack need that fallback and which already push reliable events, it is worth checking [each plugin's webhook coverage](https://docs.corsair.dev/guides/plugins) rather than assuming every integration behaves the same way.\n\nThere is one failure mode specific to agent systems worth a mention here, even though it is not the focus of this post. When an agent posts a Slack message or updates a CRM record, that action itself generates a webhook event from the provider. If that event flows back into the same agent without any filtering, the agent reacts to its own change, and a cascading loop starts.\n\nThe fix at a high level is to filter by actor: check the bot or user ID on the incoming event against the agent's own identity, and drop anything that matches before it reaches the agent's reasoning step. Providers typically include this actor metadata (`user_id`, `bot_id`, `app_id`) directly in the payload, so the check is usually a lookup rather than a guess. If a custom integration needs this filtering built in from scratch, [building a plugin on Corsair's SDK](https://docs.corsair.dev/guides/create-your-own-plugin) is one way to get webhook routing and actor filtering set up together rather than separately.\n\nThis failure mode has enough edge cases, recursion depth, timing windows, rate limits, that it earns its own dedicated post rather than a few paragraphs here.\n\nEverything above, raw body capture, per provider header formats, timestamp binding, retry semantics, is the kind of work that has to be done once correctly and then never thought about again. That is the layer [Corsair](https://corsair.dev/) is built to take off your plate.\n\nEvery plugin ships with a typed, signature verified webhook handler already wired in, and every provider routes through a single endpoint on your server rather than one per integration:\n\n``` js\napp.post('/webhooks', async (req, res) => {\n  const webhook = processWebhook(corsair, req.headers, req.body);\n  res.status(200).send('OK');\n});\n```\n\nSignature checks, raw body handling, and timestamp validation happen inside that call, scoped per tenant for multi tenant setups. What reaches your integration code is already verified and deduplicated: the event your agent acts on, not the raw request your agent has to defend itself against. That leaves your own code free to focus on what the event should cause the agent to do, rather than whether the event can be trusted in the first place.\n\nGetting webhooks right for an agent comes down to three checks done in order: confirm the request genuinely came from the provider, confirm it has not been seen before, and only then let the agent act on it. Everything else, retries, polling fallbacks, backoff timing, builds on getting that order right first.\n\nTwo related failure modes are worth reading next if you are building this out fully: how to stop an agent from triggering an infinite loop off its own actions, and how to keep a long running agent task durable once a webhook has kicked it off.\n\nIf you would rather not build and maintain this layer yourself, [Corsair](https://corsair.dev/) handles signature verification, retries, and deduplication for every integration it ships, so your agent only ever sees events that have already been checked and confirmed real. It is open source, so if a provider or plugin is missing, you can add it yourself rather than waiting on a roadmap. Self hosting is free, and credentials never leave your own database. You can see the full set of supported integrations and try it directly at [corsair.dev](https://corsair.dev/).\n\n**Why does my webhook signature verification keep failing even though the secret key is correct?** This almost always comes from body parsing. Most frameworks convert the incoming payload into an object before your handler runs, and re serializing that object to check the signature produces different bytes than the provider originally sent. Capture and hash the raw, unparsed body instead of the parsed object.\n\n**Is HMAC SHA1 still acceptable for new webhook integrations?** No. SHA1 is considered cryptographically weak and is kept by some providers, like GitHub's older X-Hub-Signature header, purely for backward compatibility. New integrations should use SHA256 variants wherever the provider offers them.\n\n**How long should I keep idempotency keys before deleting them?** 24 hours matches the retry window most providers use and is a reasonable default. 72 hours gives extra protection against a bug shipped on a Friday that is not caught until the following week. Keys are meant to guard against near term retries, not serve as a permanent log.\n\n**Should every integration use webhooks, or is polling sometimes the better choice?** Webhooks are better for fast reaction, but they should rarely stand alone. Polling is still needed for historical backfill, for providers that do not support webhooks at all, and as a reconciliation check against events a webhook silently dropped.\n\n**What is the difference between returning a 4xx and a 5xx from a webhook handler?** A 5xx tells the provider the failure was temporary and it should retry later. A 4xx tells the provider the request itself was invalid, such as a failed signature check, and retrying the same payload will not change the outcome. Returning the wrong one either causes needless retries or silent data loss.", "url": "https://wpnews.pro/news/how-to-trigger-ai-agents-from-webhooks-signature-verification-retries-and", "canonical_source": "https://dev.to/corsairdev/how-to-trigger-ai-agents-from-webhooks-signature-verification-retries-and-idempotency-49la", "published_at": "2026-10-15 12:38:43+00:00", "updated_at": "2026-10-15 12:48:58.046510+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools"], "entities": ["Slack", "GitHub", "HubSpot", "Notion", "Corsair", "Node.js", "Express"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-to-trigger-ai-agents-from-webhooks-signature-verification-retries-and", "markdown": "https://wpnews.pro/news/how-to-trigger-ai-agents-from-webhooks-signature-verification-retries-and.md", "text": "https://wpnews.pro/news/how-to-trigger-ai-agents-from-webhooks-signature-verification-retries-and.txt", "jsonld": "https://wpnews.pro/news/how-to-trigger-ai-agents-from-webhooks-signature-verification-retries-and.jsonld"}}