Overview
This guide covers the three most common integration scenarios with Formify:
- Send a document for signing — Upload a PDF and create a signature request
- Get notified when signing status changes — Use webhooks to trigger automated workflows
- Download the signed document — Retrieve the completed PDF
access_token. How you get one depends on your client type:
- Confidential client (manual registration) — see Getting Access
- Public client (metadata document or DCR) — see OAuth for AI & Third-Party Clients
- API key fallback (token-only MCP clients) — exchange the API key at
/auth/token, then use the returnedaccess_token
Use OAuth where the client supports it. The API key fallback exists for clients that cannot run an OAuth/PKCE flow.
Flow 1: Send a Document for Signing
This is the most common workflow: upload a PDF and send it to one or more signees.
POST /files with multipart/form-dataSave this ID for the next step
POST /docs with fileId and signee detailsFormify sends signing invitations automatically
Step 1: Upload PDF
POST https://docs-api.formify.eu/v1/files Authorization: Bearer YOUR_ACCESS_TOKEN Content-Type: multipart/form-data file: [PDF binary data] errorLanguage: sv
Response:
{
"fileId": "d6fb0d8c-4bd4-4ef6-b5f3-8a25570d7abc",
"name": "Contract.pdf",
"uploadedAt": "2024-03-17T10:12:34Z"
}
- Maximum size: 50 MB
- Format: PDF only
- Must not be password-protected
- Must not contain existing digital signatures
Step 2: Create Signature Request
Use the fileId from step 1 to create a signature request. The simplest approach is to use signaturePlacement: "new_page", which adds signatures on a new page at the end of the document.
POST https://docs-api.formify.eu/v1/docs
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"fileId": "d6fb0d8c-4bd4-4ef6-b5f3-8a25570d7abc",
"name": "Contract - Project Alpha",
"language": "sv",
"signeeDetails": [
{
"fullName": "Anna Andersson",
"emailAddress": "anna@example.com",
"signatureType": "bankid_identification",
"signaturePlacement": "new_page"
}
],
"personalMessage": "Please sign this contract."
}
Response:
{
"documentId": "2a591ba0-7991-4ce0-9f02-2b76c18b8c86",
"name": "Contract - Project Alpha",
"createdAt": "2024-03-17T10:13:05Z",
"documentUrl": "https://app.formify.eu/#/docs/2a591ba0-7991-4ce0-9f02-2b76c18b8c86"
}
Multiple Signees
To add multiple signees, include multiple objects in the signeeDetails array:
"signeeDetails": [
{
"fullName": "Anna Andersson",
"emailAddress": "anna@example.com",
"signatureType": "bankid_identification",
"signaturePlacement": "new_page",
"signingOrder": 1
},
{
"fullName": "Erik Eriksson",
"emailAddress": "erik@example.com",
"signatureType": "bankid_identification",
"signaturePlacement": "new_page",
"signingOrder": 2
}
]
enableSigningOrder: true in the document request and use different signingOrder values to enforce sequential signing. Signees with the same order value can sign simultaneously.
Flow 2: Get Notified When Document Status Changes
Use webhooks to receive real-time notifications when documents are signed or completed.
Step 1: Register Webhook
Register your webhook URL once per customer account after OAuth activation:
POST https://docs-api.formify.eu/v1/webhooks
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"webhookUrl": "https://your-app.com/webhooks/formify",
"name": "Formify Document Webhook",
"eventTypes": ["document.completed"]
}
Response:
{
"webhookId": "3d0cb5c4-6f51-4e56-8b7c-0f9d0d5a1234",
"webhookUrl": "https://your-app.com/webhooks/formify",
"name": "Formify Document Webhook",
"eventTypes": ["document.completed"],
"signingSecret": "whsec_xxxxxxxxxxxxxxxxx"
}
document.completed. You can also subscribe to events such as document.signed and document.created.
signingSecret for this webhook. Store it securely when the webhook is created — you will need it to verify the X-Formify-Signature header on every incoming webhook delivery.Secret management: Each webhook has its own signing secret. If the secret is compromised or lost, rotate it using
POST /webhooks/{webhookId}/rotate-secret. The response contains the new signingSecret. Update your webhook verification logic with the new secret immediately after rotating.
Step 2: Receive Webhook Events
When a subscribed event occurs, Formify sends a POST request to your webhook URL.
Headers included in every delivery:
X-Formify-Timestamp— Unix timestamp used when generating the signatureX-Formify-Signature— HMAC-SHA256 signature of{timestamp}.{raw_body}
Example payload:
{
"event": {
"eventId": "f3bcd5f8-45de-4b90-8b8b-9ce0c8e9b8e2",
"type": "document.completed",
"date": "2024-03-17T10:20:00Z"
},
"data": {
"documentId": "2a591ba0-7991-4ce0-9f02-2b76c18b8c86",
"accountId": "...",
"userId": "...",
"signeeId": null
}
}
Step 3: Verify and Handle the Webhook
Your webhook endpoint should:
- Verify the HMAC signature — Validate
X-Formify-Signatureusing your webhook signing secret and the raw request body - Check that the timestamp is recent — Validate
X-Formify-Timestampto reduce replay attacks - Make processing idempotent — Store processed
eventIdvalues to prevent duplicate processing - Look up the document — Use
data.documentIdto find the corresponding record in your system - Trigger your workflow — Update status, send notifications, download the PDF, and so on
- Respond quickly — Return HTTP 200 as soon as possible
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhooks/formify', express.raw({ type: 'application/json' }), async (req, res) => {
const timestamp = req.headers['x-formify-timestamp'];
const signature = req.headers['x-formify-signature'];
const secret = 'whsec_...'; // your webhook signing secret
const signedPayload = `${timestamp}.${req.body}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(req.body);
const { event, data } = payload;
const alreadyProcessed = await db.checkEventId(event.eventId);
if (alreadyProcessed) {
return res.status(200).send('OK');
}
await db.storeEventId(event.eventId);
if (event.type === 'document.completed') {
await handleDocumentCompleted(data.documentId);
}
res.status(200).send('OK');
});
Flow 3: Download Signed Document
Once a document is completed, you can either show a link to Formify or download the signed PDF to your own system.
Option A: Show Link to Formify
The simplest approach is to let users view the document in Formify:
GET https://docs-api.formify.eu/v1/docs/{documentId}
Authorization: Bearer YOUR_ACCESS_TOKEN
The response includes documentUrl, which you can display as a link in your UI:
<a href="{documentUrl}" target="_blank">View document in Formify</a>
Option B: Download and Store PDF
To store the signed PDF in your own system:
GET https://docs-api.formify.eu/v1/docs/{documentId}/signed-file
Authorization: Bearer YOUR_ACCESS_TOKEN
This endpoint returns a 302 Found redirect to a temporary download URL. Your HTTP client should automatically follow the redirect to download the PDF.
/signed-file.
Example: Download with cURL
curl -L -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
https://docs-api.formify.eu/v1/docs/{documentId}/signed-file \
-o signed-document.pdf
Example: Download with Node.js
const response = await fetch(
`https://docs-api.formify.eu/v1/docs/${documentId}/signed-file`,
{
headers: { 'Authorization': `Bearer ${accessToken}` },
redirect: 'follow'
}
);
const buffer = await response.arrayBuffer();
await fs.writeFile('signed-document.pdf', Buffer.from(buffer));
Flow 4: Distribute Signing Links Yourself
By default, Formify delivers the signing invitation to each signee over email, SMS, or WhatsApp. If you would rather hand the signing link to a recipient yourself — through your own app, a chat message, or any other channel — you can suppress Formify's invitation and fetch the link to distribute.
disableInvitationMessage: true on the signees you want to handle yourselfGET /docs/{documentId}/recipient-linkssigningUrl to its recipient over your own channelStep 1: Suppress Formify's invitation
Set disableInvitationMessage to true for any signee whose link you want to distribute yourself. The signing link is still created and the signee can still sign — Formify just does not send them the invitation. This works the same way in the draft flow.
POST https://docs-api.formify.eu/v1/docs
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"fileId": "...",
"name": "Contract",
"signeeDetails": [{
"fullName": "Jane Smith",
"emailAddress": "jane.smith@example.com",
"signaturePlacement": "new_page",
"disableInvitationMessage": true
}]
}
disableInvitationMessage suppresses just the initial invitation. Reminders (POST /docs/{documentId}/reminder) and the completed-document email are unaffected.
Step 2: Fetch the links
GET https://docs-api.formify.eu/v1/docs/{documentId}/recipient-links
Authorization: Bearer YOUR_ACCESS_TOKEN
The response returns one entry per recipient signing share:
{
"documentId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"recipients": [
{
"signeeId": "a1b2c3d4-0000-1111-2222-333344445555",
"fullName": "Jane Smith",
"emailAddress": "jane.smith@example.com",
"phoneNumber": null,
"verificationChannel": "email",
"signingUrl": "https://app.formify.eu/s/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"requiresVerification": true
}
]
}
signingUrl.
Notes
- A recipient given both an email and a phone number is returned as two links — the same
signeeIdappears twice, each tagged with theverificationChannel(email,sms, orwhatsapp) the code is sent to. Distribute whichever you prefer. - A recipient with no contact method on file cannot be verified, so they are omitted from the response entirely. If an expected recipient is missing, add a contact method first.
- The endpoint generates and persists the links on first call, so it requires the
writescope. Calling it again returns the same links.
Complete Integration Example
Here is how all three flows work together in a typical integration:
// 1. User initiates document signing in your app
async function sendDocumentForSigning(pdfFile, signeeEmail) {
const formData = new FormData();
formData.append('file', pdfFile);
const uploadResponse = await fetch('https://docs-api.formify.eu/v1/files', {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}` },
body: formData
});
const { fileId } = await uploadResponse.json();
const docResponse = await fetch('https://docs-api.formify.eu/v1/docs', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileId,
name: 'Contract',
signeeDetails: [{
fullName: 'John Doe',
emailAddress: signeeEmail,
signatureType: 'bankid_identification',
signaturePlacement: 'new_page'
}]
})
});
const { documentId } = await docResponse.json();
await db.saveDocument(documentId, { status: 'pending' });
return documentId;
}
// 2. Webhook handler (runs when document is completed)
app.post('/webhooks/formify', express.raw({ type: 'application/json' }), async (req, res) => {
const timestamp = req.headers['x-formify-timestamp'];
const signature = req.headers['x-formify-signature'];
const secret = 'whsec_...';
const signedPayload = `${timestamp}.${req.body}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const { event, data } = JSON.parse(req.body);
if (event.type === 'document.completed') {
await db.updateDocument(data.documentId, { status: 'completed' });
const pdfResponse = await fetch(
`https://docs-api.formify.eu/v1/docs/${data.documentId}/signed-file`,
{ headers: { 'Authorization': `Bearer ${accessToken}` }, redirect: 'follow' }
);
const pdfBuffer = await pdfResponse.arrayBuffer();
await storage.savePDF(data.documentId, pdfBuffer);
await sendEmail(user, 'Your document has been signed!');
}
res.status(200).send('OK');
});
Best Practices
- Always use HTTPS for webhook URLs
- Verify webhook signatures using your webhook signing secret and the raw request body
- Implement idempotency using
eventIdto prevent duplicate processing - Store documentId in your database to track document status
- Handle token refresh proactively before access tokens expire
- Use
new_pageplacement for simplicity unless you need precise positioning of signature fields
Next Steps
- Review the Production Checklist before going live
- Explore the full API Reference for advanced features
- Test your integration thoroughly in your own development environment before going live