Skip to content
Display settings
Reading preferences

Saved only in this browser.

Start free

eSignature API Timeouts and Unknown Outcomes: A Reconcile-First Recovery Design for Backend Teams

Executive brief

For teams evaluating resource esignature api guide

Use this guide to frame compliance risk, signing workflow fit, buyer readiness, implementation effort, and cost before choosing an eSignature path.

  • Clarifies where electronic signatures can reduce approval delays.
  • Connects the topic to relevant eSignly plans, API options, and security controls.
  • Helps decision makers compare legal, operational, and adoption tradeoffs.
View related solutionCompare plans
eSignature API Timeouts: Reconcile Before You Retry
eSignature API Timeouts: Reconcile Before You Retry

A backend worker sends a signature request for a customer contract. The HTTP call hangs, the client gives up after its timeout, and the on-call engineer sees one question in the logs: did that request go out or not? The answer decides whether a real person in another company receives zero, one, or three emails asking them to sign.

Most integration guides cover the happy path, then add a line that says "retry with an idempotency key." That advice is incomplete for consequential mutations: sending a signature request, voiding one, resending after an amendment, or finalizing a record into your archive. A timeout does not tell you the request failed. It tells you that you stopped waiting. The outcome is unknown, and unknown is a state of its own: completion could not be confirmed, which is neither success nor verified failure.

This article is written for backend leads, solution architects, and on-call engineers who own an eSignature integration. It proposes a reconcile-first operation ledger: a design you can adapt, not an established eSignly method. It covers durable tenant-scoped operation identity, immutable payload hashes, atomic work claims, webhook acknowledgment, terminal failure, uncertain input, and privacy ownership. For the fundamentals of keys, retries, and signature verification, see our existing guide on idempotency, retries, and webhook resilience. This piece starts where that one stops: the moment you cannot tell what happened.

Key Takeaways

  1. ⏱️ A timeout is an unknown outcome. Record it as such, and do not retry blindly until you have checked authoritative operation state.
  2. 🔑 An idempotency key alone is not enough. Bind a durable, tenant-scoped operation identity to a hash of the immutable approved payload, and reject key reuse with a changed payload.
  3. 🔒 Claim work atomically so two workers cannot submit the same operation. A read-then-write check is not compare-and-swap.
  4. 📥 Acknowledge a webhook only after the event is verified, deduplicated, durably recorded, and its follow-up work is scheduled, in one atomic step or an explicitly recoverable one.
  5. 🧯 Define terminal failure and uncertain-input handling before launch: quarantine what you cannot interpret, and never let silence become success.
  6. 🛡️ Identifiers such as signer emails and request IDs can be sensitive data. Assign an owner for logging, retention, and deletion.
  7. ✅ Use the go/no-go controls in the last section as all-required checks. They are not a score and they do not certify production readiness.

Why an Ambiguous Timeout Is Not the Same as a Failure

Key takeaway: A client-side timeout proves only that your wait budget ended. The provider may have rejected the request, accepted it, or still be processing it. Treat the result as unknown and resolve it with evidence.

Distributed systems textbooks describe this as the two generals problem, but the practical version is simpler. Your HTTP client closed the connection. The server may have already committed the work. The response may have been lost on the way back. A proxy in the middle may have dropped it. From your side, all three look identical.

The HTTP specification is explicit about the risk. RFC 9110 on HTTP semantics defines which methods are idempotent and notes that POST is not among them. It advises clients not to automatically retry a non-idempotent request unless they know its semantics are effectively idempotent or have some means to detect that the original request was never applied. Note the second condition. The standard describes detecting that the first attempt did not happen. That is reconciliation, and it comes before the retry.

What each observation actually proves

What your client observedWhat it provesWhat it does not proveSafe default
2xx response with a provider referenceThe provider accepted the operationThat your local database recorded itPersist the reference, mark confirmed
4xx validation or authorization rejectionThis request, as sent, was refusedThat a different earlier attempt did not succeedTerminal failure for this payload; fix input, do not loop
Rate-limit or conflict responseThe provider declined to process nowAnything about earlier attemptsRetry with backoff using the same operation identity
5xx responseThe provider or a gateway had a problemThat no side effect occurredUnknown until reconciled
Timeout or connection resetYour wait budget endedWhether the request was appliedUnknown; reconcile before any resubmission
2xx with unparsable or truncated bodyA response of some kind was producedWhich operation it refers toUnknown; reconcile

