Skip to content
Blue message tiles pass through a glass gate into an orderly queue, with one illuminated reply bubble on a dark navy background.
Outreach operations5 min read

Prevent Duplicate Bot Replies with Reliable Webhooks

A practical webhook design for agency bots: separate events from actions, keep durable reply records, and review uncertain sends before retrying.

A customer asks a question, your bot answers, and then it answers the same question again. The model may have done exactly what it was told: two deliveries reached the automation and each started a new reply.

For agencies connecting a bot to a permitted customer or provider chat, reliable automation starts before the prompt. You need to distinguish a new message from a repeated notification, preserve work through restarts, and know when an outgoing reply actually succeeded. This guide proposes an integration design; it does not describe an automatic AllProfiles feature or promise exactly-once delivery.

Separate the notification from the reply

Keep three identities in your integration: the source event, the underlying chat message, and the outgoing action your bot intends to take. A repeated delivery should not create a second action. Two different notifications concerning the same message may also need to share one action record.

Stripe's webhook documentation explicitly describes both repeated events and separate events concerning the same object. It also warns that events may arrive out of order. Those are documented Stripe behaviors, not a specification for every chat API. Check the actual event identity and replay contract of your integration.

For your own reply ledger, a possible action key is the organization, conversation, inbound message and action kind together. Treat that as a suggested internal design, not an API parameter. Include the source system where identifiers can overlap across integrations. Do not use message text alone: two legitimate messages can contain the same words.

Verify first, then store work before acknowledging it

Validate incoming notifications using the provider's documented signature scheme. Preserve the original request bytes where verification requires them; parsing JSON and reconstructing it can change what was signed. The Standard Webhooks specification explains that issue and pairs signature verification with timestamp checks and stable identifiers. Its header names apply to implementations of that specification, not automatically to every provider.

Then save the event and its pending work durably. Return a successful acknowledgement only after that save succeeds. If you acknowledge first and the process crashes before saving, your application may have nothing left to process.

GitHub recommends short webhook handlers and asynchronous processing. Its specific response deadline belongs to GitHub. Use your provider's deadline, and move model calls and message sending out of the receiving request.

Keep event storage and job creation atomic where possible. A duplicate of an already stored event can be acknowledged without creating another job, while the original pending job remains recoverable. If durable storage is unavailable, follow the provider's failure and redelivery rules rather than reporting that the work was safely accepted.

Make two workers compete for one action

A spreadsheet check or an in-memory set is not enough when two workers can start together. Both might read “no reply yet” before either writes its result.

Use a persistent uniqueness rule for the action identity and a coordinated claim of pending work. PostgreSQL unique constraints can enforce uniqueness across a group of columns. That protects the action record; it does not make an external message send atomic with your database.

Keep the reply lifecycle explicit:

StateWhat it recordsSuggested next step
PendingWork is stored; no send has startedLet one worker claim it
ProcessingOne worker is handling the actionTrack ownership and recover abandoned work carefully
SentThe sending API confirmed successSave its message identifier and suppress another send
Needs reviewDelivery outcome or conversation state is uncertainReconcile before another attempt
SkippedThe action is no longer appropriateRecord a short reason without sending

Persist the chosen reply before sending it. A retry should not silently generate a different answer for the same action.

Handle the uncertain send separately

Suppose a sending request times out after leaving your server. The destination may have accepted it even though your worker never received the response. Sending again immediately can produce the duplicate you were trying to prevent.

If the sending API documents idempotency, reuse its supported key according to its scope and retention rules. Do not invent a header and assume the server honors it. Otherwise, use an available message lookup or delivery receipt to reconcile the result. When the API provides no reliable way to decide, move the action to review instead of treating uncertainty as a confirmed failure.

Re-read the conversation before sending a delayed reply. A teammate may have answered, the client may have withdrawn the request, or a newer message may change the question. A replayed event is evidence of an earlier event, not permission to ignore the current thread.

An illustrative duplicate-delivery walkthrough

This fictional example uses internal labels, not an AllProfiles payload schema:

  1. Event event-7 reports inbound message message-42 in organization agency-A and conversation thread-9.
  2. The receiver stores the event and pending work. A repeated delivery of event-7 finds that same durable record.
  3. A worker claims the reply action for agency-A / thread-9 / message-42. A second worker cannot claim another copy of that action.
  4. The sender confirms the reply and returns reply-18. The ledger records it as sent.
  5. Replaying the original notification finds the existing action and does not send another reply.

If step four times out without a receipt, the correct record is uncertainty—not an invented success and not an automatic second send.

Test failures before turning on automatic replies

Use synthetic messages in an isolated test conversation. Check repeated delivery, simultaneous workers, a restart after accepting an event, an out-of-order notification, and a timeout after the sender might have accepted a reply. Also test a human answering while the bot waits and an outgoing bot message generating a notification: that notification must not start a reply loop.

Review the event record, action state and resulting conversation together. An HTTP success alone proves neither that a reply was sent nor that it was sent only once. Keep routine logs to identifiers and state transitions; avoid copying private chat bodies, tokens or signing secrets into them.

For supported operations, permissions and event contracts, use the current AllProfiles API documentation. Start with receiving and reviewing events before enabling writes. Keep a named human owner for the uncertain cases so a stopped automation does not become an unanswered customer.