Embed guide
In-app signing with
one component
Render the complete Signvoy signing experience directly inside your product — no redirect, no pop-up. Your server mints a scoped URL, you drop in <SignvoyEmbed url={url} />, and listen for events.
How it works
The embed flow is two steps: your backend mints a short-lived URL, your frontend renders it in an iframe.
Mint a session
Your server calls POST /v1/embed/session with a documentId and documentRecipientId. Signvoy returns a scoped signing URL that expires in 1 hour (or up to 24 h).
Return the URL to your frontend
Pass the URL from your API response to the browser. Never include your Signvoy API key in client-side code.
Render <SignvoyEmbed />
Drop <SignvoyEmbed url={url} /> into your React tree. It renders a sandboxed iframe and wires up postMessage listeners automatically.
Handle events
React to onCompleted, onDeclined, and onError callbacks to update your UI, redirect the user, or trigger downstream workflows.
Install @signvoy/react
The @signvoy/react package ships the <SignvoyEmbed /> component plus full TypeScript types. Install it in your React / Next.js project:
# npm
npm install @signvoy/react
# pnpm
pnpm add @signvoy/react
# yarn
yarn add @signvoy/reactRequires React 18+ and a browser environment. The package has no server-side rendering restrictions — it marks the component as "use client" internally.
Create an embed session
Call POST /v1/embed/session from your server (never from the browser — it requires your API key). The document must be in sent state and the recipient must be pending.
curl -X POST https://api.signvoy.com/v1/embed/session \
-H "Authorization: Bearer $SIGNVOY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"documentId": "doc-uuid",
"documentRecipientId": "recipient-uuid",
"expiresInSeconds": 3600
}'Response:
{
"url": "https://signvoy.com/sign/abc123?embed=1",
"expiresAt": "2026-07-03T11:45:00Z",
"documentId": "doc-uuid",
"documentRecipientId": "recipient-uuid"
}TypeScript — Next.js Server Action example:
"use server";
interface EmbedSessionResponse {
url: string;
expiresAt: string;
documentId: string;
documentRecipientId: string;
}
export async function createEmbedSession(
documentId: string,
recipientId: string,
): Promise<EmbedSessionResponse> {
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, documentRecipientId: recipientId }),
cache: "no-store",
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message ?? `Embed session failed: ${res.status}`);
}
return res.json() as Promise<EmbedSessionResponse>;
}Render <SignvoyEmbed />
Pass the URL from your server action to the component. Size the wrapper to fit your layout — the iframe fills 100 % of its container.
"use client";
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import { SignvoyEmbed } from "@signvoy/react";
interface Props {
embedUrl: string;
documentId: string;
}
export function SigningModal({ embedUrl, documentId }: Props) {
const router = useRouter();
const handleCompleted = useCallback(() => {
// Redirect to a confirmation page when signing is done
router.push(`/documents/${documentId}/signed`);
}, [router, documentId]);
const handleDeclined = useCallback(({ reason }: { reason?: string }) => {
console.warn("Signing declined:", reason);
// Show a toast, update state, etc.
}, []);
return (
<div style={{ width: "100%", height: "600px" }}>
<SignvoyEmbed
url={embedUrl}
onCompleted={handleCompleted}
onDeclined={handleDeclined}
onError={(e) => console.error("Embed error:", e.message)}
/>
</div>
);
}The component renders a sandboxed <iframe> with allow="camera; clipboard-write" and a tightly scoped sandbox attribute — no cross-origin data leaks.
postMessage events
The signing frame communicates with your page via window.postMessage. Each message has a type string and a payload object. <SignvoyEmbed /> handles the listener boilerplate — just pass callbacks.
Vanilla iframe (no React SDK)
If you're not using React, embed the URL in a plain <iframe> and listen for messages manually:
const iframe = document.createElement("iframe");
iframe.src = embedUrl; // from your server
iframe.style.width = "100%";
iframe.style.height = "600px";
iframe.style.border = "none";
iframe.allow = "camera; clipboard-write";
iframe.sandbox.add(
"allow-scripts",
"allow-same-origin",
"allow-forms",
"allow-popups",
"allow-top-navigation",
);
document.getElementById("signing-container")?.appendChild(iframe);
// Listen for Signvoy events
window.addEventListener("message", (event) => {
// Only accept messages from the iframe origin
if (event.origin !== new URL(embedUrl).origin) return;
const { type, payload } = event.data ?? {};
if (type === "signvoy:completed") {
console.log("Signed!", payload);
} else if (type === "signvoy:declined") {
console.warn("Declined:", payload?.reason);
}
});Security checklist
Never mint sessions client-side
The /v1/embed/session endpoint requires your API key — call it from a server action, API route, or backend service only.
Validate the postMessage origin
Always check event.origin === the signing frame's origin before processing messages. <SignvoyEmbed /> does this for you.
Set a tight expiry
Use expiresInSeconds to match your UX flow. Default is 3 600 s (1 h). Maximum is 86 400 s (24 h). Expired URLs show an error page.
One URL per page load
Mint a fresh session for each user session or page load. Reusing a URL after completion results in an error — the signing token is consumed.
sk_live_… API keys in browser bundles, client components, or public repositories. Rotate any key that may have been leaked from the Workspace Settings → API Keys page immediately.More resources
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