Why signature operations raise the stakes

A duplicated database row is annoying. A duplicated signature request leaves your system and lands with an external party. Consider the consequences of acting on a wrong guess:

  1. Duplicate send: a counterparty receives two requests for the same agreement and may sign the wrong one, or ask which is operative.
  2. Missed send: your system believes a request is out, so nobody follows up, and a deadline passes quietly.
  3. Wrong void: a retried cancellation can invalidate a request that was legitimately re-issued after an amendment.
  4. Split evidence: two provider records exist for one business event, and your audit trail has to explain both. Our guide to architecting esignature audit trails covers why that explanation matters later.

None of this is a legal conclusion. Whether a duplicate or misdirected request creates any legal consequence depends on your agreement, jurisdiction, and facts, so involve counsel for contracts where the answer matters. The engineering goal is narrower: never let an uncertain state silently turn into a business decision.

How Most Teams Handle It, and Why That Breaks

Key takeaway: The common patterns each fix one failure and open another. Blind retry duplicates, key-only retry assumes provider behavior you may not have verified, and "treat timeout as failure" invites a second click.

Teams rarely choose a poor strategy deliberately. They inherit one from a generic HTTP client library, a queue's default retry policy, or a pattern that worked for idempotent reads. The table compares four approaches for consequential mutations.

Decision artifact: four ways to handle a timed-out mutation

ApproachWhat it protectsHidden failure modeBuild effortAcceptable for
A. Blind retry on timeoutTransient network blips on readsDuplicate sends when the first attempt had succeededMinimalIdempotent reads only
B. Retry with the same idempotency keyDuplicate creation, if the provider honors the keyDepends on provider scope, key retention window, and how it treats a changed payload; says nothing about your own double-claim bugsLowLow-stakes creates after you have verified provider semantics
C. Treat timeout as failure; let the user retrySimplicityUser clicks again while the first request is live; support tickets about duplicatesMinimalInternal drafts, never external sends
D. Reconcile first, then retry under one operation identity (proposed)Duplicate, missed, and conflicting operationsMore state to operate; needs a provider lookup path or a documented escalationModerateAny send, void, resend, or finalize

Why idempotency keys are necessary but not sufficient

Idempotency keys are the right tool, and you should use them where the provider supports them. Stripe's documentation on idempotent requests is a useful public reference for the pattern. As a vendor statement, it describes saving the result of the first request for a key, returning that result on repeats, and returning an error if the same key arrives with different parameters. The same documentation notes that keys can be pruned after a period. That detail matters here: a retry that arrives after the provider has forgotten the key looks like a brand new request.

Those semantics are specific to one vendor. Do not assume any eSignature API behaves the same way. Some eSignly developer guides describe idempotency headers, and our guide to state sync and API reliability covers the surrounding patterns, but header names, supported endpoints, and retention windows must be confirmed in the current eSignly API reference for the endpoint you call. If the reference does not document idempotency for an operation, design as if it does not exist.

Even with provider support, a key does not solve four problems that live on your side of the wire:

  1. Two of your workers can both decide to submit before either records a key.
  2. A developer can reuse a key for an edited payload and silently get the original operation back.
  3. A late retry can outlive the provider's key retention.
  4. A webhook handler that returns success before recording the event loses the event with no retry.

2026 update, framed for the long run

A standard Idempotency-Key header has been proposed in the IETF. As of the most recent revision we reviewed, the Internet-Draft was listed as expired, with its latest revision dated October 2025. An expired draft is a work in progress, not a standard, and its own boilerplate says drafts should not be cited as reference material other than as work in progress. The durable lesson does not depend on any header: treat provider idempotency as a convenience layer you verify per endpoint, and keep your own durable record of intent as the source of truth.

A Proposed Design: The Reconcile-First Operation Ledger

Key takeaway: Record intent durably before calling the provider. Give every consequential action one operation identity, one approved payload hash, and an explicit state machine that includes an UNKNOWN state.

