E-Signature API Tutorial: Sign PDFs in Node.js, Python, PHP, Go, Java & Ruby
Almost every SaaS app eventually needs a signature: a contract, an NDA, an onboarding form, a statement of work. Building it yourself means field placement on PDFs, signer emails, ordering, tamper-proof audit trails, and a legal story you'd rather not own.
An e-signature API (you'll also see it called an electronic signature API or digital signature API) turns that into three calls:
- Send a PDF to one or more signers
- Get notified when they sign (webhook)
- Download the signed PDF
You can get there two ways:
- The shortcut: install one agent skill and let your AI coding agent do the setup (next section)
- By hand: sign PDFs from code with the SDK in Node.js, TypeScript, Python, PHP, Go, Java, and Ruby (the rest of this post)
The shortcut: a Claude Code skill (also works in Cursor and Copilot)
If you already work with an AI coding agent, you can skip the docs entirely. We publish an open-source Agent Skill that teaches your agent how to install and wire up the SDK for your project:
npx skills add TurboDocx/quickstart The installer is the open-source skills CLI. It asks which skills you want and writes them where your agent looks for them (.claude/skills/, .cursor/skills/, .github/skills/, and so on). You need Node.js 18+ for the installer only; the skill itself targets whatever language your project uses.
It works with Claude Code, Cursor, GitHub Copilot, OpenCode, OpenAI Codex CLI, and Gemini CLI, since it follows the open Agent Skill spec instead of tying you to one vendor.
You get two skills in one install:
-
/turbodocx-sdk: e-signatures, signature webhooks, embedded signing, document generation, and quotes in JavaScript/TypeScript, Python, Go, PHP, Java, or Ruby -
/turbodocx-html-to-docx: HTML to Word documents in any Node.js project (Express, Next.js, Fastify, NestJS, or plain Node)
On Claude Code you can also install it as a plugin with claude plugin add TurboDocx/quickstart.
After that, just ask in plain English:
Add TurboSign e-signatures to my Express app Add a /signatures/:id/download endpoint that returns the signed PDF Or skip the questions and go straight to what you need:
/turbodocx-sdk turbosign # send for signature, status, download /turbodocx-sdk turbowebhooks # webhook subscription and a verified handler Here's what the skill does, in order, so there's no magic involved:
- Detect: reads
package.json,pyproject.tomlorrequirements.txt,go.mod,composer.json,pom.xmlorbuild.gradle, orGemfileto find your language, framework, and package manager before touching a file - Ask: checks what you want (TurboSign for signatures, Deliverable for document generation, or both). Webhooks, quotes, and partner provisioning are opt-in
- Install: runs the right command for your package manager (npm, pnpm, yarn, bun, pip, poetry, pipenv, go get, composer, Maven, Gradle, Bundler)
- Generate: writes a config file and route handlers that follow your project's patterns, wires them into your app, and reads your API key from
.envinstead of hardcoding it - Verify: checks the files exist and compile (running
tscfor TypeScript), then lists what it created, which env vars to fill in, and a curl command to test the first endpoint
If you don't have an API key yet, grab one from the free plan at app.turbodocx.com and paste it into your .env.
Review what your agent writes. Agent-generated code is a starting point, not a finished feature. In particular, if you ask for a webhook handler, check it against the raw-body rule in Step 2 below. That's the part people (and agents) most often get subtly wrong.
The skill is MIT licensed and lives at github.com/TurboDocx/quickstart.
Prefer to do it by hand? Here's the whole flow
Pick your language, copy, run. Each block installs that language's e-signature SDK and sends a PDF for signature. It's the same SDK code the skill wires into your app, so it's worth reading once to know what you're shipping.
Setup (same for every language)
- Create a free account at app.turbodocx.com
- Go to Settings → API Keys and copy your API key and Organization ID
- Put the credentials in your environment:
TURBODOCX_API_KEY=your_api_key_here TURBODOCX_ORG_ID=your_org_id_here TURBODOCX_SENDER_EMAIL=you@company.com # required, used as the reply-to on signing emails TURBODOCX_SENDER_NAME=Your Company Name TURBODOCX_WEBHOOK_SECRET=your_webhook_secret_here # you get this in Step 2, when you create the webhook How field placement works
You don't need pixel coordinates. Put an anchor like {signature1} in your PDF where the signature should go, then tell the API to replace it:
{ "type": "signature", "recipientEmail": "john@example.com", "template": { "anchor": "{signature1}", "placement": "replace", "size": { "width": 100, "height": 30 } } } If you regenerate the contract and the layout shifts, the field moves with the text. Same idea works for initials, dates, text boxes, and checkboxes. If editing anchor text into PDFs gets annoying, skip ahead to drag and drop with signer roles.
Step 1: Send a PDF for signature with the e-signature API
Node.js (JavaScript)npm install @turbodocx/sdk Requires Node.js 18+.
import { TurboSign } from '@turbodocx/sdk'; import { readFileSync } from 'fs'; TurboSign.configure({ apiKey: process.env.TURBODOCX_API_KEY, orgId: process.env.TURBODOCX_ORG_ID, senderEmail: process.env.TURBODOCX_SENDER_EMAIL, // required senderName: process.env.TURBODOCX_SENDER_NAME, }); const result = await TurboSign.sendSignature({ file: readFileSync('contract.pdf'), documentName: 'Partnership Agreement', recipients: [ { name: 'John Doe', email: 'john@example.com', signingOrder: 1 }, ], fields: [ { type: 'signature', recipientEmail: 'john@example.com', template: { anchor: '{signature1}', placement: 'replace', size: { width: 100, height: 30 }, }, }, ], }); console.log('Document ID:', result.documentId); TypeScript
Types ship inside the package, so there's no @types install. Use NodeNext module resolution and esModuleInterop.
npm install @turbodocx/sdk import { TurboSign, type HttpClientConfig, type Recipient, type Field, type SendSignatureResponse, } from '@turbodocx/sdk'; import { readFileSync } from 'fs'; const config: HttpClientConfig = { apiKey: process.env.TURBODOCX_API_KEY, orgId: process.env.TURBODOCX_ORG_ID, senderEmail: process.env.TURBODOCX_SENDER_EMAIL, // required senderName: process.env.TURBODOCX_SENDER_NAME, }; TurboSign.configure(config); const recipients: Recipient[] = [ { name: 'John Doe', email: 'john@example.com', signingOrder: 1 }, ]; const fields: Field[] = [ { type: 'signature', recipientEmail: 'john@example.com', template: { anchor: '{signature1}', placement: 'replace', size: { width: 100, height: 30 }, }, }, ]; const result: SendSignatureResponse = await TurboSign.sendSignature({ file: readFileSync('contract.pdf'), documentName: 'Partnership Agreement', recipients, fields, }); console.log('Document ID:', result.documentId); An invalid field type (or a misspelled placement) fails at compile time instead of in production.
Python
Python 3.9+. The SDK is async-native (built on httpx), so it drops straight into FastAPI or Django async views.
pip install turbodocx-sdk import asyncio import os from turbodocx_sdk import TurboSign async def main(): TurboSign.configure( api_key=os.getenv("TURBODOCX_API_KEY"), org_id=os.getenv("TURBODOCX_ORG_ID"), sender_email=os.getenv("TURBODOCX_SENDER_EMAIL"), # required sender_name=os.getenv("TURBODOCX_SENDER_NAME"), ) with open("contract.pdf", "rb") as f: pdf_file = f.read() result = await TurboSign.send_signature( file=pdf_file, document_name="Partnership Agreement", recipients=[ {"name": "John Doe", "email": "john@example.com", "signingOrder": 1} ], fields=[ { "type": "signature", "recipientEmail": "john@example.com", "template": { "anchor": "{signature1}", "placement": "replace", "size": {"width": 100, "height": 30}, }, } ], ) print(f"Document ID: {result['documentId']}") asyncio.run(main()) PHP
PHP 8.1+ (enums and named parameters), works in Laravel, Symfony, or plain PHP.
composer require turbodocx/sdk <?php use TurboDocx\TurboSign; use TurboDocx\Config\HttpClientConfig; use TurboDocx\Types\Recipient; use TurboDocx\Types\Field; use TurboDocx\Types\SignatureFieldType; use TurboDocx\Types\TemplateConfig; use TurboDocx\Types\FieldPlacement; use TurboDocx\Types\Requests\SendSignatureRequest; TurboSign::configure(new HttpClientConfig( apiKey: getenv('TURBODOCX_API_KEY'), orgId: getenv('TURBODOCX_ORG_ID'), senderEmail: getenv('TURBODOCX_SENDER_EMAIL'), // required senderName: getenv('TURBODOCX_SENDER_NAME') )); $result = TurboSign::sendSignature( new SendSignatureRequest( recipients: [ new Recipient('John Doe', 'john@example.com', 1), ], fields: [ new Field( type: SignatureFieldType::SIGNATURE, recipientEmail: 'john@example.com', template: new TemplateConfig( anchor: '{signature1}', placement: FieldPlacement::REPLACE, size: ['width' => 100, 'height' => 30] ) ), ], file: file_get_contents('contract.pdf'), documentName: 'Partnership Agreement' ) ); echo "Document ID: {$result->documentId}\n"; Go
Go 1.21+, standard library only, context.Context on every call.
go get github.com/TurboDocx/SDK/packages/go-sdk package main import ( "context" "fmt" "log" "os" turbodocx "github.com/TurboDocx/SDK/packages/go-sdk" ) func main() { client, err := turbodocx.NewClientWithConfig(turbodocx.ClientConfig{ APIKey: os.Getenv("TURBODOCX_API_KEY"), OrgID: os.Getenv("TURBODOCX_ORG_ID"), SenderEmail: os.Getenv("TURBODOCX_SENDER_EMAIL"), // required SenderName: os.Getenv("TURBODOCX_SENDER_NAME"), }) if err != nil { log.Fatal(err) } pdfFile, err := os.ReadFile("contract.pdf") if err != nil { log.Fatal(err) } result, err := client.TurboSign.SendSignature(context.Background(), &turbodocx.SendSignatureRequest{ File: pdfFile, FileName: "contract.pdf", DocumentName: "Partnership Agreement", Recipients: []turbodocx.Recipient{ {Name: "John Doe", Email: "john@example.com", SigningOrder: 1}, }, Fields: []turbodocx.Field{ { Type: "signature", RecipientEmail: "john@example.com", Template: &turbodocx.TemplateAnchor{ Anchor: "{signature1}", Placement: "replace", Size: &turbodocx.Size{Width: 100, Height: 30}, }, }, }, }) if err != nil { log.Fatal(err) } fmt.Printf("Document ID: %s\n", result.DocumentID) } Java
Java 11+, available on Maven Central, fluent builders.
<dependency> <groupId>com.turbodocx</groupId> <artifactId>turbodocx-sdk</artifactId> <version>0.9.0</version> </dependency> import com.turbodocx.TurboDocxClient; import com.turbodocx.models.*; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; public class Main { public static void main(String[] args) throws Exception { TurboDocxClient client = new TurboDocxClient.Builder() .apiKey(System.getenv("TURBODOCX_API_KEY")) .orgId(System.getenv("TURBODOCX_ORG_ID")) .senderEmail(System.getenv("TURBODOCX_SENDER_EMAIL")) // required .senderName(System.getenv("TURBODOCX_SENDER_NAME")) .build(); byte[] pdfFile = Files.readAllBytes(Paths.get("contract.pdf")); SendSignatureResponse result = client.turboSign().sendSignature( new SendSignatureRequest.Builder() .file(pdfFile) .fileName("contract.pdf") .documentName("Partnership Agreement") .recipients(Arrays.asList( new Recipient("John Doe", "john@example.com", 1) )) .fields(Arrays.asList( new Field.Builder() .type("signature") .recipientEmail("john@example.com") .template(new Field.TemplateAnchor.Builder() .anchor("{signature1}") .placement("replace") .size(new Field.Size(100, 30)) .build()) .build() )) .build() ); System.out.println("Document ID: " + result.getDocumentId()); } } Ruby
Ruby 2.7+. Works in Rails, Sinatra, or plain Ruby.
gem install turbodocx-sdk Or add gem "turbodocx-sdk" to your Gemfile and run bundle install.
require "turbodocx_sdk" TurboDocxSdk::TurboSign.configure( api_key: ENV.fetch("TURBODOCX_API_KEY"), org_id: ENV.fetch("TURBODOCX_ORG_ID"), sender_email: ENV.fetch("TURBODOCX_SENDER_EMAIL"), # required sender_name: ENV.fetch("TURBODOCX_SENDER_NAME") ) result = TurboDocxSdk::TurboSign.send_signature( "file" => "contract.pdf", # a file path or any IO "documentName" => "Partnership Agreement", "recipients" => [ { "name" => "John Doe", "email" => "john@example.com", "signingOrder" => 1 } ], "fields" => [ { "type" => "signature", "recipientEmail" => "john@example.com", "template" => { "anchor" => "{signature1}", "placement" => "replace", "size" => { "width" => 100, "height" => 30 } } } ] ) puts "Document ID: #{result['documentId']}" Method names are snake_case, but the request keys stay camelCase (documentName, recipientEmail) to match the API.
John gets an email with a signing link. No UI to build on your side. To check progress without a webhook, poll getStatus (for example under_review, completed, voided, or expired).
Agent shortcut: this is the code
/turbodocx-sdk turbosignwrites for you, wired into your framework's routes (Express, FastAPI, Laravel, Spring Boot, and so on) with your keys read from.env. Install it withnpx skills add TurboDocx/quickstart.
Prefer to drag and drop? Use signer roles
Anchors are great when you generate the PDF yourself. They're annoying when the contract comes from a lawyer, changes often, or you just want to place the fields by hand. For that case, set the document up once in the TurboDocx app and send it from code by role.
One-time setup in the app:
- Upload your PDF as a template
- On its Signatures tab, add each signer as a role (for example "Client" and "Countersigner")
- For a role that changes every time, tick Fill in name and email when sending. For a role that's always the same person (your countersigner), give it a saved name and email
- Drag each role's fields onto the PDF and save the signature setup
Then from code, name each recipient's role. The fields you placed come along with the role, so you never pass coordinates or anchors. Role keys are listed under Use via API on the template's page.
This fits best when the same document goes out again and again with different signers. The quickstart skill already prefers this path for any recurring document: it tells you what to click in TurboDocx, then generates the role-based send code and a helper that lists the template's roles.
Node.js / TypeScriptimport { TurboSign } from '@turbodocx/sdk'; // TurboSign.configure({ ... }) as in Step 1 const templateId