# Formify API **Source:** https://developers-next.formify.eu/ **Full spec:** https://developers-next.formify.eu/openapi.json **Version:** 1.0.0 ## Overview The Formify API lets you send documents for electronic signature, track signing status, and retrieve signed documents. It is designed for integrating signing workflows into your application. ### Getting Access Before you can use the API, you need an OAuth 2.0 client. Formify creates this for you based on information about your company, contacts, and redirect URIs. See the **[Getting Access](/guides/getting-access/)** guide for setup instructions. Once your OAuth client has been created, you can use the [OAuth 2.0 Authorization Code flow](#tag/oauth) to obtain access tokens for API requests. ### Authentication All API requests require an OAuth 2.0 access token. To obtain one, your application redirects the user to Formify for authorization, receives an authorization code, and exchanges it for an access token. See the [OAuth 2.0 section](#tag/oauth) for the complete flow. ### Environments **API base URL**: `https://docs-api.formify.se/v1` All document operations, file uploads, and OAuth token exchange happen here. **Authorization Server**: `https://app.formify.eu` This is where users authorize your application and approve access (the OAuth consent). ### Rate Limiting The API uses token-based rate limiting to ensure fair usage: - **Headers**: Every response includes rate limit headers: - `X-RateLimit-Limit`: Maximum requests allowed - `X-RateLimit-Remaining`: Requests remaining in current window - `X-RateLimit-Reset`: The number of seconds until the rate limit resets If you exceed the limit, the API returns `429 Too Many Requests`. Wait until the reset time before making additional requests. --- ## Quick Start: Send a PDF for Signing Once you have OAuth credentials and a valid `access_token`, you can send your first document for signing in three steps: ### 1. Upload a PDF Upload a PDF file using the [Upload File](#tag/files/operation/uploadFile) endpoint: ```bash POST /files Content-Type: multipart/form-data ``` You will receive a `fileId` in the response. ### 2. Create a Signature Request Send the document for signing using the [Create Document](#tag/docs/operation/createDocument) endpoint: ```bash POST /docs { "fileId": "your-file-id", "signeeDetails": [ { "fullName": "John Doe", "emailAddress": "john@example.com", "signaturePlacement": "new_page" } ] } ``` The response returns a documentId. The signee then receives an email with a link to sign the document. ### 3. Get Notified and Download Register a [webhook](#tag/webhooks/operation/registerWebhook) to receive a notification when the document is signed: ```bash POST /webhooks { "webhookUrl": "https://your-app.com/webhook", "eventTypes": ["document.completed"] } ``` Then download the signed PDF using [Get Signed Document File](#tag/docs/operation/downloadSignedFile): ```bash GET /docs/{documentId}/signed-file ``` For more detailed walkthroughs, see the **[Integration Flows](/guides/integration-flows/)** guide. --- ### Help - **[Getting Access](/guides/getting-access/)** - How to set up OAuth 2.0 credentials - **[Integration Flows](/guides/integration-flows/)** - Step-by-step guides for common integration scenarios - **[Production Checklist](/guides/production-checklist/)** - Essential steps before going live ### Links - [Formify API Changelog](/changelog) - Track API updates and changes - [Formify App](https://app.formify.eu) - Register OAuth applications and manage settings --- ## Base URLs - **Main API server for all document related endpoints and OAuth 2.0 token exchange.**: `https://docs-api.formify.se/v1` ## Authentication ### OAuth2 OAuth2 Authentication. Allows third-party systems to access the API on behalf of the user and their account after authorization. - Type: `oauth2` ## Endpoints ### oauth #### GET `/#/oauth` **OAuth authorize URL (browser redirect, not an API request)** > **This is not a REST endpoint.** Redirect the user's browser to this URL — do not call it as a server-to-server API request. ### Step 1: Requesting authorization First, redirect the customer's web browser to the authorize endpoint in the Formify app. The customer will be asked to authorize your app to access their Formify account. **Authorize URL:** ``` GET https://app.formify.eu/#/oauth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope=read,write&state={state} ``` **Parameters:** | Parameter | Required | Description | |-----------------|----------|-------------------------------------------------------------------------------------------------------| | `client_id` | Yes | The client ID provided to you by Formify when you register your app. | | `redirect_uri` | Yes | The callback URL you provided when you registered your app. This should be URL-encoded. | | `response_type` | Yes | Must be `code`. | | `scope` | Yes | Requested access scope. One of `read`, `write`, or `read,write`. | | `state` | Recommended | Strongly recommended for CSRF protection. Send a random value and verify that the same value is returned to your callback. | **Example of URL-encoded redirect_uri:** https%3A%2F%2Fwww.your-domain.com%2Foauth%2Fcallback ### Step 2: Customer Authorizes the App The customer will see a confirmation dialog. If they authorize, they will be redirected to the callback URL with a code parameter: ``` GET https://your-callback-url.com?code={authorization_code}&state={state} ``` ### Step 3: Exchange Authorization Code for Access Token Make a `POST` request to `/oauth/token` through the API to exchange the authorization code for an access token and refresh token. The refresh token is normally valid for 60 days. **Request:** ``` POST https://docs-api.formify.se/v1/oauth/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code={authorization_code}&client_id={client_id}&client_secret={client_secret}&redirect_uri={redirect_uri} ``` **Response:** ``` { "access_token": "{access_token}", "expires_in": 3600, "refresh_token": "{refresh_token}", "token_type": "Bearer", "scope": "read,write" } ``` **Query parameters:** - `client_id` (string, required) — The client ID provided to you when you registered your app. - `state` (string) — Strongly recommended for CSRF protection. Send a random value and verify that the same value is returned to your callbac - `redirect_uri` (string, required) — The callback URL you registered for your app. - `response_type` (string, required) — Must be `code`.. One of: `code` - `scope` (string, required) — Requested access scope. Use `read,write` for full access.. One of: `read` | `write` | `read,write` - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Code examples:** ```javascript const params = new URLSearchParams({ client_id: "YOUR_CLIENT_ID", redirect_uri: "https://your-app.com/callback", response_type: "code", scope: "read,write", state: "RANDOM_STATE_STRING" }); window.location.href = `https://app.formify.eu/#/oauth?${params.toString()}`; ``` ```python from urllib.parse import urlencode from flask import redirect params = urlencode({ "client_id": "YOUR_CLIENT_ID", "redirect_uri": "https://your-app.com/callback", "response_type": "code", "scope": "read,write", "state": "RANDOM_STATE_STRING", }) return redirect(f"https://app.formify.eu/#/oauth?{params}") ``` ```curl # Open this URL in the user's browser — do not call it from your server https://app.formify.eu/#/oauth?client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback&response_type=code&scope=read%2Cwrite&state=RANDOM_STATE_STRING ``` #### POST `/oauth/token` **Generate OAuth 2.0 access token using authorization code or refresh token** This endpoint is used to exchange an authorization code or refresh token for an access token. ### Notes: - The **Authorization Code Grant Flow** requires a `grant_type` of `authorization_code` and a valid `code` obtained from the authorization URL. - The **Refresh Token Grant Flow** requires a `grant_type` of `refresh_token` and a valid `refresh_token`. - Include your `client_id` and `client_secret` in the request body for authentication. **Request body (required):** - `grant_type` (string) — The type of grant being used.. One of: `authorization_code` | `refresh_token` - `client_id` (string) — OAuth client ID. Required for both grant types. - `client_secret` (string) — OAuth client secret. Required for both grant types. - `code` (string) — The authorization code issued to the client. Required when grant_type=authorization_code. - `redirect_uri` (string) — The redirect URI used in the authorization request. Required when grant_type=authorization_code. - `refresh_token` (string) — The refresh token issued to the client. Required when grant_type=refresh_token. - `scope` (string) — Comma-separated list of scopes for the access token. - `error_language` (string) — Language for the error message. Note: this field uses snake_case (`error_language`) following OAuth 2.0 form-encoded con. One of: `en` | `sv` | `es` **Response (200):** - `access_token` (string) — The access token. - `expires_in` (integer) — The number of seconds until the access token expires. - `refresh_token` (string) — The refresh token. - `token_type` (string) — The type of token. - `scope` (string) — The scope of the access token. **Code examples:** ```curl curl -X POST "https://docs-api.formify.se/v1/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=AUTH_CODE_HERE" \ -d "redirect_uri=https://your-app.com/callback" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" ``` ```javascript const body = new URLSearchParams({ grant_type: "authorization_code", code: "AUTH_CODE_HERE", redirect_uri: "https://your-app.com/callback", client_id: "YOUR_CLIENT_ID", client_secret: "YOUR_CLIENT_SECRET" }); const response = await fetch("https://docs-api.formify.se/v1/oauth/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/oauth/token" data = { "grant_type": "authorization_code", "code": "AUTH_CODE_HERE", "redirect_uri": "https://your-app.com/callback", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", } response = requests.post(url, data=data) print(response.json()) ``` #### POST `/oauth/revoke` **Revoke OAuth 2.0 token and its associated grant** This endpoint is used to revoke an OAuth 2.0 token, which invalidates its associated grant. ### Notes: - You can revoke either using an `access_token` or a `refresh_token`. ### Example Usage: #### Request Body: ```json { "token": "your_refresh_token_here", "token_type_hint": "refresh_token" } ``` #### Response: If the token is successfully revoked: - HTTP Status: `200 OK` (no body content required). If the token is invalid or has already been revoked: - HTTP Status: `400 Bad Request`. **Request body (required):** - `token` (string) — The token to be revoked (either `access_token` or `refresh_token`). - `token_type_hint` (string) — Hint about the type of token being revoked.. One of: `access_token` | `refresh_token` **Code examples:** ```curl curl -X POST "https://docs-api.formify.se/v1/oauth/revoke" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "token=your_refresh_token_here" \ -d "token_type_hint=refresh_token" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/oauth/revoke", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ token: "your_refresh_token_here", token_type_hint: "refresh_token" }) }); console.log(response.status); // 200 ``` ```python import requests url = "https://docs-api.formify.se/v1/oauth/revoke" data = { "token": "your_refresh_token_here", "token_type_hint": "refresh_token", } response = requests.post(url, data=data) print(response.status_code) # 200 ``` ### files #### POST `/files` **Upload a PDF file for signing** Uploads a PDF file to Formify, making it available for creating document signature requests via the `POST /docs` endpoint. ### Requirements - **File Format**: Only PDF files are supported. - **File Size**: Maximum allowed file size is **50 MB**. - **Unlocked PDFs Only**: The file must not be password-protected or locked in any way. Locked files cannot be processed. - **No Pre-Signed Documents**: The PDF must not contain digital signatures from other services, as these will be removed, invalidating the document's signature integrity. - **Signature Field Handling**: If the uploaded document contains existing signature fields (interactive PDF form fields of type `signature`), these will be removed automatically. New signature fields, fully compatible with the Formify signing process, must be added to the document instead. This ensures that all signature fields function seamlessly within the Formify platform. By ensuring these requirements are met, you can use the uploaded file to create a document signature request seamlessly. **Request body (required):** - `file` (string) — The PDF document to be uploaded. Max file size: 50 MB. - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Response (201):** - `fileId` (string) — The unique identifier of the uploaded file **Code examples:** ```curl curl -X POST "https://docs-api.formify.se/v1/files" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: multipart/form-data" \ -F "file=@file.pdf" \ -F "errorLanguage=en" ``` ```javascript const formData = new FormData(); formData.append("file", fileBlob, "file.pdf"); formData.append("errorLanguage", "en"); const response = await fetch("https://docs-api.formify.se/v1/files", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" }, body: formData }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/files" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } files = { "file": open("file.pdf", "rb"), "errorLanguage": (None, "en") } response = requests.post(url, headers=headers, files=files) print(response.json()) ``` #### POST `/files/merge` **Merge multiple PDF files into a single document** Merges two or more uploaded PDF files into a single file. The resulting file will be assigned a new `fileId`. ### Requirements - **File Format**: Only pre-uploaded PDF files are supported for merging. - **Order of Merge**: The files will be merged in the order of the provided `fileIds`. - **File Name**: A name for the resulting merged file must be provided. - **Max File Size**: Ensure the combined file size does not exceed system limits (**50 MB**). - **2-8 Files**: A minimum of 2 files and a maximum of 8 `fileIds` are required (duplicate `fileIds` are not allowed.). This endpoint is particularly useful for workflows where multiple PDF files need to be combined before sending them for signatures or further processing. **Request body (required):** - `fileIds` (array of strings) — List of File IDs to merge. Must contain between 2 and 8 unique File IDs. - `name` (string) — Name for the resulting merged file. **Response (201):** - `fileId` (string) — The unique identifier of the merged file. **Code examples:** ```curl curl -X POST "https://docs-api.formify.se/v1/files/merge" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "fileIds": [ "123e4567-e89b-12d3-a456-426614174000", "123e4567-e89b-12d3-a456-426614174001" ], "name": "Merged Document" }' ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/files/merge", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ "fileIds": [ "123e4567-e89b-12d3-a456-426614174000", "123e4567-e89b-12d3-a456-426614174001" ], "name": "Merged Document" }) }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/files/merge" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } payload = { "fileIds": [ "123e4567-e89b-12d3-a456-426614174000", "123e4567-e89b-12d3-a456-426614174001" ], "name": "Merged Document" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` #### DELETE `/files/{fileId}` **Delete an uploaded file** **Path parameters:** - `fileId` (string, required) — Unique identifier of the file to delete **Query parameters:** - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Code examples:** ```curl curl -X DELETE "https://docs-api.formify.se/v1/files/{fileId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/files/{fileId}", { method: "DELETE", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); console.log(response.status); // 204 ``` ```python import requests url = "https://docs-api.formify.se/v1/files/{fileId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.delete(url, headers=headers) print(response.status_code) # 204 ``` #### GET `/files/{fileId}/fields` **Retrieve all form fields for an uploaded file** Retrieves all the form fields defined in a specific file before it is used to create a document. This is useful for pre-filling values or understanding the structure of the form. ### Notes: - Fields retrieved here may include metadata such as `readOnly` (not possible to modify by the signee) and `editable` to indicate whether they can be modified or pre-filled. - For getting actual values in a signed document, use the `/docs/{documentId}/field-values` endpoint. **Path parameters:** - `fileId` (string, required) — The unique identifier of the uploaded file. **Response (200):** *(array of objects)* **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/files/{fileId}/fields" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/files/{fileId}/fields", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/files/{fileId}/fields" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers) print(response.json()) ``` ### docs #### POST `/docs` **Create a document and request signatures** This endpoint allows you to create a document and request signatures. You can: - Provide a `fileId` to create a document from an uploaded file. - Provide a `templateId` to create a document using a pre-configured template. ### Requirements - **fileId**: Use this to create a document from an uploaded file. Use `POST /files` to upload a file beforehand. - **templateId**: Use this to create a document from an existing template. Use `POST /templates` to create a template, and `GET /templates` to list templates. - **Either `fileId` or `templateId` must be provided**, but not both. If both are included, the request will fail with a `400 Bad Request` error. - **Template Feature Activation**: The ability to use templates is only available for accounts with an active price plan. Contact Formify to activate this feature for your account if it is not already enabled. ### Ownership The **User ID** (`userId`) parameter can be optionally specified to assign the document ownership to a different user within the same account. If omitted, the document will be owned by the authenticated user. This user will receive document status updates by email. ### Signee Signature Type The **Signature Type** defines the method by which the signee will provide their signature. Specify the desired method using the `signatureType` parameter: 1. **Digital Ink** (`digital_ink`): The signee can draw their signature using a mouse, touchpad, or finger. This signature type complies with **Simple Electronic Signature (SES)** standards. 2. **BankID Identification** (`bankid_identification`): The signee will securely sign using Swedish **BankID** for identification. This signature type complies with **Advanced Electronic Signature (AES)** standards. > **Note:** To achieve AES compliance for a document, all signatures within the document must be of the AES level. > **Availability:** BankID is only available for Swedish accounts or accounts that have this signature type enabled. Please contact Formify to request access. 3. **ID Scan + Ink Signature** (`digital_ink_id_scan`): The signee verifies identity by scanning an ID document and then signs using digital ink. > **Placement requirement:** This signature type only supports `existing` placement and requires both `signatureBox` and `idScanBox`. > **Availability:** This signature type may require account enablement. Please contact Formify to request access. 4. **Face Liveness** (`face_liveness`): The signee verifies identity using an ID scan together with a video selfie biometric liveness check. > **Availability:** This signature type may require account enablement. Please contact Formify to request access. ### Signature Field Placement You can choose one of two methods for placing signature fields on the document: 1. **Automatic Placement** (`new_page`): The signature field will appear on a new page at the document's end (in A4 portrait format). - Not supported for `digital_ink_id_scan`. 2. **Custom Placement with Coordinates (`existing`)**: - Use the `signatureBox` property to specify precise `x` and `y` coordinates, the `page` number, and an optional `scale` value. - Measurements are in points, and tools like Adobe Illustrator can help determine the exact position on the document. - Validation Rules: - `page` must be >= 0 and within the bounds of the document's page count. - `x` and `y` must be >= 0. - `scale` defaults to `1.0`, with a minimum of `0.25` and a maximum of `1.5`. - If `signatureBox` is not provided in the request: - Values will fall back to the template's `x`, `y`, `page`, `scale`, and `formFieldId`, if available. Each signature field is standardized in size before scaling: - **Width:** 219 points - **Height:** 58 points ### Additional Placement for ID Scan + Ink Signature When using `digital_ink_id_scan`, you must also provide an `idScanBox` inside the document. - `idScanBox` defines the position of the scanned ID placeholder. - `signaturePlacement` must be `existing` for this signature type. - Both `signatureBox` and `idScanBox` are required. - `idScanBox.scale` defaults to `1.0`, with a minimum of `0.25` and a maximum of `1.5`. The ID scan placeholder has the following base size before scaling: - **Width:** 218 points - **Height:** 138 points ### Signee Contact Methods Each signee must have at least one contact method specified to receive the document: - **Email Address** (`emailAddress`) or **Phone Number** (`phoneNumber`): Required for distributing the document. If using SMS notifications, additional transaction fees apply, and the SMS Invitation add-on must be active. - **Phone Numbers** must include the country code. - **Phone Text Message Delivery Method** (`phoneNumberDeliveryMethod`) is possible to set to either: - **sms**: Sends a regular text message to the provided phone number (default - requires SMS Invitation add-on) - **WhatsApp**: Sends a WhatsApp text message to the provided phone number (requires WhatsApp Invitation add-on) By specifying the appropriate placement method and contact information, you ensure seamless delivery and positioning of signature fields in the document. ### Signing Order The **Signing Order** feature allows you to control the sequence in which signees receive invitations and can sign the document. This is useful when signatures must be collected in a specific order. - **Enabling Signing Order**: Set `enableSigningOrder` to `true` in the request to activate this feature. - **Setting Order Values**: Use the `signingOrder` property in each signee's details within the `signeeDetails` array to specify their position in the sequence. - **Sequential Signing**: Assign different order values (1, 2, 3, etc.) to enforce a strict signing sequence. - **Simultaneous Signing**: Assign the same order value to multiple signees to allow them to sign at the same time. - **Default Behavior**: If `enableSigningOrder` is `false` (default), all signees are treated as having order 1 and can sign simultaneously. - **Invitation Delivery**: Signees will only receive their invitation (email/SMS/WhatsApp) when it is their turn to sign based on their `signingOrder` value. - **Signing Order Feature Activation**: The ability to use signing order is only available for accounts with an active price plan. Contact Formify to activate this feature for your account if it is not already enabled. ### Pre-filling Form Fields Use the optional `fields` property to pre-fill values in form fields: - **For `fileId`**: Use `GET /files/{fileId}/fields` to retrieve the list of fields in the uploaded file. - **For `templateId`**: Use `GET /templates/{templateId}/fields` to retrieve template-specific fields. Only editable fields (`editable` from `GET /files/{fileId}/fields` or `GET /templates/{templateId}/fields`) can be pre-filled through the API. ### When Using a Template - If you provide a `templateId`, the document will be created based on the specified template. This way, the template can place signature fields (and define the document's other fields/settings) in advance. - For each signee in the `signeeDetails` array: - **Full Name (`fullName`), Email Address (`emailAddress`), and Phone Number (`phoneNumber`)**: These should be provided in the request and will not fall back to the template. You must specify at least one contact method (email or phone) for each signee. - **Signature Type**: Defaults to the template's value if not specified in the request. - **Signature Placement (`signaturePlacement`)**: Defaults to the template's value if not specified in the request. - **Signature Box (`signatureBox`)**: The following properties will use the template defaults if not provided in the request: - `x`, `y`, `page`, `scale` - These are required for `existing` signature placement. If missing in both the request and template, the request will fail. - **ID Scan Box (`idScanBox`)**: Only applicable when `signatureType` is `digital_ink_id_scan`. - `x`, `y`, `page`, `scale` - These are required for `digital_ink_id_scan`. If missing in both the request and template, the request will fail. ### Form Fields Read-Only Mode (`fieldsReadonlyMode`) Determines how form fields should be handled in terms of read-only restrictions: 1. **`keepOriginal`** (Default) – Retains the original field settings without modifying readonly state. 2. **`filled`** – Set **pre-filled** fields readonly, leaving empty fields editable. 3. **`all`** – Locks **all** fields, setting them readonly to prevent modifications. ### AI Assistant (`aiAssistant`) Configure an optional AI Assistant that helps signees while reviewing and signing the document. - **`enabled`**: Set to `true` to activate the assistant for this document (default: `false`) - **`textToSpeech`**: Enable text-to-speech so the assistant can read responses aloud (default: `false`) - **`language`**: Set the assistant's language (default: `en`) The AI Assistant can answer questions about the document content and guide signees through the signing process. It supports 28 languages including English, Swedish, Spanish, German, French, and more. **Important:** When the AI Assistant is enabled, the document creation is asynchronous: - The response will have `status: "processing"` instead of `status: "created"` - The `documentUrl` field will be `null` until processing is complete - Signee invitations (email/SMS/WhatsApp) will be sent automatically once processing is complete - Use `GET /docs/{documentId}` to check the document status > **Availability:** The AI Assistant feature may require account enablement. Please contact Formify to request access. **Request body (required):** - `name` (string) — Name of the document to be signed - `userId` (string) — Optional User ID to assign as the document owner. If omitted, the authenticated user will be the owner. - `signeeDetails` (array of objects) - `enableSigningOrder` (boolean) — Enables sequential signing order for the document. When set to `true`, signees will be invited to sign based on their `s. Default: `false` - `fields` (array of objects) - `fieldsReadonlyMode` (string) — Determines how form fields are handled in terms of read-only settings.. One of: `keepOriginal` | `filled` | `all`. Default: `keepOriginal` - `language` (string) — Language of the signature request. One of: `en` | `sv` | `es`. Default: `en` - `sharingSetting` (string) — Sharing setting for sharing the document between users within the company account. The default value depends on the acco. One of: `private` | `shared` - `personalMessage` (string) — Optional message to be included in the invitation sent to signees. This message can provide additional context or instru - `aiAssistant` (object) - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Response (201):** - `documentId` (string) — Unique identifier of the created document - `name` (string) — Name of the document - `createdAt` (string) — Document creation timestamp - `userId` (string) — User ID of the document owner - `status` (string) — Status of the document creation:. One of: `created` | `processing` - `documentUrl` (string) — Link to the sender's/owner's document overview page in Formify. **Code examples:** ```curl curl -X POST "https://docs-api.formify.se/v1/docs" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "fileId": "00000000-0000-0000-0000-000000000000", "name": "Contract", "signeeDetails": [ { "fullName": "Jane Doe", "emailAddress": "jane.doe@example.com", "signaturePlacement": "new_page" } ] }' ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/docs", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ "fileId": "00000000-0000-0000-0000-000000000000", "name": "Contract", "signeeDetails": [ { "fullName": "Jane Doe", "emailAddress": "jane.doe@example.com", "signaturePlacement": "new_page" } ] }) }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/docs" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } payload = { "fileId": "00000000-0000-0000-0000-000000000000", "name": "Contract", "signeeDetails": [ { "fullName": "Jane Doe", "emailAddress": "jane.doe@example.com", "signaturePlacement": "new_page" } ] } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` #### GET `/docs` **Get documents** Get documents created by the user or shared within the account. **Query parameters:** - `limit` (integer) — Number of documents to retrieve per request (max 50) - `offset` (integer) — Number of items to skip before starting to collect the result set - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Response (200):** - `limit` (integer) — The maximum number of documents requested per page - `offset` (integer) — The offset used in the current request - `nextOffset` (integer) — The next offset for pagination, or null if there are no more results - `documents` (array of objects) - `documentId` (string) — Document ID - `name` (string) — Name of the document - `createdAt` (string) — Document creation timestamp - `status` (string) — Current status of the document:. One of: `created` | `processing` | `awaiting_signatures` | `completed` | `revoked` | `deleted` - `userId` (string) — User ID of the document owner - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/docs" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/docs", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/docs" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers) print(response.json()) ``` #### PATCH `/docs/{documentId}/signature-field` **Update signee details for a document** Updates signee details for an existing document. ### Use Case: Updating the contact information for a signee (without updating the document content) is typically done when the sender has entered the wrong email address or phone number, preventing the signing party from accessing the document. It could also be done when the signing party has to be delegated to/replaced by another party. When there are updates to the content required, you must instead revoke the document and send a new one. **Notes:** - It's only possible to edit contact details, not to add new contact methods. - If **emailAddress** or **phoneNumber** is updated, a new document invitation will be sent to the signee with the updated contact details. - A **new `signeeId`** will be generated for the updated signee within this document. - Any previously sent document links to the old contact information will be **automatically revoked** to ensure security. - The document must not be completed or signed by the signee; otherwise, the request will fail with a `403 Forbidden` error. ### Cooldown Rules - **Update Limit**: A signee's contact details can only be updated a maximum of two times, before a **60-minute cooldown period** applies. - **Cooldown**: After updating a signature field twice, a **60-minute cooldown period** applies before another update can be made for the same signature field. - **Checking Remaining Cooldown**: The remaining cooldown time before another update is allowed is included in the response as `updates.updateCooldown` property in the signee details when retrieving document information via `GET /docs/{documentId}`. **Path parameters:** - `documentId` (string, required) — The unique identifier of the document. **Request body (required):** *(array of objects)* - `signeeId` (string) — Unique identifier of the signee to update. - `fullName` (string) — Updated full name of the signee. If null, the name remains unchanged. - `emailAddress` (string) — Updated email address of the signee. If null, the email remains unchanged. - `phoneNumber` (string) — Updated phone number of the signee. If null, the phone number remains unchanged. **Response (200):** - `signeeId` (string) — Unique identifier of the updated signee. - `updated` (object) - `fullName` (string) — Updated full name. - `emailAddress` (string) — Updated email address. - `phoneNumber` (string) — Updated phone number. - `updateCooldown` (integer) — Remaining cooldown time in seconds before another signee update can be made. **Code examples:** ```curl curl -X PATCH "https://docs-api.formify.se/v1/docs/{documentId}/signature-field" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '[ { "signeeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "fullName": "John Doe", "emailAddress": "john.doe@example.com", "phoneNumber": "+46701234567" } ]' ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/docs/{documentId}/signature-field", { method: "PATCH", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify([ { "signeeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "fullName": "John Doe", "emailAddress": "john.doe@example.com", "phoneNumber": "+46701234567" } ]) }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/docs/{documentId}/signature-field" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } payload = [ { "signeeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "fullName": "John Doe", "emailAddress": "john.doe@example.com", "phoneNumber": "+46701234567" } ] response = requests.patch(url, headers=headers, json=payload) print(response.json()) ``` #### POST `/docs/{documentId}/reminder` **Send reminders to specific signees who have not signed the document** Sends reminders to the specified signees for a document that is awaiting signatures. ### Notes: - The document must be awaiting signatures (`awaiting_signatures`) to send reminders. Otherwise, the request will fail with `403 Forbidden`. - Reminders will be sent using all configured contact methods (e.g., email, SMS, WhatsApp) for each signee provided. - Reminders may incur SMS/WhatsApp fees, depending on the contact method. Ensure the account has sufficient balance. - The response includes a breakdown of which reminders were successfully sent and which were not, along with reasons for failures. ### Reminder Rules - **Reminder Limit**: A maximum of two reminders can be sent to a signee within a rolling 60-minute period. - **Cooldown**: After sending two reminders, a **60-minute cooldown period** applies before another reminder can be sent to the same signee. - **Checking Remaining Cooldown**: You can determine the remaining cooldown time for each signee by referencing the **`reminders.reminderCooldown`** property in the signee details when retrieving document information via `GET /docs/{documentId}`. **Path parameters:** - `documentId` (string, required) — Unique identifier of the document. **Request body (required):** - `errorLanguage` (string) — Language for the error message.. One of: `en` | `sv` | `es` - `signeeIds` (array of strings, required) — List of signee IDs to whom reminders will be sent. **Response (200):** - `remindersSentTo` (array of objects) — List of signee IDs who were successfully reminded. - `signeeId` (string) — The ID of the signee who was reminded. - `reminderCooldown` (integer) — Remaining cooldown time in seconds before the next reminder can be sent. - `remindersNotSentTo` (array of objects) — List of signee IDs who could not be reminded, with reasons. - `signeeId` (string) — The ID of the signee who could not be reminded. - `message` (string) — Reason why the signee could not be reminded. - `reminderCooldown` (integer) — Remaining cooldown time in seconds before the next reminder can be sent. **Code examples:** ```curl curl -X POST "https://docs-api.formify.se/v1/docs/{documentId}/reminder" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "errorLanguage": "en", "signeeIds": [ "00000000-0000-0000-0000-000000000000" ] }' ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/docs/{documentId}/reminder", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ "errorLanguage": "en", "signeeIds": [ "00000000-0000-0000-0000-000000000000" ] }) }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/docs/{documentId}/reminder" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } payload = { "errorLanguage": "en", "signeeIds": [ "00000000-0000-0000-0000-000000000000" ] } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` #### PUT `/docs/{documentId}/revoke` **Revoke a document** Revoke a document that has been sent for signing. This operation is supported only if the document has not yet been fully signed. A revoked document can no longer be opened or signed, but it's not immediately deleted from the system. **Path parameters:** - `documentId` (string, required) — Unique identifier of the document to revoke **Request body (required):** - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Response (200):** - `documentId` (string) — The unique identifier of the revoked document. **Code examples:** ```curl curl -X PUT "https://docs-api.formify.se/v1/docs/{documentId}/revoke" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "errorLanguage": "en" }' ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/docs/{documentId}/revoke", { method: "PUT", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ "errorLanguage": "en" }) }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/docs/{documentId}/revoke" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } payload = { "errorLanguage": "en" } response = requests.put(url, headers=headers, json=payload) print(response.json()) ``` #### GET `/docs/{documentId}/history` **⚠️ Get document history** ⚠️ This endpoint is currently not available in production and is under development. Retrieves the event history for a specific document, including actions such as creation, updates, signature requests, and completions. This is useful for auditing and tracking document progress over time. **Path parameters:** - `documentId` (string, required) — The unique identifier of the document. **Query parameters:** - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Response (200):** *(array of objects)* **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/docs/{documentId}/history" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/docs/{documentId}/history", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/docs/{documentId}/history" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers) print(response.json()) ``` #### GET `/docs/{documentId}/signed-file` **Redirect to a URL for the signed PDF file download** This endpoint generates a pre-signed URL for securely downloading a signed PDF file, valid for 10 minutes. It responds with a 302 status and a 'Location' header containing the URL for the client to follow. **Path parameters:** - `documentId` (string, required) — ID of the document to download the signed file for **Query parameters:** - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/docs/{documentId}/signed-file" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/docs/{documentId}/signed-file", { method: "GET", redirect: "manual", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); console.log(response.status); // 302 console.log(response.headers.get("Location")); // pre-signed download URL ``` ```python import requests url = "https://docs-api.formify.se/v1/docs/{documentId}/signed-file" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers, allow_redirects=False) print(response.status_code) # 302 print(response.headers["Location"]) # pre-signed download URL ``` #### GET `/docs/{documentId}` **Get document details/status** Retrieves detailed information about a specific document, including its status, associated signatures, and metadata. This endpoint is useful for tracking the progress and current state of a document, as well as for accessing specific metadata required for other operations. **Path parameters:** - `documentId` (string, required) — The unique identifier of the document. **Query parameters:** - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Response (200):** - `name` (string) — Name associated with the document link - `createdAt` (string) — Timestamp when the document was created - `updatedAt` (string) — Timestamp when the document was last updated - `completedAt` (string) — Timestamp when the document was completed - `revokedAt` (string) — Timestamp when the document was revoked - `expiryDate` (string) — Timestamp when the document will expire, if applicable - `status` (string) — Status identifier for the document:. One of: `created` | `processing` | `error` | `awaiting_signatures` | `completed` | `revoked` | `deleted` - `aiAssistant` (object) — AI Assistant settings for this document. Only present if AI Assistant was enabled when the document was created. Returns - `formifyTransactionId` (string) — Transaction ID associated with Formify - `language` (string) — Language code of the document. One of: `en` | `sv` | `es` - `enableSigningOrder` (boolean) — Indicates whether sequential signing order is enabled for this document. When true, signees are invited based on their s - `signeeDetails` (array of objects) — List of signees and their statuses - `signeeId` (string) — Unique identifier of the signee - `fullName` (string) — Full name of the signee - `emailAddress` (string) — Email address of the signee - `phoneNumber` (string) — Phone number of the signee used for SMS - `signatureType` (string) — Type of signature. One of: `not_set` | `bankid_identification` | `digital_ink` | `digital_ink_id_scan` | `face_liveness` - `signaturePlacement` (string) — Option used to place the signature box on an existing page ('existing', default) or adding a new signature page (A4 port. One of: `existing` | `new_page`. Default: `existing` - `signingOrder` (integer) — The signing order assigned to this signee. Only relevant when signing order is enabled for the document. Signees with th - `signatureStatus` (string) — Status of the signee's signature. One of: `signed` | `awaiting_signature` - `signedAt` (string) — Timestamp when the signee signed the document, if applicable - `reminders` (object) — Information about reminders sent to the signee - `reminderSentCount` (integer) — Number of reminders sent to this signee - `lastReminderSentAt` (string) — Timestamp of the most recent reminder sent to the signee, if applicable - `reminderCooldown` (integer) — Number of seconds remaining until a new reminder can be sent to the signee, or 0 if reminders can be sent immediately - `updates` (object) — Information about updates of the signee - `lastUpdateAt` (string) — Timestamp of the most recent update of the signee, if applicable - `updateCooldown` (integer) — Number of seconds remaining until a new update of the signee can be made, or 0 if updates can be done immediately - `userId` (string) — User ID of the document owner - `fileId` (string) — File ID used to create the document - `documentUrl` (string) — Link to the document overview page in Formify - `templateId` (string) — Template ID used, if the document was created based on a template **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/docs/{documentId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/docs/{documentId}", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/docs/{documentId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers) print(response.json()) ``` #### DELETE `/docs/{documentId}` **Delete a document** Delete a document and its related file. This operation is only allowed if the document is completed or revoked. The file used to create the document signature request will be deleted only if it is not used in other documents. **Path parameters:** - `documentId` (string, required) — Unique identifier of the document to delete **Query parameters:** - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Code examples:** ```curl curl -X DELETE "https://docs-api.formify.se/v1/docs/{documentId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/docs/{documentId}", { method: "DELETE", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); console.log(response.status); // 204 ``` ```python import requests url = "https://docs-api.formify.se/v1/docs/{documentId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.delete(url, headers=headers) print(response.status_code) # 204 ``` #### GET `/docs/{documentId}/fields` **Retrieve all form fields for a document** Retrieves all the form fields defined for a specific document. This endpoint is particularly useful for extracting form field metadata before processing or integration. This endpoint is not for getting form field values. For that, use `/docs/{documentId}/field-values`. ### Supported Form Field Types The following form field types are possible: - **checkbox**: A field that allows a binary selection (checked or unchecked). - **combobox**: A dropdown field allowing selection of one option. - **listbox**: A list field allowing selection of one or multiple options. - **radioButton**: A field that allows a single selection from a group of options. - **textField**: A single-line input field for text. ### Notes: - **Required Fields**: A field is marked as required if the `required` property is `true`. This property is determined by the field's metadata. - **Read-Only Fields**: A field is marked as read-only if the `readOnly` property is `true`. Such fields cannot be modified by the end user, but can be modified through the API before a document has been sent for signing. - **Editable Fields**: A field can be entered/edited through the API if the `editable` property is `true`. For that, use `POST /docs/{documentId}/field-values` - **Bounding Box**: The `boundingBox` property provides the position and dimensions of the form field on the document. The coordinates are measured in points. - **Default Values**: For applicable fields, `defaultValues` contains the default options or input values defined in the document. - **Options**: Dropdowns, radio buttons, and similar fields include a list of options with `label` and `value` pairs. ### Form Field Name as Key - **Key Usage**: The `name` property is used as the key for interacting with form field values instead of `fieldId`. - **Reason**: Using the `name` aligns with PDF standards and ensures compatibility with existing PDF workflows and tools. - **Advantages**: - **Consistency**: `name` is the standard identifier for form fields in PDFs. - **Human-Readable**: Names are easier to interpret compared to arbitrary IDs. - **Integration-Friendly**: Enables seamless integration with PDF tools that use names as primary identifiers. - **`fieldId` Purpose**: While the `fieldId` is included in the metadata for internal purposes, it is not used as the primary identifier in API interactions. **Path parameters:** - `documentId` (string, required) — The unique identifier of the document. **Response (200):** *(array of objects)* **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/docs/{documentId}/fields" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/docs/{documentId}/fields", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/docs/{documentId}/fields" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers) print(response.json()) ``` #### GET `/docs/{documentId}/field-values` **Retrieve form field values for a completed document** Retrieves all form field values for a specific completed document. This endpoint is only supported for **completed documents**. If the document is not completed, the request will fail with a `403 Forbidden` error. ### Notes: - **Non-Empty Fields**: Only fields with non-empty values will be included in the response. - **Value Format**: The `value` property is always an array of strings. For fields with a single value, the array will contain one element. For fields like checkboxes or multi-select dropdowns, the array may contain multiple elements. ### Example Response: ```json [ { "name": "email", "value": ["test@example.com"] }, { "name": "interests", "value": ["Photography", "Travel"] } ] ``` The response does not include metadata or structural details about the form fields. For that, use `/docs/{documentId}/fields`. **Path parameters:** - `documentId` (string, required) — The unique identifier of the document. **Response (200):** *(array of objects)* - `name` (string) — The name of the form field. - `value` (array of strings) — An array of strings representing the field values. **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/docs/{documentId}/field-values" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/docs/{documentId}/field-values", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/docs/{documentId}/field-values" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers) print(response.json()) ``` #### POST `/docs/{documentId}/field-values` **Set form field values for a document** Updates the values of one or more form fields for a specific document. ### Usage: - This endpoint **only allows updating form fields**. - The document must **not be completed or signed by anyone**; otherwise, the request will fail with a `403 Forbidden` error. - Only fields where `editable = true` (from `GET /docs/{documentId}/fields`) can be updated. ### Dynamic Value Format: - The `value` property must match the expected format of the field. - **Single-value fields (e.g., text fields, radio buttons):** Provide a **single string**. - **Multi-value fields (e.g., checkboxes, listboxes):** Provide an **array of strings**, even if it only contains one string value. - To clear the values in a listbox or combobox, provide an array including a single empty string. - For radio buttons, an empty string clears the selection. - Get the expected format (and options, when available) for each field type using `GET /docs/{documentId}/fields`. ### Supported Field Types and Expected Formats: | Field Type | Description | Example Format | |---------------------|---------------------------------|----------------| | `textField` | A standard text input field. | `"value": "John Doe"` | | `checkbox` | A field supporting multiple selections (on/off). | `"value": ["Yes"]` | | `radioButton` | A field where only one option can be selected. | `"value": "Red"` or `"value": ""` to clear selection | | `listbox` | A list where users can select one or multiple options. | `"value": ["Option1", "Option2"]` or `"value": [""]` to clear values | | `combobox` | A dropdown field allowing selection of one option. | `"value": ["SelectedOption"]` or `"value": [""]` to clear values | **Path parameters:** - `documentId` (string, required) — The unique identifier of the document. **Request body (required):** - `fields` (array of objects) **Response (200):** - `success` (boolean) **Code examples:** ```curl curl -X POST "https://docs-api.formify.se/v1/docs/{documentId}/field-values" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "fields": [ { "name": "email", "value": "test@example.com" } ] }' ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/docs/{documentId}/field-values", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ "fields": [ { "name": "email", "value": "test@example.com" } ] }) }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/docs/{documentId}/field-values" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } payload = { "fields": [ { "name": "email", "value": "test@example.com" } ] } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ### templates #### GET `/templates` **List all templates** Retrieves a paginated list of all templates available to the user. ### Notes: - Templates are reusable documents pre-configured for signature requests. - **Template Feature Activation**: The ability to use templates is only available for accounts with an active price plan. Contact Formify to activate this feature for your account if it is not already enabled. **Query parameters:** - `limit` (integer) — Number of documents to retrieve per request (max 50) - `offset` (integer) — Number of items to skip before starting to collect the result set - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Response (200):** - `limit` (integer) — The maximum number of templates requested per page - `offset` (integer) — The offset used in the current request - `nextOffset` (integer) — The next offset for pagination, or null if there are no more results - `templates` (array of objects) - `templateId` (string) — Template ID - `name` (string) — Name of the template - `createdAt` (string) — Template creation timestamp - `userId` (string) — User ID of the document owner **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/templates" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/templates", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/templates" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers) print(response.json()) ``` #### GET `/templates/{templateId}` **Retrieve template details** Retrieves detailed information about a specific template, including its fields, signees, and metadata. ### Notes: - Templates contain pre-configured signature fields and signee details. - You can use this endpoint to review the template's details before creating a document using the template. **Path parameters:** - `templateId` (string, required) — The unique identifier of the template. **Response (200):** - `templateId` (string) — Unique identifier of the template - `name` (string) — Name of the template - `createdAt` (string) — Timestamp when the template was created - `userId` (string) — User ID of the document owner - `sharingSetting` (string) — Sharing setting for the template. One of: `shared` | `private` - `signeeDetails` (array of objects) — List of signees and their default signature details **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/templates/{templateId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/templates/{templateId}", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/templates/{templateId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers) print(response.json()) ``` #### GET `/templates/{templateId}/fields` **Retrieve all form fields for a template** Retrieves all the form fields defined in a specific template. This endpoint is useful for extracting form field metadata before creating a document from the template. ### Supported Form Field Types The following form field types are possible: - **checkbox**: A field that allows a binary selection (checked or unchecked). - **combobox**: A dropdown field allowing selection of one option. - **listbox**: A list field allowing selection of one or multiple options. - **radioButton**: A field that allows a single selection from a group of options. - **textField**: A single-line input field for text. ### Notes: - **Required Fields**: A field is marked as required if the `required` property is `true`. - **Read-Only Fields**: A field is marked as read-only if the `readOnly` property is `true`. - **Editable Fields**: Fields with `editable = true` can be pre-filled when creating a document. **Path parameters:** - `templateId` (string, required) — The unique identifier of the template. **Response (200):** *(array of objects)* **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/templates/{templateId}/fields" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/templates/{templateId}/fields", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/templates/{templateId}/fields" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers) print(response.json()) ``` ### account #### GET `/account/users` **Retrieve list of users on the account** Retrieve a list of users on the account. Invited users with pending invitations are also included in the list (`invited`). **Query parameters:** - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Response (200):** - `users` (array of objects) - `userId` (string) — Unique identifier for the user. - `email` (string) — Email address of the user. - `firstName` (string) — User's first name. - `lastName` (string) — User's last name. - `fullName` (string) — User's full name, if available. - `roles` (array of strings) — Roles assigned to the user within the account. - `status` (string) — Status of the user on the account.. One of: `active` | `invited` **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/account/users" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/account/users", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/account/users" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers) print(response.json()) ``` ### user #### GET `/user` **Get user info settings** Get info about the authenticated user. **Query parameters:** - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Response (200):** - `userId` (string) — Unique identifier of the user - `emailAddress` (string) — Email address of the user on this account - `fullName` (string) — Full name of the user - `firstName` (string) — First name of the user - `lastName` (string) — Last name of the user - `accountId` (string) — Unique identifier of the user's account - `accountName` (string) — Name of the user's account **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/user" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/user", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/user" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers) print(response.json()) ``` ### webhooks #### POST `/webhooks` **Register a new webhook** Register a new webhook to receive event notifications. ### About Webhooks Webhooks allow Formify to communicate with your system using callbacks triggered by specified Formify events. Webhooks provide real-time data updates, enabling you to integrate Formify events directly into your workflows. #### What’s the use for webhooks? With webhooks, you can be notified of specific events to automate workflows such as sending documents for signature or tracking their status in real-time. #### How do I set up webhooks? When you create a webhook, specify the events you'd like to subscribe to. To reduce unnecessary requests to your server, only subscribe to the events you plan to handle. ### Entity Types Formify uses entity types to filter webhook events based on the scope of data you want to receive. The entity type defines which object changes in Formify trigger your webhook. | Entity Type | Description | Example | |-------------------|-------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------| | **Document** | Subscribe to events involving a specific document. | A document is sent for signature, signed, or completed (sealed). Each change triggers your webhook. | ### Supported Events - `document.created`: A document has been created/sent for signature. - `document.opened`: A document has been opened. - `document.signed`: A document has been signed by a signee. - `document.completed`: A document process has been signed by all parties and is sealed. - `document.revoked`: A document has been revoked. - `document.deleted`: A document has been deleted. ### Notes - Other entity types (e.g., `settings.x`) are not yet supported. - Do not assume ordering: Events may arrive late, more than once, or out of order ### Webhook Request Payload - See the `DocumentWebhookEvent` schema for the structure of the Document entity type payload. #### Example for General Events (`document.created`, `document.opened`, etc.) ```json { "event": { "eventId": "123e4567-e89b-12d3-a456-426614174000", "type": "document.created", "date": "2024-10-25T14:23:55Z", "data": { "documentId": "789e4567-e89b-12d3-a456-426614174111", "accountId": "456e4567-e89b-12d3-a456-426614174222", "userId": "123e4567-e89b-12d3-a456-426614174333" } } } ``` - **eventId**: The unique identifier for the event instance. - **type**: The type of the event that triggered the webhook. - **date**: The date and time the event was triggered in ISO 8601 format. - **data**: Contains the `documentId`, `userId` and `accountId` relevant to the event. #### Example for `document.signed` Event ```json { "event": { "eventId": "123e4567-e89b-12d3-a456-426614174000", "type": "document.signed", "date": "2024-10-25T14:23:55Z", "data": { "documentId": "789e4567-e89b-12d3-a456-426614174111", "accountId": "456e4567-e89b-12d3-a456-426614174222", "userId": "123e4567-e89b-12d3-a456-426614174333", "signeeId": "789e4567-e89b-12d3-a456-426614174555" } } } ``` ### Notes - All event payloads include `eventId`, `type`, `date`, and a `data` object with event-specific details. - The data object may differ between events and the `signeeId` field is only included for the `document.signed` event. ### Expected Response for Webhooks To acknowledge receipt of the webhook, your server should respond with an HTTP status code in the 2xx range. No specific content is required in the response body; a `200 OK` status alone will indicate successful processing. #### Example Response ```http HTTP/1.1 200 OK Content-Type: application/json ``` ### Retry Attempts Formify will attempt to deliver webhook events up to 5 times if the initial attempt fails. The retry intervals* are as follows: - First retry: "immediately" - Second retry: after ~5 minutes - Third retry: after ~30 minutes - Fourth retry: after ~2 hours - Fifth retry: after ~24 hours *The intervals are not exact and may vary slightly. After 5 retries, the event will not be sent again. Formify must receive a success status code (2xx) from your server to acknowledge the event. If a success status code is not received, Formify will retry sending the event according to the intervals specified above. Each event sent by Formify includes a unique `eventId`. You can use this `eventId` to ensure that your system processes each event only once and to avoid handling duplicate events. ### Security and Reliability To secure your webhook endpoint: - **Use HTTPS**: Always use HTTPS URLs for your webhook endpoint to encrypt data in transit - **Verify webhook signatures**: Use HMAC-SHA256 to verify that requests are authentic (see Webhook Signature Verification below) - **Use eventId for idempotency**: Store processed `eventId` values to safely ignore duplicate deliveries ### Webhook Delivery Headers Every webhook delivery includes the following headers: | Header | Type | Description | |---|---|---| | `Content-Type` | string | Always `application/json` | | `X-Formify-Timestamp` | string | Unix timestamp (seconds) of when the event was sent | | `X-Formify-Signature` | string | Lowercase hexadecimal HMAC-SHA256 digest of `{timestamp}.{raw_body}` | ### Webhook Signature Verification Formify signs all webhook payloads using HMAC-SHA256 so you can verify that requests are authentic. Each webhook endpoint is assigned a unique signing secret (prefixed with `whsec_`) upon creation. **How to verify:** The signature is computed over the string `{timestamp}.{raw_body}` using your webhook's signing secret as the HMAC key. **Example (Node.js with Express):** ```javascript const crypto = require('crypto'); const express = require('express'); const app = express(); app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const timestamp = req.headers['x-formify-timestamp']; const signature = req.headers['x-formify-signature']; const secret = 'whsec_...'; // your 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 event = JSON.parse(req.body); // Process the event... 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. **Signature format:** The `X-Formify-Signature` header contains the lowercase hexadecimal HMAC-SHA256 digest of: ``` {timestamp}.{raw_body} ``` **Timestamp verification:** You should also verify that the `X-Formify-Timestamp` value is recent to reduce replay attacks. ### Testing Webhooks For local development, you can use tools such as [ngrok](https://ngrok.com) to expose your local server to the internet and receive webhook calls from Formify. Example: ngrok http 3000 This gives you a public HTTPS URL that you can register as your webhook endpoint for testing. You can also use request inspection tools such as: - [webhook.site](https://webhook.site) — Inspect webhook payloads without writing code - [requestbin.com](https://requestbin.com) — Capture and inspect HTTP requests These tools are useful for development and debugging before you build your own webhook handler. **Typical test flow** 1. Register a webhook endpoint 2. Trigger an event, for example by creating or signing a document 3. Verify that the webhook payload is received 4. Return a `2xx` response from your server > **Note:** These tools are useful for development and debugging. Production webhook endpoints should use your own secure, publicly accessible HTTPS endpoint. ### Idempotency Webhooks may be delivered more than once due to network issues or retries. To handle this: ```javascript // Example: Node.js/Express webhook handler app.post('/webhook', async (req, res) => { const { event } = req.body; const eventId = event.eventId; // Check if we've already processed this event const alreadyProcessed = await db.checkEventId(eventId); if (alreadyProcessed) { console.log(`Event ${eventId} already processed, skipping`); return res.status(200).send('OK'); } // Store eventId to prevent duplicate processing await db.storeEventId(eventId); // Process the event await handleEvent(event); res.status(200).send('OK'); }); ``` ### Debugging **Common Issues:** | Issue | Cause | Solution | |-------|-------|----------| | Webhook not received | URL incorrect or server down | Verify URL is accessible from the internet | | Webhook retrying repeatedly | Server not returning 2xx status | Check server logs, ensure 200/201/204 response | | Duplicate events | Not tracking eventId | Implement idempotency using eventId | | Events out of order | Network delays | Don't rely on event order; use timestamps | **Logging:** - Log all incoming webhook requests with timestamp, eventId, and event type - Log your server's response status code **Monitoring:** - Track webhook delivery success rate in Formify dashboard (if available) - Set up alerts if webhook endpoint returns errors consistently - Monitor processing time to ensure webhooks are handled quickly **Request body (required):** - `webhookUrl` (string) — The URL where webhook events will be sent. - `name` (string) — The name/alias of the webhook for you to identify it. - `eventTypes` (array of strings) — A list of event types that the webhook will be subscribed to. **Response (201):** - `webhookId` (string) — The unique identifier of the registered webhook. - `eventTypes` (array of strings) — A list of event types the webhook is subscribed to. - `signingSecret` (string) — HMAC signing secret for this webhook. Store this securely — it is used to verify webhook payloads. **Code examples:** ```curl curl -X POST "https://docs-api.formify.se/v1/webhooks" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "string", "name": "string", "eventTypes": [ "document.created" ] }' ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/webhooks", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ "webhookUrl": "string", "name": "string", "eventTypes": [ "document.created" ] }) }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/webhooks" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } payload = { "webhookUrl": "string", "name": "string", "eventTypes": [ "document.created" ] } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` #### GET `/webhooks` **Get all registered webhooks** This endpoint retrieves all registered webhooks. **Query parameters:** - `errorLanguage` (string) — The language for error messages.. One of: `en` | `sv` | `es` **Response (200):** *(array of objects)* - `webhookId` (string) — The unique identifier of the webhook. - `webhookUrl` (string) — The URL where webhook events are sent. - `name` (string) — The name of the webhook. - `eventTypes` (array of strings) - `signingSecret` (string) — HMAC signing secret for this webhook. **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/webhooks" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/webhooks", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/webhooks" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers) print(response.json()) ``` #### GET `/webhooks/{webhookId}` **Get a specific webhook** This endpoint retrieves a specific webhook by its unique identifier. **Path parameters:** - `webhookId` (string, required) — The unique identifier of the webhook to retrieve. **Response (200):** - `webhookId` (string) — The unique identifier of the webhook. - `webhookUrl` (string) — The URL where webhook events are sent. - `name` (string) — The name of the webhook. - `eventTypes` (array of strings) - `hasSigningSecret` (boolean) — Whether HMAC signing secret is used for this webhook. **Code examples:** ```curl curl -X GET "https://docs-api.formify.se/v1/webhooks/{webhookId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/webhooks/{webhookId}", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/webhooks/{webhookId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.get(url, headers=headers) print(response.json()) ``` #### DELETE `/webhooks/{webhookId}` **Delete a webhook** This endpoint deletes a registered webhook. **Path parameters:** - `webhookId` (string, required) — The unique identifier of the webhook to delete. **Code examples:** ```curl curl -X DELETE "https://docs-api.formify.se/v1/webhooks/{webhookId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/webhooks/{webhookId}", { method: "DELETE", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); console.log(response.status); // 204 ``` ```python import requests url = "https://docs-api.formify.se/v1/webhooks/{webhookId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.delete(url, headers=headers) print(response.status_code) # 204 ``` #### POST `/webhooks/{webhookId}/rotate-secret` **Rotate webhook signing secret** Generates a new signing secret for the specified webhook. The old secret is immediately invalidated. Make sure to update your verification logic with the new secret. **Path parameters:** - `webhookId` (string, required) — The unique identifier of the webhook. **Query parameters:** - `errorLanguage` (string) — Language for the error message. One of: `en` | `sv` | `es` **Response (200):** - `webhookId` (string) — The unique identifier of the webhook. - `signingSecret` (string) — The new signing secret. **Code examples:** ```curl curl -X POST "https://docs-api.formify.se/v1/webhooks/{webhookId}/rotate-secret" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://docs-api.formify.se/v1/webhooks/{webhookId}/rotate-secret", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } }); const data = await response.json(); console.log(data); ``` ```python import requests url = "https://docs-api.formify.se/v1/webhooks/{webhookId}/rotate-secret" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } response = requests.post(url, headers=headers) print(response.json()) ```