The following is a proposed design for teams to adapt. It is not a certified pattern, and it is not a description of how any specific eSignly feature works internally. It rests on five rules.

  1. Intent before action. Write the operation record, including the payload hash and a provider-facing key, before the first network call.
  2. Identity is tenant-scoped. Every lookup, uniqueness constraint, and queue message carries the tenant. Two tenants can never collide on an operation.
  3. Payload is immutable once approved. The hash binds the identity to exactly what a person or policy approved.
  4. Unknown is a first-class state. It is reached on timeout, 5xx, or an unparsable response, and it can only be left through reconciliation.
  5. Work is claimed atomically. A single conditional update decides which worker acts, and everyone else backs off.

The operation state machine

StateMeaningEntered whenAllowed next states
PLANNEDApproved and recorded; not yet submittedOperation createdIN_FLIGHT, SUPERSEDED
IN_FLIGHTOne worker holds a lease and is calling the providerAtomic claim succeedsCONFIRMED, TERMINAL_FAILED, UNKNOWN
UNKNOWNCompletion could not be confirmedTimeout, 5xx, bad response body, or lease expired mid-callRECONCILING
RECONCILINGOne worker is checking authoritative stateAtomic claim on UNKNOWNCONFIRMED, PLANNED (proven absent), UNKNOWN (inconclusive), NEEDS_REVIEW
CONFIRMEDProvider reference stored; operation is real2xx with reference, or reconciliation found itTerminal for this operation
TERMINAL_FAILEDProvider definitively refused this payloadValidation or permission rejectionTerminal; a corrected payload is a new operation
NEEDS_REVIEWEvidence conflicts, for example duplicates foundReconciliation sees more than one matchHuman decision, then CONFIRMED or SUPERSEDED

The control flow in plain steps

  1. The business workflow approves a payload. Your service canonicalizes it, hashes it, and inserts a ledger row in PLANNED. Insert and any outbox message commit together.
  2. A worker atomically claims the row. If the claim returns zero rows, it stops.
  3. The worker calls the provider with the persisted provider key and a bounded timeout.
  4. A 2xx stores the provider reference and moves to CONFIRMED. A definitive rejection moves to TERMINAL_FAILED. Anything else moves to UNKNOWN.
  5. A reconciler claims UNKNOWN rows, asks the provider for authoritative state, and applies the decision table in the next section.
  6. Only a proven-absent result returns the row to PLANNED for resubmission under the same operation identity and key.

Teams running the eSignly integrations or building on the API can map these states onto their own datastore. The shape is vendor-neutral, which is intentional: your ledger should outlive any single provider choice.

Building It: Identity, Payload Hash, Atomic Claim, and Reconciliation

Key takeaway: Uniqueness on (tenant, intent) prevents double creation, the payload hash prevents silent substitution, and a conditional UPDATE decides who owns the work. Code below is illustrative. It is not an official eSignly API contract.

1. Durable, tenant-scoped identity bound to an immutable payload

The business intent key describes what the business meant to do, for example "send revision 3 of contract C-1042 for signature." It should come from your workflow, not from the retry layer. The provider-facing key is a separate opaque value, generated once and stored before the first attempt. Keep it free of personal data.

The payload hash should cover everything that changes what the signer receives or what the workflow means: document content digest (not file name), signer identities and order, field placement, expiration and reminder settings, and message text. Exclude volatile values such as timestamps and request IDs. Use a deterministic serialization so the same approved content always hashes the same.

-- Illustrative schema (PostgreSQL-style) CREATE TABLE signing_operation (   tenant_id         text        NOT NULL,   operation_id      uuid        NOT NULL,   intent_key        text        NOT NULL,  -- e.g. contract id + revision + action   action            text        NOT NULL,  -- SEND, VOID, RESEND, FINALIZE   payload_hash      bytea       NOT NULL,  -- hash of canonical approved payload   provider_key      uuid        NOT NULL,  -- opaque; persisted before first attempt   provider_ref      text,                  -- set only when confirmed   state             text        NOT NULL,   attempt_no        int         NOT NULL DEFAULT 0,   lease_owner       text,   lease_expires_at  timestamptz,   PRIMARY KEY (tenant_id, operation_id),   UNIQUE (tenant_id, intent_key) );  -- Create or fetch. A conflict is resolved by comparing hashes in application code. INSERT INTO signing_operation   (tenant_id, operation_id, intent_key, action, payload_hash, provider_key, state) VALUES (:tenant, :op, :intent, :action, :hash, :pkey, 'PLANNED') ON CONFLICT (tenant_id, intent_key) DO NOTHING;  -- If zero rows inserted: SELECT the existing row. --   same payload_hash      -> return the existing operation (a true duplicate request) --   different payload_hash -> REJECT. Never reuse an intent or key for a changed payload.

