Overview

This guide covers the three most common integration scenarios with Formify:

  1. Send a document for signing — Upload a PDF and create a signature request
  2. Get notified when signing status changes — Use webhooks to trigger automated workflows
  3. Download the signed document — Retrieve the completed PDF
Prerequisites: Before starting, you need a valid 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 returned access_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.

1
Upload PDF file
POST /files with multipart/form-data
2
Receive fileId
Save this ID for the next step
3
Create signature request
POST /docs with fileId and signee details
4
Signees receive email or SMS
Formify 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"
}
File requirements:
  • 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"
}
Save the documentId! You will need it to track document status and retrieve the signed file later.

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
  }
]
Signing order: Set 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"
}
Important: The event type is document.completed. You can also subscribe to events such as document.signed and document.created.
Save the signing secret: The response includes a 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:

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:

  1. Verify the HMAC signature — Validate X-Formify-Signature using your webhook signing secret and the raw request body
  2. Check that the timestamp is recent — Validate X-Formify-Timestamp to reduce replay attacks
  3. Make processing idempotent — Store processed eventId values to prevent duplicate processing
  4. Look up the document — Use data.documentId to find the corresponding record in your system
  5. Trigger your workflow — Update status, send notifications, download the PDF, and so on
  6. 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');
});
Important: Use the raw request body exactly as received when verifying the signature. Do not verify against a re-serialized JSON payload.

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.

Temporary URL: The download URL is short-lived. If you need to download the file again later, make a new request to /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.

1
Create the document with disableInvitationMessage: true on the signees you want to handle yourself
2
Fetch the per-recipient links with GET /docs/{documentId}/recipient-links
3
Deliver each signingUrl to its recipient over your own channel

Step 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
  }]
}
Invitation only: 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
    }
  ]
}
Links require verification on open. Because a self-distributed link can be forwarded or leaked, opening one requires verification: the recipient must first enter a one-time code that Formify sends to the contact method on file (the email or phone you provided) — never to a value the opener types in. This is enforced regardless of the document's other verification settings, so a leaked link cannot be opened by anyone else. You don't need to drive this yourself: the verification step happens automatically in the browser when the recipient opens the link. You only deliver the signingUrl.

Notes

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

Next Steps