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. See the OAuth 2.0 section in the API reference.
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.se/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.se/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.se/#/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.se/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.se/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.se/v1/docs/{documentId}/signed-file
Authorization: Bearer YOUR_ACCESS_TOKEN
This endpoint returns a 302 Found redirect to a pre-signed S3 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.se/v1/docs/{documentId}/signed-file \
-o signed-document.pdf
Example: Download with Node.js
const response = await fetch(
`https://docs-api.formify.se/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));
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.se/v1/files', {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}` },
body: formData
});
const { fileId } = await uploadResponse.json();
const docResponse = await fetch('https://docs-api.formify.se/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.se/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