Rejecting a changed payload is the point. If a user edits a contract after approval, the edit is not a retry. It is a different operation with its own identity, discussed below.

2. Atomic claim, not read-compare-then-write

A pattern that reads the state, checks it in application code, and then writes is a race. Two workers can both read PLANNED and both proceed. The claim must be a single conditional mutation that the database evaluates atomically, with the outcome read from the affected row count.

-- Claim for submit: exactly one worker can win UPDATE signing_operation    SET state = 'IN_FLIGHT',        lease_owner = :worker,        lease_expires_at = now() + interval '2 minutes',        attempt_no = attempt_no + 1  WHERE tenant_id = :tenant    AND operation_id = :op    AND payload_hash = :hash    AND state = 'PLANNED'; -- rows affected = 1 -> you own it; 0 -> stop, someone else does or state moved on.  -- Sweeper: an expired lease means the outcome is unknown, not that the call failed UPDATE signing_operation    SET state = 'UNKNOWN', lease_owner = NULL  WHERE state = 'IN_FLIGHT' AND lease_expires_at < now();

The sweeper rule is easy to miss. A worker that crashes mid-call may have sent the request. Returning the row to PLANNED would cause exactly the duplicate you are trying to prevent. If your datastore lacks conditional updates, disclose that limitation and serialize through a single-writer queue per tenant, accepting the throughput cost.

3. Worker behavior around the provider call

# Illustrative pseudo-code; function names are placeholders, not an eSignly SDK def submit(tenant_id, operation_id):     op = ledger.claim_for_submit(tenant_id, operation_id, worker_id)     if op is None:         return                       # another worker owns it, or state moved on      payload = approved_payloads.load(tenant_id, op.payload_ref)     if canonical_hash(payload) != op.payload_hash:         ledger.needs_review(tenant_id, operation_id, 'payload changed after approval')         return      try:         resp = provider.create_signature_request(             payload, idempotency_key=op.provider_key, timeout_seconds=30)     except (TimeoutError, ConnectionResetError):         ledger.mark_unknown(tenant_id, operation_id)   # not success, not failure         schedule_reconcile(tenant_id, operation_id)         return      if resp.ok and resp.provider_ref:         ledger.confirm(tenant_id, operation_id, resp.provider_ref)     elif resp.is_definitive_rejection:   # validation or permission refusal         ledger.terminal_fail(tenant_id, operation_id, resp.error_code)     else:         ledger.mark_unknown(tenant_id, operation_id)   # 5xx, truncated body, etc.         schedule_reconcile(tenant_id, operation_id)

4. Reconciliation: decide with evidence

Reconciliation asks the provider's authoritative record what exists, using the strongest identifier available. That identifier might be a provider reference you already stored, or a correlation value you embedded in the request if the API supports one. Confirm in the API reference which lookup paths exist. If there is no way to query by your reference or list recent requests, you cannot prove absence, and the safe choices narrow to relying on verified provider-side idempotency, escalating to a human, or accepting a duplicate and voiding the extra.

Reconciliation resultInterpretationAction
Exactly one matching request foundThe first attempt succeededStore provider reference, move to CONFIRMED. Do not resubmit.
No match, and lookup is authoritative and past the quiescence windowAbsence is supported by evidenceReturn to PLANNED; resubmit with the same operation identity and key
No match, but the original call may still be in flight or lookup is eventually consistentAbsence is not provenStay UNKNOWN; re-check after a longer delay
Lookup itself fails or times outStill unknownStay UNKNOWN; back off; alert if age exceeds your threshold
More than one matchA duplicate existsNEEDS_REVIEW; a human or policy chooses which to keep and voids the rest through a new, ledgered VOID operation

