Ask Anvil

Answers to questions about automating PDFs, e-signatures, Webforms, and other paperwork problems.
E-signatures
Categories

Why does my e-signature webhook fire twice, and how do I handle it?

What is actually going wrong

You send a document for signature. The signer completes it once. But your endpoint runs twice: two records get created, the signer gets two "completed" emails, or a downstream API is called twice. The event data is correct, and the signature is fine. The problem is that you received the same event more than once. Almost every webhook system, e-signature platforms included, guarantees at-least-once delivery rather than exactly-once. If your endpoint is slow, returns a non-2xx status, or the connection drops after you finished processing but before your acknowledgement reaches the provider, the provider assumes delivery failed and sends the event again.

Make the handler idempotent

The durable fix is to make processing the same event twice have the same effect as processing it once. Two moves do most of the work. First, dedupe on the stable identifier the provider includes with every delivery (usually an event ID or delivery ID) rather than on your own timestamp or the document ID. Record the IDs you have already handled and skip any repeat. Second, acknowledge fast: return 2xx as soon as you have safely recorded the event, and push slow work such as downloading the signed PDF or sending email into a background job. A handler that finishes in tens of milliseconds rarely trips the retry.

const express = require('express')
const app = express()
app.use(express.json())

// Demo stores. In production use a persistent, atomic store
// (a unique constraint in your DB, or Redis) instead of these.
const processed = new Set()
const documents = new Map()

app.post('/webhooks/esign', (req, res) => {
  const { id: eventId, documentId, status } = req.body

  // 1) Acknowledge fast so the provider does not retry.
  res.sendStatus(200)

  // 2) Dedupe on the provider's stable event id.
  if (processed.has(eventId)) return
  processed.add(eventId)

  // 3) Idempotent write: set the state, do not increment.
  documents.set(documentId, { status })
  // enqueue slow work (download the signed PDF, send email) here
})

module.exports = app

Do not dedupe in memory

An in-memory Set works in a demo and fails in production: it is wiped on every restart and is not shared across multiple instances behind a load balancer, so a retry that lands on a different instance still double-processes. Use something atomic and persistent instead, such as a unique constraint on the event ID in your database, or Redis with SET key value NX. Two more things to check: verify the webhook signature before you trust the event ID, and remember that events can also arrive out of order. Write each handler as "set the state to this value," not "add one," so a late "viewed" event that arrives after "completed" cannot overwrite your final status.

Back to All Questions

The fastest way to build software for documents

Anvil Document SDK is a comprehensive toolbox for product teams launching document flows where PDF filling, signing, and complex conditional scenarios are necessary.
Explore Anvil
Anvil Webforms