How to Embed E-Signatures in a React App
1 install and 3 steps: allow-list your app origin, mint a per-recipient embed URL on your server with the TurboDocx SDK, then render <TurboSignForm> from @turbodocx/embed/react. The component frames the TurboSign signing page, trusts only messages from the origin you pin, and calls onCompleted when the signer finishes, so your customers sign without leaving your product.
By Yacine Kahlerras. Last updated: October 7, 2026. Covers @turbodocx/embed 0.2.0.
Key takeaways
- 1 package, 3 entry points: a React component, a framework-agnostic web component, and a pure message handler.
- 0 API keys in the browser: the embed URL is minted on your server, and the widget only receives that URL.
- 2 fail-closed guards: no pinned origin means every message is ignored, and an empty embedding allow-list means the page frames nowhere.
- 4 fields in every completion payload, scoped to 1 recipient, so multi-signer documents still need a server-side status check.
What is @turbodocx/embed?
@turbodocx/embed is TurboDocx's open-source (MIT) widget for embedded e-signature. It replaces the hand-rolled iframe plus window.addEventListener('message') code every app otherwise writes, and it does the security checks that code usually skips: it verifies the sender's origin and that the message came from its own iframe. Pick the entry point that matches your stack.
| Entry point | Import | Use it when |
|---|---|---|
| <TurboSignForm> | import { TurboSignForm } from '@turbodocx/embed/react' | React 18+ apps (Next.js, Vite, Remix). React is a peer dependency and never bundled. |
| <turbosign-form> | import '@turbodocx/embed' (auto-registers the element) | Plain HTML, Vue, Angular, Svelte, or any stack that renders DOM. No framework required. |
| handleTurboSignMessage | import { handleTurboSignMessage } from '@turbodocx/embed' | You already own the iframe and the message listener and only want the verified decision logic. |
How do I install it?
Install the widget in your front end and the TurboDocx SDK on your server. For the React component you also need React 18 or later, which the package lists as a peer dependency.
npm i @turbodocx/embed # front end
npm i @turbodocx/sdk # serverAllow-list your app origin
An org admin opens the E-Signature settings and adds your app's exact https origin (for example https://app.yourcompany.com) under Allowed embedding domains. The list is deny-by-default: while it is empty, browsers refuse to render the signing page in an iframe on any site. The full walkthrough is in How to Enable Embedded Signing.
Mint the embed URL on your server
Create the signature request and the embed URL in one server call. createEmbeddedSignature sends the document without signing emails and returns one embedUrl per recipient, in signing order. For a document you already created, call createSigningUrl instead; the createSigningUrl guide covers that path, plus redirect and new-tab placement.
// server.ts (Node.js + Express). Your API key never reaches the browser.
import express from 'express';
import { readFileSync } from 'fs';
import { TurboSign } from '@turbodocx/sdk';
TurboSign.configure({
apiKey: process.env.TURBODOCX_API_KEY,
orgId: process.env.TURBODOCX_ORG_ID,
senderEmail: process.env.TURBODOCX_SENDER_EMAIL,
});
const app = express();
app.use(express.json());
app.post('/api/signing-session', async (req, res) => {
const { name, email } = req.body; // the signed-in user in your app
const { documentId, recipients } = await TurboSign.createEmbeddedSignature({
file: readFileSync('service-agreement.pdf'),
documentName: 'Service Agreement',
recipients: [
{
name,
email,
auth: { emailOtp: true }, // optional: passcode before signing
fields: { signature: '{signature1}', date: '{date1}' },
},
],
});
res.json({ documentId, embedUrl: recipients[0].embedUrl });
});Verify the signer: each recipient can require an email or SMS passcode, an assertion from your own identity verification provider, or an explicit sender override. SMS passcodes depend on your plan. See e-signature identity verification. The same method exists in the JavaScript, Python, PHP, Java, and Go SDKs.
Render TurboSignForm and handle completion
Fetch the embed URL from your server and pass it to <TurboSignForm> with the TurboSign origin pinned. The example below is a complete signing step: loading state, the frame, and a completion handler that skips one-time side effects when a signer reopens a link they already completed.
// SigningStep.tsx (add 'use client' at the top in a Next.js App Router project)
import { useEffect, useRef, useState } from 'react';
import { TurboSignForm } from '@turbodocx/embed/react';
export function SigningStep({ user }: { user: { name: string; email: string } }) {
const [embedUrl, setEmbedUrl] = useState<string | null>(null);
const [done, setDone] = useState(false);
const started = useRef(false); // each POST creates a document, so request it once
useEffect(() => {
if (started.current) return;
started.current = true;
fetch('/api/signing-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user),
})
.then((r) => r.json())
.then((data) => setEmbedUrl(data.embedUrl));
}, [user.email]);
if (done) return <p>Thanks, you are all signed.</p>;
if (!embedUrl) return <p>Preparing your agreement...</p>;
return (
<TurboSignForm
embedUrl={embedUrl}
origin="https://app.turbodocx.com" // required: without it every message is ignored
height={720}
title="Sign your service agreement"
onCompleted={({ documentId, event }) => {
// "already_signed" means a reopened link: skip one-time side effects
if (event === 'signing_complete') {
fetch('/api/signing-complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ documentId }),
});
}
setDone(true);
}}
/>
);
}Props: embedUrl and onCompleted are required; origin is required for any message to be delivered. Optional props are height (default 720px), title (the iframe's accessible name), className, style, and the dev-only allowAnyOrigin. The listener is attached in a useEffect and removed on unmount.
What does onCompleted receive?
Every completion carries 4 fields. Treat it as a front-end signal for this signer; confirm the whole document on your server.
| Field | Value |
|---|---|
| documentId | The signing document's id, when the signing page includes it. |
| status | "completed". |
| event | "signing_complete" when the signer just finished, or "already_signed" when they reopened a link they had already completed. |
| scope | "recipient": this signer finished. Other signers on the document may still be pending. |
How does the widget fail closed?
The signing page posts its completion message to the parent window, so any page that frames it could also post a fake one. The widget defends against that in 2 layers, and both default to "deny".
- Origin and source pinning. Messages whose
event.origindoes not exactly matchoriginare ignored, and so are messages that do not come from the component's own iframe. Leaveoriginempty and every message is ignored, with a one-time console warning. - Embedding allow-list. The signing page only renders inside origins your admin allow-listed. An empty list denies framing everywhere.
For local development only, allowAnyOrigin (or the allow-any-origin attribute) accepts messages from any origin. It fails open, so never ship it. When an origin is also set, the origin wins.
Can I use it without React?
Yes. Loading the package's ES module (from a script tag or your bundle) registers a <turbosign-form> custom element that works in plain HTML, Vue, Angular, or Svelte. It renders a full-width iframe, re-emits turbosign:completed as a bubbling event, and removes its listener when it leaves the DOM. To register a different tag name, call defineTurboSignForm('my-tag').
<!-- Serve node_modules/@turbodocx/embed/dist/ with your static assets,
or import '@turbodocx/embed' from your bundled entry file instead. -->
<script type="module" src="/vendor/turbodocx-embed/index.js"></script>
<turbosign-form
embed-url="EMBED_URL_FROM_YOUR_SERVER"
origin="https://app.turbodocx.com"
height="720"
></turbosign-form>
<script>
document.querySelector('turbosign-form')
.addEventListener('turbosign:completed', (e) => {
console.log('Signed:', e.detail.documentId);
});
</script>If you already own the iframe, use the pure handler. It holds the same origin and source checks the components use, without rendering anything.
import { handleTurboSignMessage } from '@turbodocx/embed';
const iframe = document.querySelector('iframe#signing') as HTMLIFrameElement;
window.addEventListener('message', (event) => {
handleTurboSignMessage(event, {
expectedOrigin: 'https://app.turbodocx.com',
expectedSource: iframe.contentWindow, // reject forged messages from other frames
onCompleted: ({ documentId }) => console.log('Signed:', documentId),
});
});Want signing that fits your product without building it?
The widget is the fast path for a dev team. If you would rather have the whole flow (document generation, signer verification, the embedded step, and the write-back into your systems) built and maintained for you, our team does that as part of your subscription.
Book a demoRelated
Embed Signing with createSigningUrl
Mint signing URLs for existing documents, and use redirect or new-tab placement instead of an iframe.
Embedded E-Signature
What embedded signing looks like in your product, the integration options, and pricing.
Signer Identity Verification
Email and SMS passcodes, your own identity provider, or an explicit override, per recipient.
E-Signature API
Send, track, and download signed documents with the REST API and 5 server SDKs.
Resources
Frequently Asked Questions
How do I embed an e-signature in a React app?
3 steps. Allow-list your app origin in the TurboSign E-Signature settings, mint a per-recipient embed URL on your server with TurboSign.createEmbeddedSignature, then render <TurboSignForm embedUrl={embedUrl} origin="https://app.turbodocx.com" onCompleted={...} /> from @turbodocx/embed/react. The component frames the signing page, checks every message against the pinned origin and its own iframe, and calls onCompleted when the signer finishes.
Why does onCompleted never fire?
2 settings cause almost every case. First, the origin prop: if it is missing or empty, the component fails closed and ignores every message, with a one-time console warning. Set it to your TurboSign origin, for example https://app.turbodocx.com. Second, the embedding domain: if your app origin is not in Allowed embedding domains, the signing page will not render in the iframe at all, so there is nothing to complete.
Does onCompleted mean the whole document is signed?
1 recipient only. The completion payload carries scope: "recipient", which means this signer finished their step. On a multi-signer document other recipients may still be pending, so confirm the document status on your server with TurboSign.getStatus or listen for the signature.document.completed webhook before you treat the agreement as fully executed in your system.
Can I use @turbodocx/embed without React?
2 options ship in the same package. The <turbosign-form> custom element works in plain HTML, Vue, Angular, Svelte, or any framework that renders DOM, and dispatches a bubbling turbosign:completed event. For full control, keep your own iframe and call the pure handleTurboSignMessage function from your message listener, passing the expected origin and the iframe contentWindow.
Put signing inside your app this week
Create a free account, allow-list your origin, and render your first embedded signing step.