Note that a VOID is itself a consequential mutation. It gets its own operation identity, its own payload hash, and its own reconciliation path.

5. Retry versus a legitimate later operation

The ledger must tell "the same thing again" apart from "a new thing." The rule: a retry reuses the operation; a new business decision creates a new operation.

SituationSame operation?Why
Worker timed out; same approved payloadYesSame intent, same hash
Queue redelivers the same messageYesClaim fails; no second submit
Contract amended, new revision approvedNoNew payload hash and new revision in intent key
Signer email corrected by an authorized userNoChanges who receives the request; needs fresh approval
Deliberate resend after the signer reports a lost emailNoA distinct RESEND action decided by a person or policy
Same payload sent again by a different user by mistakeYes, rejected as duplicateIntent key already exists with the same hash

If an amendment supersedes an earlier request, mark the earlier operation SUPERSEDED in the same transaction that creates the new one, and queue a ledgered VOID for the old provider request.

Webhooks: Verified Acknowledgment, Terminal Failure, and Uncertain Input

Key takeaway: A 2xx response to a webhook is a promise that you will not need the event again. Give it only after the event is verified, deduplicated, durably recorded, and its work is scheduled. Anything you cannot interpret goes to quarantine, not to the void.

The ledger covers calls you make. Webhooks cover state changes you did not initiate, such as a signer completing or declining. The eSignly integrations page states that you configure a webhook URL in your account settings and that eSignly sends a POST request with a JSON payload when events such as sent, viewed, signed, or declined occur. Confirm the exact event names, signature scheme, retry schedule, and event identifier fields in the current API reference before you build your handler. Our deeper walkthrough of webhook architecture and event integrity covers the transport details.

What "verified acknowledgment" means

Responding 200 "quickly" is common advice, and it is incomplete. Speed is useful only if the response is honest. Acknowledge only after all of these are true:

  1. The request authenticated against the verification mechanism the provider documents.
  2. The tenant was derived from the verified endpoint or secret mapping, not from a field inside the unverified body.
  3. The event identifier was inserted into a uniqueness-constrained inbox, or was found to be already present.
  4. The follow-up work item was written in the same transaction, or an explicitly recoverable mechanism guarantees it will exist.
-- Illustrative: dedupe + durable inbox + work scheduling in ONE transaction BEGIN;  INSERT INTO webhook_inbox (tenant_id, provider_event_id, received_at, body_ref, status) VALUES (:tenant, :event_id, now(), :body_ref, 'RECEIVED') ON CONFLICT (tenant_id, provider_event_id) DO NOTHING;  -- Only when the inbox insert actually inserted a row: INSERT INTO work_item (tenant_id, kind, subject_ref, dedupe_key, run_after) VALUES (:tenant, 'APPLY_SIGNING_EVENT', :provider_ref, :event_id, now()) ON CONFLICT (tenant_id, dedupe_key) DO NOTHING;  COMMIT; -- Return 2xx only after COMMIT succeeds. If the transaction fails, return 5xx -- so the provider's delivery mechanism, if documented, can try again.

If the provider does not supply a stable event identifier, deduplication on derived fields is weaker. Say so in your runbook, and prefer fetching authoritative state after each event rather than trusting the payload alone.

Treat the webhook as a signal, apply state with conditional mutation

Events can arrive late, twice, or out of order. When a worker applies one, do not read local state, compare in code, and then write. Push the condition into the write itself so the datastore arbitrates:

UPDATE contract_signing    SET status = 'COMPLETED', completed_at = :event_time  WHERE tenant_id = :tenant    AND provider_ref = :ref    AND status IN ('SENT', 'PARTIALLY_SIGNED'); -- rows affected = 1 -> applied -- rows affected = 0 -> inspect current status: --    COMPLETED -> duplicate or late event, record as no-op --    VOIDED or DECLINED -> conflicting history, route to NEEDS_REVIEW --    row missing -> uncertain input, quarantine

This covers your own database. The provider side has its own limits: if its API offers no conditional mutation, such as an entity-version check on void, a race between your void and a signer completing is decided on the provider's side. Disclose that in your design and reconcile afterward.

