Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ec48f45
feat(js-sdk): embedded identity verification — createSigningUrl + rec…
nicolasiscoding Sep 16, 2026
d8129d4
docs(js-sdk): embedded identity-verification example
nicolasiscoding Sep 16, 2026
db24637
docs(js-sdk): clarify identityVerification is optional in the embedde…
nicolasiscoding Sep 16, 2026
5759dc2
fix(js-sdk): unwrap results in createSigningUrl; add getEmbeddedSigni…
nicolasiscoding Sep 16, 2026
a255a9f
test(turbosign): add real-wire-contract tests for embedded-identity m…
nicolasiscoding Sep 16, 2026
b5190aa
docs(turbosign): note browser-enforced frame-ancestors on the embedde…
nicolasiscoding Sep 17, 2026
67a6d1b
docs(sdk): add embedded-signing host web-app example (fake integrator)
nicolasiscoding Sep 17, 2026
68119ec
docs(sdk): add the embedded host page (index.html, force-added past *…
nicolasiscoding Sep 17, 2026
a2af7f3
docs(sdk): embedded-web-app example now collects signer email + does …
nicolasiscoding Sep 17, 2026
f783e55
fix(sdk): embedded-web-app example only sends returnUrl when the host…
nicolasiscoding Sep 17, 2026
443769e
docs(sdk): move embedded-web-app to the top-level examples/ folder + …
nicolasiscoding Sep 17, 2026
7ec60cc
chore(examples/embedded-web-app): load config from .env via dotenv
nicolasiscoding Sep 17, 2026
68d6645
feat(sdk): add TurboSign.createEmbeddedSignature one-call embedded si…
nicolasiscoding Sep 17, 2026
248ef03
feat(embed): add TurboSign embedded-signing widget and a second example
nicolasiscoding Sep 17, 2026
1231dd7
feat(sdk): make createEmbeddedSignature turn-aware for sequential sig…
nicolasiscoding Sep 17, 2026
42926ce
feat(examples): unified Vite/React/shadcn embedded-signing app (three…
nicolasiscoding Sep 18, 2026
18d1467
docs(embed): headless / custom-UI roadmap — the signing flow is alrea…
nicolasiscoding Sep 18, 2026
993fc06
fix(examples,embed): address code-review findings on the embedded-sig…
nicolasiscoding Sep 18, 2026
fff948c
feat(sdk): port embedded signing + identity verification to go/py/php…
nicolasiscoding Sep 18, 2026
b760f66
fix(sdk): resolve cross-language divergences from code-review of the …
nicolasiscoding Sep 18, 2026
96ad8aa
docs(sdk): add consistent embedded-signing examples for go/py/php/jav…
nicolasiscoding Sep 18, 2026
10ae791
fix(ruby-sdk): accept snake_case auth keys (email_otp) in embedded si…
nicolasiscoding Sep 18, 2026
d651f33
chore(embed): tsconfig module node20; feat(example): lead with the Wi…
nicolasiscoding Sep 18, 2026
6977c3d
feat(turbosign): external IdV enrichment fields + Identity Verificati…
nicolasiscoding Sep 20, 2026
11a2c81
fix(example): commit the Vite entry page; stop claiming the OTP code …
amitsharma-turbodocx Oct 1, 2026
90b5550
feat(sdk): bring embedded signing in line with the API across all SDKs
amitsharma-turbodocx Oct 1, 2026
5a790a3
feat(example): guided identity check for the External IdV path
Oct 5, 2026
4e873b5
fix(example): make the identity check's close button visible on the d…
Oct 5, 2026
5331212
fix(js-sdk): document single-use-URL recipients, OTP channel default …
Oct 6, 2026
52ead82
docs(py-sdk): document single-use-URL recipients, OTP channel default…
Oct 6, 2026
fbee210
docs(go-sdk): document single-use-URL recipients, OTP channel default…
Oct 6, 2026
1885571
docs(php-sdk): document single-use-URL recipients, OTP channel defaul…
Oct 6, 2026
0fe7e21
docs(java-sdk): document single-use-URL recipients, OTP channel defau…
Oct 6, 2026
a138171
docs(ruby-sdk): document single-use-URL recipients, OTP channel defau…
Oct 6, 2026
a9b7380
docs: list the 403 and 409 error classes and the single-use-URL remin…
Oct 6, 2026
ea1e41d
docs(sdk): use generic example identifiers and fix PHP resend docs (a…
Oct 6, 2026
3b81252
feat(embed): fail closed without an origin, verify message source, ad…
Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .claude/rules/cross-sdk-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,32 @@ All SDKs must implement the same operations. When adding a feature to one SDK, i
| resend | `resend()` | `resend_email()` | `ResendEmail()` | `resend()` | `resendEmail()` | `resend_email()` |
| getAuditTrail | `getAuditTrail()` | `get_audit_trail()` | `GetAuditTrail()` | `getAuditTrail()` | `getAuditTrail()` | `get_audit_trail()` |
| sendReminder | `sendReminder()` | `send_reminder()` | `SendReminder()` | `sendReminder()` | `sendReminder()` | `send_reminder()` |
| createSigningUrl | `createSigningUrl()` | `create_signing_url()` | `CreateSigningURL()` | `createSigningUrl()` | `createSigningUrl()` | `create_signing_url()` |
| getEmbeddedSigningSettings | `getEmbeddedSigningSettings()` | `get_embedded_signing_settings()` | `GetEmbeddedSigningSettings()` | `getEmbeddedSigningSettings()` | `getEmbeddedSigningSettings()` | `get_embedded_signing_settings()` |
| createEmbeddedSignature | `createEmbeddedSignature()` | `create_embedded_signature()` | `CreateEmbeddedSignature()` | `createEmbeddedSignature()` | `createEmbeddedSignature()` | `create_embedded_signature()` |

**Embedded signing notes:** `createSigningUrl` posts `{ recipientId | externalId (exactly one),
returnUrl? (https), identityAssertion? }` and unwraps `{ data: { results } }`. The API rejects unknown
keys (no `senderName` here). An identity assertion has four required keys plus six optional ones
(`method`, `methodDetail` — required when method is `other` — `assuranceLevel`, `verifiedName`,
`evidenceUrl`, `overrideEmailMatching`); typed SDKs omit unset optional keys from the wire.
`getEmbeddedSigningSettings` returns `enabled`, `allowExternalIdv`, `allowIdentityOverride`,
`defaultChannel`, `allowChannelOverride` and `allowedFrameAncestors` (empty = framing denied
everywhere); typed SDKs keep `allowChannelOverride` nullable so an API that doesn't report it reads
as unknown, not locked. `createEmbeddedSignature` defaults `sendEmail` to false, never sends an
explicit `otpChannel` (an omitted channel takes the org default), and treats `RecipientNotInTurn` /
`NotSignersTurn` / `RecipientAlreadySigned` as `pending` / `completed` results, not errors.

**sendReminder note:** a standalone nudge, deliberately decoupled from the automatic reminder
schedule — it ignores the configured cadence, works when reminders are disabled or the per-signer
cap is spent, and does not consume that cap. Only CURRENT-signing-order signers are emailed; a
later-order or already-signed recipient comes back as a `skipped_*` result rather than being
dropped. `recipientIds` is optional (omit to remind everyone eligible); when supplied the API is
all-or-nothing. Every SDK omits the key entirely for an empty list — the API requires at least one
id when the key is present, so sending `[]` would guarantee a 400.
id when the key is present, so sending `[]` would guarantee a 400. `external_idv` and `override`
recipients sign only through a single-use `createSigningUrl` link and are never emailed: remind
reports them as `skipped_requires_single_use_url`, resend skips them, and naming only such
recipients in either call is a 409 (conflict class) with code `RecipientRequiresSingleUseUrl`.

**Schedule overrides:** both send paths (`createSignatureReviewLink`, `sendSignature`) accept the
eight per-document reminder/expiration fields. Two rules every SDK follows:
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ jobs:
- name: Run tests
run: npm test

test-embed:
name: Test Embed Widget
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/embed
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

- name: Run tests
run: npm test

test-py:
name: Test Python SDK
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ target/
*.pdf
*.html
!ExampleAssets/**
# The Vite example app's entry page is source, not a generated file.
!examples/embedded-web-app/index.html
convert-template.js
generate-all-pdfs.js
test-php-sdk/
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,14 @@ await TurboSign.void(documentId, 'Contract terms changed');
await TurboSign.resend(documentId, ['recipient-uuid']);
```

### Embedded signing (host web-app)

Embed the signing page in your own app instead of emailing a link: mint a signing URL server-side with
`createSigningUrl`, frame it, and (optionally) verify the signer with email/SMS OTP or your own IdP. See
the full runnable example — a host web-app that collects the signer's email, does email OTP (or a
simulated external identity verification), and completes via postMessage — in
[`examples/embedded-web-app`](./examples/embedded-web-app).

### Field Types

| Type | Description |
Expand Down
4 changes: 3 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Each SDK wraps a single `HttpClient` class responsible for:
1. **Authentication**: Bearer token via `apiKey` or `accessToken`, sent in `Authorization` header. Org ID sent as `x-rapiddocx-org-id` header.
2. **Base URL**: Configurable, defaults to `https://api.turbodocx.com`. Env var fallback: `TURBODOCX_BASE_URL`.
3. **Response unwrapping**: Backend wraps responses in `{ "data": ... }`. The client auto-unwraps when the response has only a `data` key (smart unwrap).
4. **Error mapping**: HTTP status codes map to typed errors (400→Validation, 401→Auth, 404→NotFound, 429→RateLimit, other→TurboDocxError).
4. **Error mapping**: HTTP status codes map to typed errors (400→Validation, 401→Auth, 403→Authorization, 404→NotFound, 409→Conflict, 429→RateLimit, other→TurboDocxError).
5. **File upload**: Multipart form upload with magic-byte file type detection.

### Partner Client
Expand All @@ -40,8 +40,10 @@ All SDKs follow: **explicit config > env var fallback > error**.
```
TurboDocxError (base)
├── AuthenticationError (401)
├── AuthorizationError (403)
├── ValidationError (400)
├── NotFoundError (404)
├── ConflictError (409)
├── RateLimitError (429)
└── NetworkError (no status — fetch/connection failure)
```
Expand Down
11 changes: 11 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# TurboDocx examples

Runnable, self-contained examples that show a full integration end to end (beyond the per-language
snippets in each `packages/<lang>-sdk/examples/`).

| Example | What it shows |
|---|---|
| [`embedded-web-app`](./embedded-web-app) | Embedded TurboSign signing in a **Vite + React + shadcn** app with **four paths** in one place: **Single signer** (hand-rolled `<iframe>` + `postMessage` listener), **External IdV** (your identity provider verifies the signer, simulated, no passcode), **Sequential kiosk** (two signers in order on one device — turn-aware), and **Widget** (the drop-in `<TurboSignForm>` from [`@turbodocx/embed`](../packages/embed)). All four go through a **key-safe backend-for-frontend** (`server.ts`): the browser calls `/api/*`, the server holds the API key and uses `@turbodocx/sdk` to mint the embed URLs. Verified with email OTP. |
| [`partner-preferences`](./partner-preferences) | Reading and setting a partner/customer organization's preferences (JS + Python). |

Each example has its own README with run instructions and prerequisites.
24 changes: 24 additions & 0 deletions examples/embedded-web-app/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copy to `.env` and fill in. `.env` is gitignored — never commit real credentials.
#
# Read by server.ts (the backend-for-frontend) via dotenv. The API key stays SERVER-SIDE and never
# reaches the browser — the React SPA only ever calls this server's /api/* routes.

# Required — a TurboDocx API key for an org with embedded signing enabled.
TURBODOCX_API_KEY=your-api-key-here

# Required — the TurboDocx org id.
TURBODOCX_ORG_ID=your-org-id-here

# Required — reply-to address, recorded as the sender in the audit trail.
TURBODOCX_SENDER_EMAIL=you@yourcompany.com

# Optional — sender display name shown to signers and in the audit trail.
TURBODOCX_SENDER_NAME=Embedded Signing Demo

# Optional — defaults to TurboDocx production. Set only for local testing against a dev backend
# (e.g. http://localhost:3000), and allow this app's origin in that backend's CORS.
# TURBODOCX_API_URL=http://localhost:3000

# Optional — port for server.ts (the /api backend). Default 4000. The Vite dev proxy targets this
# same value, so change both together via this one var.
# PORT=4000
6 changes: 6 additions & 0 deletions examples/embedded-web-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
dist
dist-ssr
*.local
.env
.vite
94 changes: 94 additions & 0 deletions examples/embedded-web-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Embedded signing — Vite + React + shadcn (four paths)

A single host web-app that embeds TurboSign four ways, so you can compare the approaches side by side:

- **Single signer** — the host hand-rolls the `<iframe>` and its own origin-checked
`window.addEventListener("message", …)` completion listener.
- **External IdV** — your own identity provider verifies the signer (simulated here by an "Identity
Verification Simulator" dialog), and the server passes that verification as an `identityAssertion`
when it mints the URL, so the signer skips the passcode. Requires the org to allow external identity
verification.
- **Sequential kiosk** — one document, two signers **in order** on the same device. The next signer's
URL is minted **just-in-time** when it's their turn (turn-aware), with a short retry to ride out the
moment right after the previous signer completes.
- **Widget** — the same flow, but the page drops in `<TurboSignForm>` from
[`@turbodocx/embed`](../../packages/embed), which owns the iframe, origin pinning, and completion
event for you.

## The key stays on the server

This is the pattern real integrations use in production:

```
React SPA ──/api/*──▶ server.ts (holds the API key) ──▶ TurboDocx
```

The browser **never** imports the SDK or sees the API key. `server.ts` is a tiny backend-for-frontend
that holds the key and uses `@turbodocx/sdk` to create documents and mint embeddable signing URLs; the
SPA calls `/api/single`, `/api/external-idv`, `/api/kiosk/start`, `/api/kiosk/next` and just frames the
URLs it gets back
(or hands them to the widget). In dev, Vite proxies `/api` to `server.ts`.

## Run

Two processes. Configure the server's `.env` first (copy the template, fill in your creds):

```bash
cd examples/embedded-web-app
cp .env.example .env # set TURBODOCX_API_KEY + TURBODOCX_ORG_ID (see the file)
npm install

# terminal 1 — the key-holding backend (default :4000)
npm run server

# terminal 2 — the SPA (Vite, :5173, proxies /api → :4000)
npm run dev
```

Then open <http://localhost:5173> and pick a path.

`.env` documents every variable. At minimum set `TURBODOCX_API_KEY`, `TURBODOCX_ORG_ID`, and
`TURBODOCX_SENDER_EMAIL`. `TURBODOCX_API_URL` defaults to TurboDocx production; set it (e.g.
`http://localhost:3000`) only when testing against a dev backend. Your real `.env` is gitignored.

## Prerequisite: allow-list this app's origin

Embedded signing is **default-deny**. The browser refuses to render the iframe unless the signer's org
lists this app's origin in its embedded-signing allowed origins. For this demo add:

```
http://localhost:5173
```

in **TurboDocx → E-Signature settings → Identity & embedding → Allowed embedding domains**. Production origins
must be `https://`; `http://localhost` is accepted only as a clearly-flagged dev-only override. Without
an allow-listed origin you get a blank/refused frame — that's the clickjacking protection
(`frame-ancestors`) working, not a bug.

## Tests

```bash
npm test
```

`handlers.test.ts` covers the server handlers (SDK mocked): single/external-IdV/kiosk mapping, the turn-aware
`ready`/`pending` split, and the null-URL guard. `src/lib/turbosign.test.ts` covers the frontend API
client (fetch mocked): its endpoints, error propagation, and the kiosk mint retry.

## What to notice

- **The API key never reaches the browser** — only `server.ts` talks to TurboDocx.
- **Email OTP is the identity step** (single signer, kiosk, widget) — the signer clicks "Send Code" in
the signing panel, then enters the 6-digit code before the document opens. The External IdV path
replaces the passcode with your provider's verification.
- **No signing-link emails** — signers are not emailed a link (`sendEmail: false`, the
`createEmbeddedSignature` default); the passcode and completed-copy emails still go out.
- **Your org's verification policy still applies** — if the org verifies every request (a default
channel other than `none`), a recipient that doesn't ask for verification gets the org default, and
asking for a different channel when the org locked the method (`allowChannelOverride: false` in
`getEmbeddedSigningSettings()`) fails with `OtpOverrideNotAllowed`.
- **Completion is push** — the single-signer path listens for the `turbosign:completed` postMessage;
the widget surfaces it as an `onCompleted` callback. In production, pin the listener to your known
TurboSign origin and/or confirm via the `completed` webhook.
- **The kiosk mints just-in-time** — a later signer's URL is created only when it's their turn; the
backend enforces the order.
21 changes: 21 additions & 0 deletions examples/embedded-web-app/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
Loading
Loading