A practical guide to embedding document signing directly in your SaaS application. Compare iframe embed approaches across DocuSign, Documenso, and Signvoy — with working React code.
Redirecting users to a third-party signing page is fine for simple workflows. But when signing is a core part of your product — an onboarding flow, a contract approval step, a customer-facing portal — you want the experience to live inside your own UI, not someone else's.
This guide covers how to embed e-signature collection in your own web application using the Signvoy API.
Why embed instead of redirect?
- Conversion: users who leave your app to sign don't always come back
- Brand consistency: your design language, your colours, your domain
- Completion rate: embedded flows show measurably higher signing rates than redirect-based ones (documented by DocuSign's own data from 2023)
- PostMessage events: react to completion or decline in real time without polling
The three-step embed pattern
- Server-side: mint a scoped signing URL using your API key
- Client-side: render the URL in an
<iframe>(or<SignvoyEmbed />) - React to events: listen for
postMessagecompletion/decline events
Step 1 — Mint an embed session
Call this from your backend. Never call the Signvoy API directly from the browser — your API key would be exposed.
// With the @signvoy/node SDK (recommended)
import Signvoy from "@signvoy/node";
const signvoy = new Signvoy({
apiKey: process.env.SIGNVOY_API_KEY!,
workspaceId: process.env.SIGNVOY_WORKSPACE_ID!,
});
const session = await signvoy.embed.createSession({
documentId: "doc-uuid",
documentRecipientId: "recipient-uuid",
expiresInSeconds: 3600,
});
// session.url is safe to return to your frontend — single-use, time-limited
Or with a plain fetch call if you prefer not to add the dependency:
const res = await fetch("https://api.signvoy.com/v1/embed/session", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SIGNVOY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
documentId: "doc-uuid",
documentRecipientId: "recipient-uuid",
expiresInSeconds: 3600,
}),
});
const { url } = await res.json();
Step 2 — Render <SignvoyEmbed />
"use client";
import { useRouter } from "next/navigation";
import { SignvoyEmbed } from "@signvoy/react";
export function DocumentSigningStep({ embedUrl }: { embedUrl: string }) {
const router = useRouter();
return (
<div className="h-full w-full rounded-xl overflow-hidden border border-gray-200">
<SignvoyEmbed
url={embedUrl}
onReady={() => console.log("Signing UI loaded")}
onComplete={(e) => {
console.log("Signed! Document:", e.documentId);
router.push("/onboarding/next-step");
}}
onDecline={(e) => {
console.log("Declined:", e.reason);
router.push("/onboarding/declined");
}}
onError={(e) => console.error("Embed error:", e.message)}
/>
</div>
);
}
The component handles all the postMessage wiring for you. The callbacks map directly to the events the iframe emits: onReady fires once the signing UI loads, onComplete fires when all fields are signed, onDecline fires if the recipient clicks Decline.
Step 3 (optional) — Listen for webhooks server-side
For server-side confirmation (don't rely solely on postMessage — the browser tab may close):
import { verifyWebhookSignature } from "@signvoy/node/webhooks";
app.post("/webhooks/signvoy", express.raw({ type: "*/*" }), async (req, res) => {
const valid = verifyWebhookSignature(
req.body,
req.headers["signvoy-signature"] as string,
process.env.SIGNVOY_WEBHOOK_SECRET!,
);
if (!valid) return res.status(401).send("Bad signature");
const event = JSON.parse(req.body.toString());
if (event.type === "document.completed") {
await markOnboardingComplete(event.data.documentId);
}
res.sendStatus(200);
});
Plain iframe (no React)
If you're not using React, you can embed directly:
<iframe
src="<your-embed-url>"
style="width: 100%; height: 100%; border: none;"
allow="camera; clipboard-write"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
></iframe>
<script>
window.addEventListener("message", (event) => {
if (event.origin !== "https://signvoy.com") return;
if (event.data.type === "signvoy:completed") {
// handle completion
}
});
</script>
Security notes
- The embed session URL is scoped to a single recipient and expires after your configured window (default 1 hour).
- It is safe to pass to the client — it cannot be used to access other documents or recipients.
- The
postMessagelistener always checksevent.origin— don't skip this check. - For server-side confirmation, always verify the HMAC signature on incoming webhooks.
Getting started
- Create a Signvoy account (free)
- Create a template and send a document
- Call
POST /v1/embed/sessionwith a validdocumentId+documentRecipientId - Install
@signvoy/reactand render<SignvoyEmbed />
Full API docs at signvoy.com/developers/reference.
Stop chasing signatures
Join thousands of teams using Signvoy to close deals, onboard clients, and sign contracts — faster.
No credit card · Free forever plan · 14-day Pro trial