Three rules for the unhappy paths

ConditionClassificationHandling
Signature or authentication check failsUntrusted inputReject; log minimal security metadata; never create business state
Verified event, unknown event type or unmapped provider referenceUncertain inputRecord in inbox as QUARANTINED, alert, and acknowledge only because it is durably stored for review. Do not drop or guess.
Verified event that repeatedly fails to applyTerminal failure for that work itemDead-letter after a bounded number of attempts, retain the raw event, open an owned ticket, and keep reconciling against provider state

Terminal failure needs an owner and a clock. A dead-letter queue nobody reads is just a slower way to lose events. Name the team responsible, set an alert on age, and give them a way to replay safely, through the same conditional updates, so a replay cannot corrupt state.

The same discipline applies to automated callers. If an internal tool, workflow engine, or AI agent can trigger sends or voids, route it through the ledger instead of letting it call the provider directly. An automated caller that retries aggressively is the fastest way to generate duplicates.

Why This Fails in the Real World

Key takeaway: Experienced teams still fail here because the dangerous states are rare, the controls span several owners, and each piece looks correct in isolation. The scenarios below are illustrative composites of common patterns, not customer results.

Failure pattern 1: The correct retry that was correct for the wrong layer

A platform team configures its HTTP client to retry timeouts three times with backoff, which is a sensible default. A separate product team's job queue also retries failed jobs. A send request times out at the client, the client retries, and the first attempt turns out to have succeeded, so the provider holds two requests. Each layer behaved reasonably. Nobody owned the question "what is the identity of this operation across layers?" The system gap is that retries were configured per library, while operation identity belonged to nobody. The fix is structural: exactly one layer retries consequential calls, and it does so only through the ledger.

Failure pattern 2: The key that outlived its payload

A developer generates an idempotency key from the contract ID, which seems natural and stable. A user then edits the signer list and re-submits. Depending on provider semantics, the call is either rejected as a key conflict or, worse, returns the original request's result, so the team's system believes the new signer list is out when the old one is. Careful engineers still miss this because the key looked deterministic, and deterministic feels safe. The root cause is that identity was bound to the business object rather than to an approved payload. Binding the key to a payload hash, and rejecting reuse with a different hash, closes it.

Failure pattern 3: Success acknowledged, work lost

A webhook handler validates the signature, returns 200 to stay under a latency budget, and then enqueues work to an in-memory queue. A deploy restarts the process before the worker runs. The provider saw success and will not redeliver. The contract stays "sent" in your system forever, and the signer waits for a follow-up that never comes. The team had load tests and dashboards. What it lacked was an explicit definition of what a 2xx promises. The remedy is the single-transaction inbox plus work item shown earlier, with an age alert on anything still RECEIVED.

Failure pattern 4: The unknown state with no owner

A reconciler exists, but when the lookup is inconclusive it quietly retries forever, or worse, flips the row back to PLANNED after a fixed delay to "unstick" it. During a provider degradation, hundreds of unknown rows get resubmitted at once. This happens because an alert on stuck state feels like noise and a timer feels like a solution. Make inconclusive reconciliation escalate to a named owner, and make the return to PLANNED conditional on evidence, not elapsed time.

Why smart teams repeat these

  1. The failure window is small, so staging and load tests rarely hit it.
  2. Ownership is split between platform, product, and compliance, so nobody sees the whole path.
  3. Dashboards show request success rates, not unresolved operations.
  4. Provider documentation describes the happy path and leaves edge semantics to inference.

Go/No-Go Controls, Privacy Ownership, and Trade-offs

Key takeaway: Use the controls below as an all-required gate. If any one is missing, the answer is no-go for consequential mutations, regardless of how many others pass. This list helps you find gaps. It does not certify production readiness or legal compliance.

Mandatory go/no-go controls

