Developer guide
Signadoc turns a PDF into a reusable template with named fields, then produces signing links that stamp in the data you already know. There are two ways to integrate: embed Signadoc’s pages into your own site with a few lines of HTML, or drive everything from your server with the REST API. Most integrations use both: the embed for setup, the API for sending.
Embed: put Signadoc in your web page
An iframe (or our tiny helper script) shows the field designer or the signing page inside your site. Your users never see Signadoc or log into it.
- No backend code needed to start
- Your users upload PDFs and place fields themselves
- You get JavaScript events when things happen
REST API: drive Signadoc from your server
Plain HTTPS + JSON with your secret key. Create signing links from a CRM record, send a contract to 200 people, or fill a PDF without any signature at all.
- Pre-fill fields with data from your system
- Email signers or embed the link in your own flow
- Fetch status, audit trail and the signed PDF
Which should I use?
| You want to… | Use | Why |
|---|---|---|
| Let customers upload their own contract and mark where to sign, inside your portal | Embed (designer) | The designer is a full UI. Embedding it is one iframe; rebuilding it on the API would be a lot of work. |
| Send your standard agreement to a new customer when they are created in your CRM | REST API | One POST with the customer’s name, address and email. Signadoc stamps the values and emails the link. |
| Have the customer sign without leaving your checkout or onboarding page | REST API + embed (signing page) | Create the envelope server-side, then show its embed_sign_url in an iframe and listen for the signed event. |
| Generate a filled-in PDF (no signature) for records or printing | REST API | POST /api/documents/:id/fill returns the PDF bytes directly. |
| Try it in an afternoon with no developers available | Dashboard only | Upload, Detect blanks, Send for signature. Come back to this page when you want to automate. |
1Embedding Signadoc in your site
Embedding means placing one of Signadoc’s pages inside yours with an <iframe>. Two pages can be embedded: the field designer (upload a PDF, place fields, create a signing link) and the signing page (what a signer sees). Signadoc talks back to your page with window.postMessage events so you know when something happened.
A. Embed the field designer
- Allow your origin. In Settings → Allowed embed origins add the origin of the page that will host the iframe, for example
https://portal.example.com. Signadoc refuses to load the designer from anywhere else, which is what makes it safe to use the publishable key in HTML. - Add the iframe. Point it at
/embed/definewith your publishable key. Optionally pre-select an existing template so the user only places fields or creates a link. - Listen for events. Every message has
source: "signadoc"and aneventname. Use them to close a modal, store the new document ID, or show the signing link.
<!-- 1. The iframe. Signadoc checks that this page's origin is on your allowed list. -->
<iframe id="signadoc" src="https://signadoc.app/embed/define?key=pk_YOUR_PUBLISHABLE_KEY"
width="100%" height="820" style="border:0;border-radius:12px"></iframe>
<!-- 2. Listen for what happens inside it. -->
<script>
window.addEventListener('message', (e) => {
if (e.data?.source !== 'signadoc') return;
switch (e.data.event) {
case 'document.uploaded': console.log('New template', e.data.document_id); break;
case 'fields.saved': console.log('Fields defined', e.data.document_id); break;
case 'envelope.created': console.log('Signing link', e.data.envelope.sign_url); break;
case 'done': /* close your modal, move on */ break;
}
});
</script>Events from the designer
| Event | Meaning |
|---|---|
ready | The designer has loaded and is ready for input. |
document.uploaded | The user uploaded a PDF. Includes document_id. Store it: it is now a reusable template on your account. |
document.opened | An existing document was opened (when you passed document=). |
fields.saved | Fields were saved. Includes document_id and the field list. |
envelope.created | The user clicked “Send for signature”. Includes the envelope with sign_url and embed_sign_url. |
done | The user finished. A good moment to close your modal. |
error | Something went wrong. Includes message. |
resize | The content height changed. Includes height, so you can grow the iframe. |
Prefer a helper script?
Load https://signadoc.app/embed.js and let it build the iframe, handle auto-resizing and route events to one callback:
<script src="https://signadoc.app/embed.js"></script>
<div id="signadoc"></div>
<script>
Signadoc.embed({
container: '#signadoc',
publishableKey: 'pk_YOUR_PUBLISHABLE_KEY',
documentId: 'doc_…', // optional: open an existing template
options: { title: 'Set up your contract', accent: '#4f46e5', allow_upload: 1 },
onEvent(event, data) {
if (event === 'envelope.created') window.location = data.envelope.sign_url;
},
});
</script>URL options (also accepted by the helper’s options): document (open a template), title, accent (hex colour), allow_upload=0 (hide the upload step), external_ref (your own ID stored on the document).
Keep the publishable key out of the page
If you would rather not expose any key in HTML, create a short-lived embed session from your server with the secret key and give the iframe the returned URL. The token expires (default 1 hour) and can only touch the document you name.
curl -X POST https://signadoc.app/api/embed/sessions \
-H "X-Api-Key: sk_YOUR_SECRET_KEY" -H "Content-Type: application/json" \
-d '{ "kind": "define", "document_id": "doc_…", "ttl_seconds": 1800, "title": "Set up your contract" }'
# → { "kind": "define", "token": "emb_…", "url": "https://signadoc.app/embed/define?token=emb_…", "expires_in": 1800 }
# Use "url" as the iframe src. No key appears in your HTML.B. Embed the signing page
Every signing request (an envelope) has two links: sign_url for emails, and embed_sign_url which is the same page with ?embed=1. The embed version hides Signadoc branding and emits events, so you can show it in an iframe on your own thank-you, checkout or onboarding page.
- Create the envelope on your server (see the API section below, or use
POST /api/embed/sessionswithkind: "sign"which creates it and returns the embed URL in one call). - Render the iframe with
embed_sign_urland listen forsigned.
<!-- Server side: create the envelope and hand embed_sign_url to your template -->
<iframe id="sign" src="EMBED_SIGN_URL" width="100%" height="900" style="border:0"></iframe>
<script>
window.addEventListener('message', (e) => {
if (e.data?.source !== 'signadoc') return;
if (e.data.event === 'progress') console.log(e.data.done + ' of ' + e.data.total + ' fields');
if (e.data.event === 'signed') { /* e.data.envelope_id is complete: fetch the PDF from your server */ }
});
</script>
<!-- Or with the helper -->
<script>
Signadoc.embed({ container: '#sign', mode: 'sign', signUrl: 'SIGN_URL', onEvent: (ev, d) => ev === 'signed' && done(d) });
</script>| Event | Meaning |
|---|---|
ready | The signing page has loaded. |
progress | The signer completed a field. Includes done and total counts. |
signed | The signer finished. The signed PDF is now available through the API. |
error | The link is invalid, expired, voided or already signed. Includes message. |
resize | Content height changed. Includes height. |
You can also pre-fill unlocked fields on the URL: …?embed=1&Phone=555-0100. A complete host page lives at /examples/host-page.html.
2Using the REST API from your server
The API is ordinary HTTPS with JSON bodies. Authenticate every call with your secret key in an X-Api-Key header (or Authorization: Bearer). The typical flow has four steps; steps 1 and 2 happen once per template, steps 3 and 4 once per signer.
Upload a template
POST /api/documentsUpload the PDF once. It becomes a document with an ID you reuse forever.
Define the fields
PUT /api/documents/:id/fieldsOr skip the API and place fields in the dashboard with Detect blanks. Each field has a key such as Name or Address.
Create a signing request
POST /api/documents/:id/envelopesPass values for the keys you know. They are stamped in and locked. Signadoc returns sign_url (and emails it if you ask).
Collect the result
GET /api/envelopes/:idPoll status, read the audit trail, and download the signed PDF from signed_pdf_url.
Example: send a contract to a new customer
Suppose you created the template “Service Agreement” in the dashboard with fields Name, Address, Phone, Email, Date (automatic) and Signature. When a customer signs up, your server runs:
curl -X POST https://signadoc.app/api/documents/DOC_ID/envelopes \
-H "X-Api-Key: sk_YOUR_SECRET_KEY" -H "Content-Type: application/json" \
-d '{
"signer_name": "Jane Doe",
"signer_email": "jane@example.com",
"values": { "Name": "Jane Doe", "Address": "123 Main St\nSpringfield, IL", "Phone": "555-0100", "Email": "jane@example.com" },
"locked": true,
"redirect_url": "https://example.com/thanks",
"external_ref": "customer_8841",
"send": true,
"subject": "Please sign your service agreement"
}'Signadoc creates the envelope, stamps the four values into the PDF as read-only text, emails Jane a private link, and responds:
{
"envelope": {
"id": "env_gKGmIzcI59N6",
"status": "pending",
"signer_name": "Jane Doe",
"signer_email": "jane@example.com",
"sign_url": "https://signadoc.app/sign/…",
"embed_sign_url": "https://signadoc.app/sign/…?embed=1",
"signed_pdf_url": null,
"external_ref": "customer_8841",
"created_at": "2026-09-28T02:31:10.512Z"
},
"delivery": { "sent": true, "mode": "smtp" }
}Jane opens the link, fills anything you did not supply, signs, and is redirected to your redirect_url. Later your server checks on it:
curl https://signadoc.app/api/envelopes/env_gKGmIzcI59N6 -H "X-Api-Key: sk_YOUR_SECRET_KEY"
# → { "envelope": { "status": "completed", "completed_at": "…", "signed_pdf_url": "https://signadoc.app/api/envelopes/env_…/signed.pdf",
# "audit": [ { "event": "envelope.created", "at": "…" }, { "event": "envelope.viewed", "ip": "…" }, { "event": "envelope.signed" }, … ] } }
curl https://signadoc.app/api/envelopes/env_gKGmIzcI59N6/signed.pdf -H "X-Api-Key: sk_YOUR_SECRET_KEY" -o signed.pdfThe same thing in JavaScript
// Node 18+ (or any runtime with fetch). Keep SIGNADOC_SECRET_KEY in an environment variable.
const BASE = 'https://signadoc.app';
const headers = { 'X-Api-Key': process.env.SIGNADOC_SECRET_KEY, 'Content-Type': 'application/json' };
async function sendAgreement(customer) {
const res = await fetch(`${BASE}/api/documents/${process.env.AGREEMENT_DOC_ID}/envelopes`, {
method: 'POST', headers,
body: JSON.stringify({
signer_name: customer.name, signer_email: customer.email,
values: { Name: customer.name, Address: customer.address, Phone: customer.phone },
external_ref: customer.id, send: true,
}),
});
if (!res.ok) throw new Error((await res.json()).error); // every error is { "error": "why" }
const { envelope } = await res.json();
return envelope; // envelope.sign_url, envelope.id
}
async function isSigned(envelopeId) {
const res = await fetch(`${BASE}/api/envelopes/${envelopeId}`, { headers });
const { envelope } = await res.json();
return envelope.status === 'completed' ? envelope.signed_pdf_url : null;
}Other common calls
Send one document to many people
Each recipient gets their own envelope and email. Shared values apply to everyone; per-recipient values override them.
curl -X POST https://signadoc.app/api/documents/DOC_ID/send \
-H "X-Api-Key: sk_YOUR_SECRET_KEY" -H "Content-Type: application/json" \
-d '{ "recipients": [
{ "name": "Jane Doe", "email": "jane@example.com", "values": { "Name": "Jane Doe" } },
{ "name": "Bob Ray", "email": "bob@example.com", "values": { "Name": "Bob Ray" } } ],
"values": { "Company": "Acme LLC" },
"subject": "Please sign the updated policy" }'
# → { "created": 2, "sent": 2, "results": [ { "email": "jane@example.com", "envelope": { "sign_url": "…" }, "delivery": { "sent": true } }, … ] }Fill a PDF without any signing
curl -X POST https://signadoc.app/api/documents/DOC_ID/fill -H "X-Api-Key: sk_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" -d '{ "values": { "Name": "Jane Doe", "Phone": "555-0100" } }' -o filled.pdfUpload a template and define its fields in code
# Upload (multipart). Optional: name, tags, external_ref, and "fields" as a JSON string.
curl -X POST https://signadoc.app/api/documents -H "X-Api-Key: sk_YOUR_SECRET_KEY" \
-F "file=@agreement.pdf" -F "name=Service Agreement" -F "tags=Sales, Onboarding"
# → { "document": { "id": "doc_…", "pages": [ { "width": 612, "height": 792 } ], "fields": [] } }
# Fields: coordinates are PDF points from the top-left of the page.
curl -X PUT https://signadoc.app/api/documents/DOC_ID/fields -H "X-Api-Key: sk_YOUR_SECRET_KEY" -H "Content-Type: application/json" \
-d '{ "fields": [
{ "key": "Name", "label": "Name", "type": "name", "page": 1, "x": 90, "y": 150, "w": 200, "h": 18, "fill": "api" },
{ "key": "Date", "label": "Date", "type": "date", "page": 1, "x": 400, "y": 150, "w": 110, "h": 18, "fill": "auto", "auto_source": "date" },
{ "key": "Signature", "label": "Signature", "type": "signature", "page": 1, "x": 90, "y": 640, "w": 200, "h": 44 } ] }'Tip: it is usually easier to place fields visually in the dashboard, then read them back with GET /api/documents/:id/fields if your code needs the keys.
Full endpoint reference
Every endpoint with parameters, example requests and responses, error codes and object shapes.