Short answer: in an embedded eSignature flow, anything that reaches your application through the signer's browser is a hint, not evidence. That covers an iframe completion callback, a postMessage event, and a return URL that says status=signed. Your server should unlock downstream actions only after it confirms the document's state with the signing provider over an authenticated server-to-server call. This article calls that approach verify-before-unlock.
Embedded signing is attractive because the signer never leaves your product. The cost is a new trust boundary. The signing ceremony runs in a browser you do not control, while the business consequence (provisioning an account, releasing a payment, opening a record) runs on your servers. Every signal that crosses between the two passes through territory a signer, a script, or an attacker can influence.
This guide is written for full-stack developers who are building or hardening an embedded signing integration. It covers the browser-side controls, the server-side state machine, tenant-safe idempotency, retries after ambiguous timeouts, and the privacy questions that teams usually discover late. It also ends with a set of mandatory go/no-go controls rather than a score, because a weighted score can hide a missing control.
- Scope: iframe, JavaScript widget, and redirect-return patterns for embedded signing.
- Not covered: whether a particular signature satisfies a particular law. That depends on your jurisdiction, document type, and counsel's advice.
- Code: every snippet is illustrative. Confirm exact endpoints, field names, and event contracts in your provider's official API documentation before you build.
Key Takeaways
- Browser signals are UX hints. Use them to show a "checking your signature" state and to trigger verification. Never use them to unlock anything.
- Verify server-side. Fetch authoritative document state over an authenticated server-to-server call, bound to the right tenant and operation, before any downstream action.
- Acknowledge only after durable recording. Record the inbound signal, deduplicate it, and schedule the verification job in one atomic step before returning 2xx to a webhook sender.
- Unknown is its own state. A timeout or conflicting signal means completion could not be confirmed. It is neither success nor verified failure. Reconcile before retrying any consequential mutation.
- Bind idempotency to the approved payload. Use a tenant-scoped operation key plus a payload hash, and reject a reused key that carries different content.
- Treat signing URLs and identifiers as sensitive. Signer emails, document IDs, and signing links need an owner, a retention rule, and a place where they must never be logged.
- Ship on all-required controls. Use the go/no-go checklist in this article. Any single "no" blocks release.
Why Embedded Signing Creates a Trust-Boundary Problem
Key takeaway: the signing ceremony and your business logic live in different places. Every signal that travels between them should be graded by who could have forged it.
With a standard emailed signing request, the signer leaves your product, signs on the provider's page, and your system learns the result through a server notification or a status check. The browser is barely involved in your state changes. Embedding changes that. Your page now hosts the signing experience, and your frontend code gets callbacks, messages, and redirects that look like authoritative news.
They are not authoritative. A signer can open developer tools. A browser extension can modify a page. A user can paste a return URL into the address bar. A tab can close one second before a callback fires. None of that makes embedded signing a poor choice. It means the signal source determines how much weight the signal deserves. For the strategic question of whether to embed at all, see our comparison of embedded eSignature APIs vs. third-party portals. This article assumes you have decided to embed and need it to be safe.
Signal grading: who can forge what
| Signal | Where it originates | Who can forge or replay it | Safe use |
|---|---|---|---|
| Widget or iframe completion callback | JavaScript in the signer's browser | Any user with developer tools; any script on your page | Update the UI; request server verification |
postMessage from the signing frame | Another window in the frame hierarchy | Any window that can reach yours, unless you check origin and source | Same as above, after origin, source, and shape checks |
| Return URL with query parameters | The signer's browser navigation | Anyone who can type or share a URL | Locate the pending operation; request verification |
| Webhook delivery | Provider server to your server | Anyone who can reach your endpoint, unless you verify authenticity | Authenticated notification; trigger verification or reconciliation |
| Authenticated status fetch | Your server calling the provider API | Only a party holding your credentials | The basis for unlocking downstream actions |
Notice the pattern: only the last row involves a credential that your server holds and the browser never sees. That is the row your business logic should trust. For webhook authenticity specifically, our guide to esignature webhook security goes deeper on integrity and signature checks.
What "unlock" means in practice
The risk is proportional to what the signal unlocks. Typical unlock actions include granting access to a customer portal, provisioning a paid account, releasing a shipment, marking a contract as executed in a CRM, triggering invoicing, or exposing a protected record. A forged "signed" signal that only changes a progress bar is a nuisance. A forged signal that releases funds is an incident. Classify each downstream action before you decide how much verification it needs, and default to full verification for anything that moves money, grants access, or creates obligations.
How Most Teams Wire Embedded Signing, and Why It Breaks
Key takeaway: the common first implementation lets the browser tell the backend that signing finished. It works in every demo and fails under real conditions.
Quick-start snippets for embedded signing, including ours, are designed to show the shape of the integration: render the signing session, receive a completion callback, update the page. The eSignly embedded widget page shows an onComplete callback that updates an application status, and the eSignly API page shows a server call that creates an embedded session and returns a signing URL for an iframe, along with a redirect URL. Those examples are legitimate starting points. They are not a production control design, and no vendor quick-start should be read as one.
The pattern that causes trouble looks like this:
- The frontend receives the completion callback or lands on the return URL.
- It calls your own endpoint, such as
POST /contracts/{id}/signed, passing a document or contract ID. - Your endpoint marks the contract executed and triggers downstream work.
Here is what is wrong with that, step by step:
- Forgeable. Anyone who can call your endpoint can declare any contract signed.
- Insecure direct object reference. The client supplies the ID, so a signer may be able to "complete" someone else's contract, including across tenants in a multi-tenant product.
- Lossy. If the tab closes, the network drops, or an ad blocker interferes, the callback never arrives and a genuinely signed document stays "pending" in your system.
- Replayable. A page refresh or a repeated callback triggers downstream actions twice.
- Blind to outcomes. A decline, expiry, or void produces no "signed" callback, so the UI spins forever and support tickets pile up.
Three approaches compared
| Dimension | A. Emailed or hosted signing request | B. Embedded, browser callback unlocks | C. Embedded, verify-before-unlock (proposed) |
|---|---|---|---|
| Build effort | Lowest | Low | Moderate: state machine, inbox, verification job |
| Signer experience | Leaves your product | Stays in your product | Stays in your product, with a brief "confirming" state |
| Spoofing exposure | Low if you rely on server notifications | High | Low |
| Lost-callback recovery | Server-driven | Weak: depends on the browser | Server-driven reconciliation |
| Operational burden | Low | Low until incidents begin | Moderate: monitoring of unknown and terminal states |
| Best fit | Low-volume or low-risk flows | Prototypes and demos only | Production flows that unlock access, money, or records |
Option B is not wrong for a prototype. It is wrong the moment its output has a consequence you would have to explain to an auditor, a customer, or counsel.
A Proposed Design: The Verify-Before-Unlock Model
Key takeaway: separate hints from authority. Hints move the UI and trigger verification. Only a server-side authenticated check can move business state.
The model below is a proposed design that we are suggesting for embedded flows. It is not an industry standard, and it is not a statement about how any specific vendor's product behaves. It is a pattern assembled from well-understood web security practice and reliable-messaging practice, and you should adapt it to your architecture.
The three tiers
- Tier 1, hints: iframe callbacks,
postMessageevents, return URL parameters. Effect: show "confirming your signature" and enqueue a verification request. Never change contract state. - Tier 2, authenticated notifications: verified webhooks. Effect: record durably, deduplicate, enqueue verification. Treat as a trigger that may be late, duplicated, out of order, or missing.
- Tier 3, authoritative state: a server-to-server fetch of document status, signer set, and signed artifact reference. Effect: the only input that may transition the operation to a signed state.
The state machine
Browser hint ----\
Return URL ------+--> inbound_signal (dedupe) --> verify job --> provider API (server-to-server) Webhook ---------/ | v AWAITING_SIGNER -> VERIFYING -> VERIFIED_SIGNED -> outbox -> downstream unlock |--> DECLINED | EXPIRED | VOIDED (terminal, no automatic resend) |--> UNKNOWN (reconcile with backoff, then human review)Three details deserve emphasis:
- Terminal states are real states. Declined, expired, and voided outcomes must end the workflow cleanly. If they do not, retries continue forever and support has no ground truth.
- UNKNOWN is a first-class state. It means completion could not be confirmed. It is not success and it is not verified failure. A worker that cannot reach the provider, or that sees conflicting signals, records UNKNOWN and reconciles later.
- Legitimate later operations are new operations. A corrected document that needs to be re-sent is not a retry. It is a new, versioned operation that supersedes the old one. Our supersession design for voiding, correcting, and re-sending requests covers that case.
Six rules to enforce
- R1. No state transition to a signed state from any Tier 1 input.
- R2. Every inbound signal is recorded before it is acknowledged.
- R3. Every verification binds the provider document ID to the tenant and operation that created it.
- R4. Every state transition is a conditional write on the target store, not a read followed by an unconditional write.
- R5. Every consequential provider call has a durable operation record created before the call is made.
- R6. Ambiguity is reconciled against authoritative state before any retry of a mutation.
If your team also handles what happens after verification, the completion handoff guide picks up at the point this design hands off to downstream systems.
Browser-Side Hardening: Frames, Messages, Return URLs, and Signing Links
Key takeaway: you cannot make the browser trustworthy, but you can make it hard to abuse. Lock down framing, message handling, redirects, and the signing link itself.
1. Control who can frame what
Two separate questions apply. First, which origins may your own pages be embedded in? Second, which origin may your page embed? The CSP frame-ancestors directive answers the first and is the modern defense against clickjacking of your own pages. The frame-src directive answers the second, and it should name the exact signing origin documented by your provider. For the provider's side of the relationship, confirm in its documentation which of your domains it allows to frame its signing page, and register only exact production and staging domains.
# Pages of your app that must never be framed by other sites Content-Security-Policy: frame-ancestors 'none' # The page that hosts the signing frame (replace with the exact origin from provider docs) Content-Security-Policy: frame-src https://signing.provider.example; frame-ancestors 'self' Referrer-Policy: no-referrer Cache-Control: no-storeIf you use the iframe sandbox attribute, document the flags you chose and test them against the provider's signing page. An over-restrictive sandbox can break signature capture, and an over-permissive one defeats the purpose.
2. Validate every message
MDN's guidance on window.postMessage is blunt: if you expect messages from other sites, always verify the sender using origin and possibly source, and then still verify the syntax of the message. The HTML Standard's web messaging security notes add that wildcard target origins should not be used for confidential data. A defensive listener looks like this:
var PROVIDER_ORIGIN = 'https://signing.provider.example'; // exact origin from provider docs var frame = document.getElementById('signing-frame'); window.addEventListener('message', function (event) { if (event.origin !== PROVIDER_ORIGIN) return; if (event.source !== frame.contentWindow) return; var msg = event.data; if (!msg || typeof msg.type !== 'string' || msg.type.length > 64) return; if (msg.type === 'signing_finished') { // UX hint only: do not forward msg contents to your server as proof showStatus('Checking your signature...'); fetch('/api/signing/' + encodeURIComponent(window.SIGNING_HANDLE) + '/verify', { method: 'POST', credentials: 'same-origin' }); } });Note what the handler does not do: it does not send a document ID, a status, or any message content to your server. It sends an opaque handle tied to the authenticated user's session. The server looks up the operation it created and verifies it independently. The event name signing_finished is illustrative; use the real event contract from your provider's documentation.
3. Treat the return URL as a locator, not a verdict
A return URL tells you where to send the browser after signing. It should be static or exactly allowlisted, because open redirects are a well-documented weakness; the OWASP guidance on unvalidated redirects and forwards is the right reference. Register exact URLs where your provider supports it, and avoid wildcard patterns. When the browser lands on the return page, resolve the operation from your own authenticated session, not from query parameters, and show a "confirming" state while verification runs. If query parameters are present, log their existence for diagnostics, but never let them decide the outcome.
4. Protect the signing link itself
An embedded signing URL typically works as a capability: whoever holds it may be able to open the signing session. Handle it like a short-lived secret:
- Create it server-side, as late as possible, and return it only to the authenticated user for whom the operation was created.
- Keep it out of logs, analytics events, error trackers, and support screenshots.
- Set a restrictive Referrer-Policy on pages that render it so the URL is not leaked to third-party resources, and use
Cache-Control: no-storeon those responses. - Do not email or chat the embedded link as a substitute for a standard signing request. If the signer needs to continue later, issue a fresh session.
- Confirm the link's lifetime and single-use behavior in your provider's documentation rather than assuming them.
You can see how eSignly describes its embedding options on the integrations page, which lists iframe and JavaScript library embedding alongside webhooks and white-label controls. Whichever vendor you evaluate, ask the same question: what exactly can I verify on the server after the browser says it is done?
Server-Side Verification: Idempotency, Atomic Claims, and Reconciliation
Key takeaway: the server needs a durable operation identity, an atomic claim on work, an inbox that records before it acknowledges, and conditional state transitions. Retries are safe only when the operation's real state is known.
Step 1: Create a durable, tenant-scoped operation before calling the provider
Before your code asks the provider to create a signing request or session, write an operation record. Its identity should combine the tenant, the operation type, the business object, and a version number, for example create_session:{business_object_id}:v3. Store a hash of the approved, canonical payload (document version, signer identities, field configuration). Then enforce three rules:
- Same key, same hash: return the existing operation. Do not create a second signing request.
- Same key, different hash: reject with a conflict. A changed payload under an old key is either a bug or an attack, and quietly accepting it would bind the old identity to new content.
- New version, new key: a legitimate correction or re-send is a distinct operation that explicitly supersedes the earlier one.
Scope every key and every unique constraint by tenant. A global dedupe key is a cross-tenant incident waiting to happen: two tenants with the same business object number could suppress or overwrite each other's events.
Step 2: Claim work atomically
Multiple workers will pick up the same pending operation. Let the database decide who wins, with a single conditional update that returns the number of affected rows. Do not read the row, compare in application code, and then write; that is read-compare-then-write, and two workers can both pass the compare. If your provider's API offers no conditional precondition for a call, say so in your design document and compensate with per-operation serialization and post-call reconciliation.
CREATE TABLE signing_operation ( tenant_id TEXT NOT NULL, operation_key TEXT NOT NULL, -- e.g. create_session:{business_object_id}:v3 payload_hash TEXT NOT NULL, -- sha256 of the approved, canonical payload state TEXT NOT NULL, -- PENDING, CLAIMED, AWAITING_SIGNER, VERIFYING, -- VERIFIED_SIGNED, DECLINED, EXPIRED, VOIDED, UNKNOWN_OUTCOME version INTEGER NOT NULL DEFAULT 0, provider_doc_id TEXT, lease_owner TEXT, lease_expires_at TIMESTAMPTZ, PRIMARY KEY (tenant_id, operation_key) ); CREATE TABLE inbound_signal ( tenant_id TEXT NOT NULL, dedupe_key TEXT NOT NULL, -- provider event id, or hash of source + doc id + event type source TEXT NOT NULL, -- WEBHOOK, RETURN_URL, BROWSER_HINT received_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (tenant_id, dedupe_key) ); -- Claim: exactly one worker wins (rows affected = 1) UPDATE signing_operation SET state = 'CLAIMED', lease_owner = $1, version = version + 1, lease_expires_at = now() + interval '2 minutes' WHERE tenant_id = $2 AND operation_key = $3 AND state = 'PENDING';Notice that the claim only matches PENDING. A claimed create-call whose lease expires may or may not have reached the provider. A sweeper should move it to UNKNOWN_OUTCOME for reconciliation, not hand it back to PENDING for a blind second attempt.
Step 3: Reconcile before retrying an ambiguous timeout
If the create call times out, you do not know whether a signing request exists. Retrying blindly can send the signer two requests, and if both are completed you have two executed documents for one intent. Instead:
- Mark the operation
UNKNOWN_OUTCOME. - Look for the authoritative result using whatever the provider supports: a lookup by a client reference you attached to the request, a filtered list within the time window, or a status check by returned ID if you received one. Confirm in the official API documentation which of these exist.
- If a matching request exists, adopt it and move to
AWAITING_SIGNER. - If none exists and the provider can confirm that, safely re-issue under the same operation key.
- If the provider cannot confirm either way, escalate to review rather than guess.
The full pattern is described in our guide to API timeouts and unknown outcomes.
Step 4: Acknowledge signals only after durable recording
When a webhook arrives, or the browser asks for verification, do the following in a single database transaction: verify authenticity, insert the inbound_signal row (a unique-constraint conflict means a duplicate, which is fine), and insert the verification job into the queue table. Return 2xx only after the commit succeeds. If the commit fails, return a non-2xx response so the sender's retry mechanism, whose exact schedule you should confirm in provider documentation, gets another chance. Acknowledging first and recording later is how events vanish.
Step 5: Verify and transition with a conditional write
async function verifySigning(tenantId, operationKey) { var op = await db.getOperation(tenantId, operationKey); var remote; try { // illustrative call: confirm the real endpoint and fields in official API docs remote = await provider.getDocumentStatus(op.provider_doc_id); } catch (err) { return reschedule(op, 'UNKNOWN'); // could not confirm: not success, not failure } if (remote.document_id !== op.provider_doc_id) return quarantine(op); if (remote.status === 'completed' && signersMatch(remote, op) && await artifactReferenceRecorded(remote)) { return transitionAndEnqueue(op, 'VERIFIED_SIGNED'); } if (['declined', 'expired', 'voided'].includes(remote.status)) { return transitionTerminal(op, remote.status.toUpperCase()); } return reschedule(op, 'VERIFYING'); // still pending } -- transitionAndEnqueue runs one transaction: UPDATE signing_operation SET state = 'VERIFIED_SIGNED', version = version + 1 WHERE tenant_id = $1 AND operation_key = $2 AND provider_doc_id = $3 AND state IN ('AWAITING_SIGNER', 'VERIFYING') AND version = $4; -- rows affected = 1: this worker won; insert the outbox row in the same transaction. -- rows affected = 0: another worker moved it; re-read and do not overwrite.The conditional UPDATE on your own store is a true compare-and-swap, because the comparison and the write happen in one statement. Pair it with an outbox row written in the same transaction so the downstream unlock cannot be lost or fired twice. Downstream consumers should then use their own idempotency key derived from the operation, so that an outbox redelivery does not provision two accounts.
Step 6: Privacy ownership
Identifiers can themselves be sensitive. A signer email, a document ID, a signing URL, and a webhook payload each reveal who is transacting and about what. Decide ownership before launch, not after an audit question.
| Data element | Why it is sensitive | Rule to set |
|---|---|---|
| Signer email or name | Personal data; links a person to a transaction | Keep out of URLs and query strings; mask in logs |
| Signing URL | May act as a bearer capability for the session | Never log; short lifetime; no-referrer; no caching |
| Provider document ID | Correlates records across systems; may be guessable or enumerable if mishandled | Resolve server-side from the session; never accept from the client as proof |
| Webhook payload | May contain personal data and contract metadata | Store minimum necessary; define retention and access owner |
| Signed artifact and audit trail | The evidence record | Name a retention owner; align with counsel on how long and where |
Whether you act as a controller, processor, or something else for this data, and what your contractual and regulatory duties are, depends on your situation. That is a question for your legal and privacy teams, not for a vendor's marketing page.
Why This Fails in the Real World: Common Failure Patterns
Key takeaway: experienced teams still ship the weak pattern because the failure only appears at the edges: closed tabs, retried timeouts, shared ID spaces, and users who edit URLs. The causes are ownership gaps and happy-path testing, not individual carelessness.
The scenarios below are illustrative composites of failure modes that this class of integration is prone to. They are not customer results, and they contain no measured figures.
Pattern 1: The closed tab and the double send
A signer completes the document, then closes the tab before the callback fires. The product still shows "awaiting signature." The signer emails support, and an agent clicks "resend," which creates a second signing request. The signer, confused, signs again. The system now holds two executed documents for one intent, and only one of them is linked to the customer record. Nobody on the team was careless. The frontend team assumed the backend would reconcile. The backend team assumed the frontend callback was the source of truth. Neither owned the question "what is the real state of this document?"
Design fix: server-driven reconciliation on a schedule for every operation in AWAITING_SIGNER or UNKNOWN, and a resend action that first checks authoritative state and creates a superseding operation, not a duplicate.
Pattern 2: The editable return URL
The return page reads ?status=signed&doc=... and calls the backend to mark the contract executed. During testing, a colleague pastes the URL with a different ID to see what happens, and a different customer's contract flips to "signed." The team had tested the happy path and an unauthorized-user path, but not the "authorized user, wrong object" path. In a multi-tenant product the same flaw can cross tenant boundaries.
Design fix: resolve the operation from the authenticated session, ignore client-supplied status, and verify the provider document ID against the tenant-scoped operation record.
Pattern 3: The timeout that was not a failure
A create-session call times out at the gateway while the provider has in fact accepted it. A worker retries. The signer receives two requests. Retry logic was written for idempotent reads and reused for a consequential mutation. The retry library did exactly what it was configured to do.
Design fix: classify operations as safe-to-retry or reconcile-first, and route consequential mutations through UNKNOWN_OUTCOME.
Pattern 4: The global dedupe key
A webhook consumer dedupes on the provider document ID alone, in a table shared by every tenant. When test data is imported into a second tenant with identical IDs, events are dropped as duplicates. Nothing errors. Contracts simply never complete downstream.
Design fix: every unique constraint and idempotency key includes the tenant, and import tooling is tested for ID collisions.
Why capable teams still land here
- Ownership gaps: embedded signing straddles frontend, backend, security, and legal. The control that matters most (server verification) belongs to no single team's backlog.
- Demo-driven testing: sandbox flows succeed. Failure modes require deliberate injection. Our failure injection plan lists the cases to simulate.
- Sample code promoted to production: snippets show the mechanism. Controls are left as an exercise that never gets assigned.
Trade-offs of the verify-before-unlock design
| Cost | What it means | Mitigation |
|---|---|---|
| Extra latency | A short "confirming" state between signing and unlock | Trigger verification immediately on any hint; show clear progress |
| More moving parts | Inbox, queue, state machine, sweeper | Start with a small relational implementation; add infrastructure only when volume demands it |
| Additional API calls | Verification fetches count against rate limits | Deduplicate triggers per operation; use backoff; confirm rate limits in provider documentation |
| Strict CSP and sandbox settings | Misconfiguration can break signing | Test in staging with the exact provider origin; document each setting |
| Residual provider-side ambiguity | If a provider call has no conditional precondition, a concurrent change outside your system cannot be prevented | Serialize per operation on your side, reconcile after every mutation, and let the provider's authoritative state win |
A Lower-Risk Rollout: Mandatory Go/No-Go Controls
Key takeaway: do not score your way to production. Every control below is required. One "no" is a stop, regardless of how many others pass.
Scoring models tempt teams to trade a missing control against a strong one. A missing server verification step cannot be offset by excellent CSP headers. Treat the list as all-required gates, and have someone other than the author of the integration sign off each one with evidence.
Go/no-go control list
| # | Control (all required) | Evidence to attach |
|---|---|---|
| 1 | No code path marks a document signed based on a browser callback, message, or return-URL parameter. | Code review note; negative test where a forged callback leaves state unchanged |
| 2 | postMessage listeners check exact origin, source window, and message shape. | Unit test with a wrong-origin message |
| 3 | Return URLs are exact-match allowlisted; the page resolves the operation from the authenticated session. | Test with a tampered query string and a different user's session |
| 4 | Signing URLs are excluded from logs, analytics, and error reports, and served with no-referrer and no-store. | Log search; response header capture |
| 5 | Every operation has a durable, tenant-scoped key bound to an immutable payload hash; a reused key with a different hash is rejected. | Test: same key, changed payload returns a conflict |
| 6 | Work is claimed with a conditional update; concurrent workers cannot both proceed. | Concurrency test with parallel workers |
| 7 | Webhook and hint recording, deduplication, and job scheduling commit atomically before 2xx is returned. | Fault test: kill the process between steps; no signal is lost |
| 8 | Webhook authenticity is verified using the provider's documented mechanism before any processing. | Test with an unsigned and a tampered payload |
| 9 | An ambiguous timeout on a consequential mutation moves to UNKNOWN_OUTCOME and reconciles before any retry. | Failure injection: timeout after provider acceptance produces one request, not two |
| 10 | Declined, expired, and voided outcomes reach terminal states with no automatic resend. | State-transition tests for each terminal outcome |
| 11 | State transitions are conditional writes on the target store; downstream unlock uses an outbox and a consumer-side idempotency key. | Duplicate-delivery test: downstream action runs once |
| 12 | A named owner exists for retention, access, and deletion of signer identifiers, webhook payloads, and signed artifacts, confirmed with legal and privacy teams. | Documented ownership and retention decision |
Roll out in two phases
- Shadow mode. Build the verification pipeline first, but leave the old behavior in charge. Log every case where a browser hint disagrees with authoritative state. That mismatch log is your evidence for how often Tier 1 signals would have been wrong, and it comes from your own traffic rather than from anyone's benchmark.
- Enforce mode. Switch unlock decisions to verified state, keep the reconciliation sweeper running, and alert on operations stuck in
UNKNOWNorVERIFYINGbeyond a threshold you set.
Vendor claims versus your obligations
When you evaluate any provider, separate three things. First, vendor statements about security and compliance, such as the ISO 27001, SOC 2 Type II, HIPAA, and GDPR claims eSignly makes on its embedded widget page and describes on its security page; ask for current attestations and read their scope. Second, compliance-support features, such as audit trails and access controls, which help you meet obligations but do not satisfy them on their own. Third, your own legal obligations, which depend on your industry, jurisdiction, and use case, and which a vendor's certifications do not discharge. A signed-document audit trail from the provider is useful evidence; the integrity of your decision to unlock is evidence you must produce yourself.
Update note: as of 2026, embedded flows still rest on the same browser primitives (framing policy, message events, redirects) and the same server-side reliability patterns described here. Provider event names, allowed-origin settings, and webhook mechanics can change, so recheck the official documentation at each release rather than relying on any article, including this one.
Prototype the verify step before you commit
Build and break the embedded flow in a sandbox first. Forge a callback. Kill a worker. Time out a call. See what your state machine does.
Start with a free eSignly API plan and test your controls end to end.
Free TrialConclusion: Decide What Your Server Is Willing to Believe
Embedded signing is a sound pattern when the server, not the browser, decides what happened. The decision for your team is not whether to embed, but which signals are allowed to change state. Five concrete actions:
- Inventory your unlock actions. List everything that happens after a signature (access, provisioning, payment, record changes) and mark which ones are consequential.
- Remove every Tier 1 state change. Search your codebase for any endpoint that accepts a client-supplied "signed" status or document ID, and replace it with a verification request keyed to the authenticated session.
- Add the operation and inbox tables. Create tenant-scoped operation keys with payload hashes, an inbound signal table with a tenant-scoped unique key, and conditional state transitions with an outbox.
- Inject failures before launch. Test forged callbacks, wrong-origin messages, timeouts after provider acceptance, duplicate webhooks, and cross-tenant ID collisions.
- Assign privacy ownership. Name who owns signer identifiers, signing URLs, webhook payloads, and signed artifacts, and agree retention rules with legal and privacy counsel.
The right level of rigor depends on what a wrong "signed" decision would cost you. For a low-stakes acknowledgment, a lighter variant of this design may be defensible. For anything that moves money, grants access, or creates a contractual record, treat the controls above as required.
About this article
eSignly has provided online eSignature and eSignature API services since 2014 and states that it holds ISO 27001, SOC 2 Type II, HIPAA, GDPR, 21 CFR Part 11, and PCI DSS compliance or certification; confirm current scope and attestations with eSignly directly before relying on them for your own compliance needs. Certifications describe the vendor's controls and do not replace your own legal analysis. Reviewed by the eSignly Expert Team.
Frequently Asked Questions
Can I trust the iframe or widget completion callback to mark a contract as signed?
No, not for any consequential action. A completion callback runs in the signer's browser, so it can be missed, repeated, or forged. Use it to update the interface and to ask your server to verify. Your server should then confirm the document's state with the provider through an authenticated server-to-server call before changing business state.
Is a webhook enough, or do I still need to verify with an API call?
A webhook that passes the provider's documented authenticity check is a much stronger signal than a browser hint, but it can still be duplicated, delayed, reordered, or lost. The proposed design treats it as an authenticated trigger and then confirms state with a fetch. If your risk profile allows relying on the webhook alone, document that decision and keep a reconciliation sweeper for missed events.
What should I do when the create-signing-session call times out?
Do not retry blindly. A timeout means you do not know whether the provider created the request. Mark the operation as unknown, check authoritative state using whatever lookup the provider supports (such as a client reference or a filtered list), and only re-issue under the same operation key once you can confirm none exists. If the provider cannot confirm either way, escalate for review.
- Unknown means completion could not be confirmed.
- It is not success and it is not verified failure.
How do I make idempotency safe in a multi-tenant integration?
Include the tenant in every operation key and every unique constraint, bind the key to a hash of the approved payload, and reject a reused key that arrives with a different hash. Claim work with a conditional database update so only one worker proceeds. Treat a corrected or re-sent document as a new, versioned operation that supersedes the earlier one rather than as a retry.
Which web security controls matter most for an embedded signing page?
- A CSP
frame-ancestorspolicy for your own pages and aframe-srcallowlist naming the exact signing origin. - Strict
postMessagehandling: checkorigin,source, and message shape. - Exact-match return URL allowlisting to avoid open redirects.
- A no-referrer policy and no-store caching wherever a signing URL is rendered.
Confirm the framing and origin settings your provider requires in its official documentation.
Does a vendor's SOC 2 or ISO 27001 status make my embedded integration compliant?
No. Vendor certifications describe the vendor's own controls and scope. Your integration, your data handling, and your legal obligations remain yours. Ask for current attestations, read their scope, and have your legal and compliance teams assess how they map to your specific use case and jurisdiction.
How should I handle declined, expired, or voided documents?
Model them as terminal states. When verification shows one of these outcomes, stop retries, notify the owner of the workflow, and do not automatically resend. If the business wants another attempt, create a new operation that references the old one so the history stays clear.
Ready to build embedded signing you can defend?
Callbacks are cheap. Verified state is what holds up under review. Test your flow against forged signals and failed calls before real contracts depend on it.
Try the eSignly API in a free sandbox, then pressure-test your verify-before-unlock design.
Free TrialResource Esignature Api Guide
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.
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.
For regulated, high-volume, or customer-facing workflows, validate legal duties, plan assumptions, and integration requirements with your internal stakeholders before rollout.