#ControlEvidence to show
1Operation intent is written durably before any provider callLedger row exists with payload hash prior to first attempt
2Operation identity and uniqueness are tenant-scopedUnique constraint includes tenant; cross-tenant test fails closed
3Key reuse with a changed payload is rejectedAutomated test: same intent, different hash, expect rejection
4Work is claimed by atomic conditional mutationConcurrency test with parallel workers: one claim wins
5Timeouts and ambiguous responses land in UNKNOWNFault-injection test, including lost response after provider success
6No resubmission occurs before reconciliationCode path review: only reconciled-absent rows return to PLANNED
7Provider idempotency behavior is verified, not assumedWritten note citing the current API reference for each endpoint, or a statement that it is unsupported
8Webhook ack follows verification, dedupe, durable record, and schedulingCrash test between receipt and processing loses no event
9Terminal failure and quarantine have owners and age alertsNamed team, runbook, alert thresholds
10State changes use target-side conditional writesRows-affected handling for duplicate, late, and conflicting events
11Legitimate later operations get new identitiesAmendment, correction, and resend tests produce new operations
12Privacy ownership for identifiers is assignedDocumented owner, retention rule, and log redaction policy

Identifiers can themselves be sensitive

It is tempting to treat IDs as safe because they are not names. They often are not. Signer email addresses, provider references that map to a specific person and agreement, IP addresses in audit data, and even the existence of a signing request can reveal a business relationship or a health, employment, or financial matter. The OWASP Logging Cheat Sheet advises against recording sensitive data in logs and recommends considering what to remove, mask, or hash. Apply that to this design:

  1. Log the operation ID and tenant, not signer emails or document names.
  2. Keep payload hashes in the ledger. If a hash could be guessed from low-entropy inputs, use a keyed hash in anywhere it is exposed outside the ledger.
  3. Store raw webhook bodies in an access-controlled store with a retention period, and reference them from the inbox rather than copying them into logs.
  4. Decide who owns deletion requests. Ledger rows may be part of evidence you need to keep, while copies in logs and queues usually are not. Counsel and your privacy lead should decide retention, since obligations vary by jurisdiction, contract, and industry.

Vendor security statements are a separate question from your obligations. eSignly publishes its position on security and compliance; read those pages for the scope of each certification or attestation and request the underlying reports where relevant. A provider's controls do not discharge your own duties for how your ledger, logs, and queues handle personal data.

Trade-offs to accept with eyes open

Cost of the designWhat you get in returnMitigation
More state and a reconciler to operateNo blind duplicates; explicit unknown handlingStart with SEND and VOID; add actions gradually
Slower recovery when lookup is inconclusiveEvidence-based resubmissionEscalation path and a human review queue
Possible duplicates remain if provider has no lookup and no idempotencyHonest disclosure instead of false safetyVoid-on-duplicate policy, user messaging, ledgered VOID
Per-tenant serialization if no conditional writesCorrectness over throughputPartition by tenant, scale horizontally

What to do next, based on your gaps

  1. Fewer than all controls today: pick SEND, add the ledger row and the atomic claim, and ship that alone first. Controls 1 through 6 give the largest reduction in duplicate risk.
  2. Controls 1 through 6 in place: work on webhook inbox atomicity and the quarantine path.
  3. All twelve with evidence: run a game day. Kill a worker mid-call, drop a response, and replay a day of webhooks. The test is whether the ledger explains every outcome.

Building on an eSignature API and want fewer 2 a.m. surprises?

Start with a free account, read the API reference, and test your timeout and webhook paths in a sandbox-style workflow before real signers are involved.

Try eSignly's account and API plans and validate each control against the current documentation.

Start Free

Conclusion: Make Unknown a State You Can Explain

Reliable eSignature integrations are rarely lost on the happy path. They are lost in the minute after a timeout, when a system quietly converts "we do not know" into "it worked" or "it failed." The design in this article gives that minute a structure. Five concrete actions to take this quarter:

  1. Inventory your consequential mutations. List every place your systems send, void, resend, correct, or finalize a signing request, including workflow engines and automated tools. Identify which layer retries each one today.
  2. Add an operation ledger for SEND first. Persist intent, tenant, and payload hash before the first call, and use a conditional UPDATE for claims.
  3. Introduce an UNKNOWN state and a reconciler. Forbid resubmission until reconciliation proves absence, and write down what you will do if the provider offers no lookup.
  4. Fix webhook acknowledgment semantics. Make verify, dedupe, record, and schedule a single transaction or a documented recoverable sequence, with quarantine and dead-letter ownership.
  5. Assign privacy ownership. Decide who owns identifiers in logs, queues, and the ledger, and verify provider behavior against the current API reference rather than assumptions.

