Developer Guide

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 pointImportUse 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.
handleTurboSignMessageimport { 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.

terminal
npm i @turbodocx/embed    # front end
npm i @turbodocx/sdk      # server
1

Allow-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.

2

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

3

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

FieldValue
documentIdThe 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.origin does not exactly match origin are ignored, and so are messages that do not come from the component's own iframe. Leave origin empty 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').

index.html
<!-- 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.

signing.ts
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 demo

Related

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.