All articles
embedesignaturereactdeveloperapi

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.

Signvoy TeamJuly 3, 2026

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

  1. Server-side: mint a scoped signing URL using your API key
  2. Client-side: render the URL in an <iframe> (or <SignvoyEmbed />)
  3. React to events: listen for postMessage completion/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.

typescript
// 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:

typescript
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 />

tsx
"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):

typescript
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:

html
<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 postMessage listener always checks event.origin — don't skip this check.
  • For server-side confirmation, always verify the HMAC signature on incoming webhooks.

Getting started

  1. Create a Signvoy account (free)
  2. Create a template and send a document
  3. Call POST /v1/embed/session with a valid documentId + documentRecipientId
  4. Install @signvoy/react and 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