The reconcile-first ledger is a proposed design. Adapt it to your datastore, your provider's documented behavior, and your counsel's guidance, and test it with fault injection before you rely on it.

About eSignly

eSignly has provided online electronic signature software and API services since 2014, serving businesses in the United States, EMEA, and Australia. The company states that it maintains ISO 27001, SOC 2 Type II, HIPAA, GDPR, PCI DSS, and 21 CFR Part 11 compliance programs; the scope of each is described on its security page and compliance page, and your own obligations depend on your use case and jurisdiction. This article was reviewed by the eSignly Expert Team. It is general technical guidance, not legal advice.

Frequently Asked Questions

Is it ever safe to retry an eSignature API request after a timeout?

It can be safe, but only after you have either verified that the provider treats the retry as the same operation or confirmed through an authoritative lookup that the first attempt was never applied. For consequential mutations such as sending or voiding, mark the operation unknown, reconcile its state, and then resubmit under the same operation identity. Blind retry is acceptable for idempotent reads.

Does an idempotency key remove the need to reconcile?

No. A key helps only if the provider supports it for that endpoint, retains it long enough, and handles changed payloads in a way you have verified. It does not stop two of your workers from both submitting, and it does not help when a retry arrives after the key has been pruned. Confirm behavior in the current API reference, and keep your own durable ledger.

What should happen if the same operation key arrives with a different payload?

Reject it. Bind the operation identity to a hash of the immutable approved payload and return a conflict to the caller. A changed payload is a new business decision, so it needs a new operation, fresh approval, and, if it replaces an earlier request, an explicit ledgered void of the old one.

What does an unknown result mean, and how is it different from failure?

An unknown result means completion could not be confirmed. It is not success and it is not verified failure. Typical causes are timeouts, 5xx responses, truncated bodies, and workers that crash mid-call. The only way out of unknown is reconciliation against authoritative state, or human review if no lookup exists.

When should a webhook handler return a success response?

Return 2xx only after the request is authenticated, the tenant is derived from a trusted mapping, the event is deduplicated and durably recorded, and the follow-up work is scheduled atomically or by an explicitly recoverable mechanism. If recording fails, return an error so the provider's documented delivery behavior can try again. Check the API reference for retry details.

How should we handle webhook events we cannot interpret?

Authenticate first. If authentication fails, reject and create no business state. If it passes but the event type or reference is unknown, store the raw event in a quarantine status, alert an owner, and do not guess. Acknowledge it only because it is durably stored for review.

Are signer emails and request IDs sensitive?

They can be. Emails identify people, and a request reference can reveal a relationship or transaction when combined with other data. Minimize them in logs, use operation IDs for tracing, restrict access to raw payloads, and assign an owner for retention and deletion. Specific obligations depend on your jurisdiction and contracts, so consult counsel.

Do the go/no-go controls prove my integration is production-ready or compliant?

No. They are all-required checks for one risk area, ambiguous outcomes on consequential mutations. Passing them does not certify production readiness, security, or legal compliance. Combine them with your security review, load testing, provider due diligence, and legal advice.

Ready to design signing workflows that stay explainable under failure?

Explore eSignly's API, account, and on-premises options, then test your retry, reconciliation, and webhook paths against the current API reference.

Create a free eSignly account and see how your integration behaves before real signers depend on it.

Free Trial
Related solution

This article is most relevant for CTOs and developers who need to roll out a practical signing workflow. Use the related eSignly path to compare plans, API options, compliance fit, and implementation next steps.

Explore related solutionCompare plans
Editorial review

Reviewed for electronic signature decision makers

This guide is reviewed for clarity, legal and operational relevance, service alignment, and practical conversion path before being connected to an eSignly plan or API workflow.

Reviewed byeSignly content, product, and conversion review team
Reviewed2026-10-16
FocusResource Esignature Api Guide

For regulated, high-volume, or customer-facing workflows, validate legal duties, plan assumptions, and integration requirements with your internal stakeholders before rollout.