DEV Community

Cover image for A developer's guide to eSignature API integration
Lucien Chemaly
Lucien Chemaly

Posted on

A developer's guide to eSignature API integration

Most signing integrations follow the same shortcut, redirecting the user to a hosted signing page, then polling an endpoint until the status flips to "completed." That works well enough for simple workflows, but breaks down when enterprise customers expect to stay inside your product, when a healthcare deployment requires a HIPAA-compliant audit trail your team actually controls, and when eIDAS 2.0 compliance across the EU means you can't treat conformance as an afterthought.

This guide covers the full integration path for a production-grade eSignature API, from OAuth2 token acquisition and PDF field placement to embedded signing sessions and webhook-driven completion handling. You'll come away with working patterns for every layer of the stack, all verified against the live Foxit eSign API.

Prerequisites

Everything here runs against real endpoints, so set up a workspace before the first request:

  • Python 3.8+ with pip, plus a virtual environment so the dependencies stay isolated.
  • The requests library for the API calls and Flask for the webhook receiver.
  • A free Foxit eSign developer account, created with no credit card. Activate the API tab in your account settings to get a client_id and client_secret.
  • A tagged sample PDF, so you don't have to author one. This guide uses agreement-signable.pdf, which already carries Text Tags for a single signer.
  • A code editor. VS Code with the Python extension is a good default; any editor works.

Scaffold the workspace in one shot, then store your credentials as environment variables so they never land in source control:

mkdir foxit-esign && cd foxit-esign
python3 -m venv .venv && source .venv/bin/activate
pip install requests flask
export ESIGN_CLIENT_ID="your_client_id"
export ESIGN_CLIENT_SECRET="your_client_secret"
Enter fullscreen mode Exit fullscreen mode

API concepts and compliance baseline

The Foxit eSign API organizes signing workflows around folders. A folder holds one or more documents, a list of parties (signers, approvers, carbon-copy recipients), and the metadata that governs how those parties interact with those documents.

Fields inside a document (signature boxes, text inputs, date stamps) are each assigned to a specific party. You can define that assignment in two ways, either by embedding Text Tags directly in the PDF to bake field definitions into the file itself, or by specifying field ownership in the API call. When both the API request and the PDF tags supply recipient or party information, the API call values take precedence.

Signing order is controlled by three workflow modes, all driven by the signInSequence parameter:

  • Sequential: parties sign one after another in a defined order, and the next signer receives access only when the previous one completes.
  • Parallel: all parties receive signing access simultaneously (signInSequence set to false).
  • Hybrid: a mix of sequential stages, each of which may contain multiple parallel signers.

Before you write a line of code, establish your compliance scope. HIPAA, eIDAS Advanced Electronic Signatures (AES) and Qualified Electronic Signatures (QES), ESIGN, and UETA are supported out of the box. For healthcare deployments, confirm HIPAA configurations with your account team before go-live. For EU deployments, choose the eu1 regional endpoint to keep data residency inside the EU and satisfy eIDAS 2.0 requirements. Make these architecture decisions at the start, because waiting until a customer's legal team raises them costs you a re-architecture.

Authentication

Foxit eSign uses the OAuth 2.0 client-credentials grant. You exchange a client_id and client_secret, available under the API tab in your Foxit eSign account settings, for a short-lived Bearer token that authorizes all subsequent calls.

Get these two things right before you hit the endpoint:

  1. The request body must be form-encoded (application/x-www-form-urlencoded). Sending a JSON body returns HTTP 415. Use requests.post(url, data={...}), not json={...}.
  2. Choose your regional host at token time: na1.foxitesign.foxit.com for US deployments, eu1.foxitesign.foxit.com for EU. Both return the same error shape for bad credentials.
import os
import requests

# Regional endpoint (swap na1 for eu1 for EU data residency)
TOKEN_URL = "https://na1.foxitesign.foxit.com/api/oauth2/access_token"

# Body must be form-encoded. A JSON body returns HTTP 415.
response = requests.post(
    TOKEN_URL,
    data={
        "grant_type": "client_credentials",
        "client_id": os.environ["ESIGN_CLIENT_ID"],
        "client_secret": os.environ["ESIGN_CLIENT_SECRET"],
        "scope": "read-write",
    },
)

token_data = response.json()

# Response fields: access_token, token_type, expires_in, instance_url
access_token = token_data["access_token"]
instance_url = token_data["instance_url"].rstrip("/")  # base URL for all calls

# Attach the Bearer token to every downstream API request
headers = {
    "Authorization": f"Bearer {access_token}",
    "Content-Type": "application/json",
}
Enter fullscreen mode Exit fullscreen mode

The code above reads your credentials from the environment, posts them as a form-encoded body to the regional token endpoint, and unpacks the response. The instance_url it returns is already a full URL, so use it directly as the base for every downstream call rather than prepending a scheme yourself. Cache the token and schedule a refresh before expires_in seconds elapse, because re-acquiring on every request adds unnecessary overhead. For account activation steps, the Foxit eSign developer quickstart covers those without repetition here.

Preparing and sending a document

Create the document

You can supply a PDF two ways. Pass a publicly accessible HTTPS URL in fileUrls and Foxit fetches the file, or send the bytes inline by setting inputType to "base64" and passing the encoded string in a base64FileString array when the PDF lives behind authentication or hasn't been published externally.

Define recipients and field ownership

Define each signer as a party with a first name, last name, email address (emailId), a sequence number, and a permission such as FILL_FIELDS_AND_SIGN. Assign field ownership either via Text Tags embedded in the PDF or by specifying field coordinates in the API call. A Text Tag follows the syntax ${fieldtype:party_number:required:field_name:width}, so a required signature for the first party looks like ${signfield:1:y:____}, where y marks the field required, the party number maps to a signer's sequence, and width is expressed as underscores. If the API call and the PDF tags both specify party information, the API call wins.

Send modes

Two request parameters control how a document goes out. Choose based on whether you need a human review step or an in-app signing experience.

Draft creates the document but holds it for review. Set sendNow to false and no invitation email goes out, which suits flows where a user confirms the recipient list before the envelope is dispatched.

{
  "folderName": "Service Agreement - Acme Corp",
  "sendNow": false,
  "fileUrls": ["https://your-storage.example.com/agreement.pdf"],
  "fileNames": ["agreement.pdf"],
  "parties": [
    {
      "firstName": "Jane",
      "lastName": "Smith",
      "emailId": "jane@acme.com",
      "permission": "FILL_FIELDS_AND_SIGN",
      "sequence": 1
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Direct send dispatches immediately with no intermediate review step. Flip sendNow to true and Foxit emails the signers right away.

{
  "folderName": "NDA - Standard",
  "sendNow": true,
  "fileUrls": ["https://your-storage.example.com/nda.pdf"],
  "fileNames": ["nda.pdf"],
  "parties": [
    {
      "firstName": "Alex",
      "lastName": "Rivera",
      "emailId": "alex@partner.com",
      "permission": "FILL_FIELDS_AND_SIGN",
      "sequence": 1
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Embedded signing adds createEmbeddedSigningSession and a list of embeddedSignersEmailIds, and the response returns a session URL you load inside your application, keeping the signer in your UI from start to finish.

{
  "folderName": "Onboarding - User #4421",
  "sendNow": false,
  "createEmbeddedSigningSession": true,
  "embeddedSignersEmailIds": ["sam@yourapp.com"],
  "signSuccessUrl": "https://yourapp.example.com/signed",
  "fileUrls": ["https://your-storage.example.com/onboarding.pdf"],
  "fileNames": ["onboarding.pdf"],
  "parties": [
    {
      "firstName": "Sam",
      "lastName": "Lee",
      "emailId": "sam@yourapp.com",
      "permission": "FILL_FIELDS_AND_SIGN",
      "sequence": 1
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

One nuance to expect here. sendNow: false on its own produces a DRAFT folder, but pairing it with createEmbeddedSigningSession returns a folderStatus of SHARED, since the folder has to be live for the session URL to open. No email goes out either way. The response carries an embeddedSigningSessions array, and each entry holds emailIdOfSigner, embeddedToken, and the embeddedSessionURL you render in the next step. Omitting embeddedSignersEmailIds returns email id of embedded signer(s) not submitted, so always list your embedded signers explicitly.

Embedded signing and custom branding

With an embedded signing session, the signer never leaves your application. Load the embeddedSessionURL in an iframe or a dedicated view, with no redirect, no hosted page from another domain, and no disorienting context switch mid-workflow.

The Foxit eSign embedded signing session rendered inside a host application, showing the document with interactive signature and date fields and no redirect to an external domain
An embedded session loaded in-app. The signer completes every field without leaving your product.

The session exposes configurable UI options that give you control over the signing surface:

  • Custom logo and colors: pass branding parameters at session creation to match your product's visual identity.
  • Hidden controls: suppress UI elements like the "Add Parties" button when operating in draft or template mode, preventing signers from modifying the recipient list.
  • Self-sign via API: trigger a signing action programmatically without user interaction, which is useful for automated counter-signature workflows where your system is one of the parties.

For a document where every recipient signs inside your app, createEmbeddedSigningSessionForAllParties set to true covers all of them at once rather than naming each email individually.

sequenceDiagram
    participant App as Your Application
    participant API as Foxit eSign API
    participant Signer as Signer
    App->>API: POST /api/oauth2/access_token<br/>(form-encoded client credentials)
    API-->>App: access_token + instance_url
    App->>API: POST /api/folders/createfolder<br/>(PDF + party definitions)
    API-->>App: folderId + embeddedSessionURL
    App->>Signer: Render embeddedSessionURL in iframe<br/>(no redirect)
    Signer->>API: Complete signing session
    API->>App: POST webhook<br/>(folder_executed event)
    App->>App: Validate HMAC, trigger<br/>CRM update or next workflow step

Webhooks and bulk automation

Polling for document status doesn't scale, and it creates unnecessary load on both sides. Register a webhook endpoint instead, and Foxit eSign will POST an event payload to your server whenever a folder status changes, including the folder_executed event you need to trigger downstream actions.

Registration lives on the eSign portal's API settings page under Configure Webhooks, where you set the callback URL, a webhook secret, and the events you want. Store the secret the platform generates, because it authenticates every inbound event. That page is visible only to the account owner, so an admin-level user will not find it, and your endpoint has to be reachable over public HTTPS.

The Foxit eSign Configure Webhooks section of the API settings page, showing the callback URL field, the webhook secret, and per-event checkboxes
The owner-only webhook settings. The event checkboxes control which callbacks reach your endpoint.

The events you can subscribe to are folder_sent, folder_viewed, folder_signed, folder_cancelled, folder_executed, folder_deleted, folder_completed, folder_assigned, and folder_access_code_failure. The event to build on is folder_executed. folder_completed fires once every party has signed, but folder_executed fires after Foxit applies the digital signature and locks the audit trail, so it is the point at which a download gives you the final document.

Implement HMAC secret-key validation on your webhook handler before you move to production. Foxit delivers each callback as <your-url>?signature=<base64>, where the signature is the base64 of an HMAC-SHA-256 over the raw request body keyed with your webhook secret. Verify it against the unparsed bytes, since re-serializing the JSON changes whitespace or key order and breaks the comparison. An annotated Python handler shows how each piece fits together:

import base64
import hashlib
import hmac
import os

from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["ESIGN_WEBHOOK_SECRET"]  # from the Foxit eSign portal


def verify_signature(raw_body: bytes, signature: str) -> bool:
    # Compute the base64 HMAC-SHA256 of the raw body and compare in constant time
    expected = base64.b64encode(
        hmac.new(WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256).digest()
    ).decode()
    return hmac.compare_digest(expected, signature)


@app.route("/webhook/foxit", methods=["POST"])
def foxit_webhook():
    # Read the unparsed bytes and verify against the signature query parameter
    raw_body = request.get_data()
    if not verify_signature(raw_body, request.args.get("signature", "")):
        return jsonify({"error": "invalid signature"}), 403

    # Signature valid, so parse the event and branch on folder_executed
    payload = request.get_json(silent=True) or {}
    folder = payload.get("data", {}).get("folder", {})
    if payload.get("event_name") == "folder_executed":
        handle_completion(folder.get("folderId"))

    return jsonify({"status": "received"}), 200  # acknowledge before heavy work


def handle_completion(folder_id: str) -> None:
    # Move heavy work to a task queue to keep response times low
    pass
Enter fullscreen mode Exit fullscreen mode

In this handler you read the unparsed body first, recompute the base64 HMAC-SHA256 over those exact bytes with your webhook secret, and compare it against the signature query parameter using hmac.compare_digest so the check runs in constant time and never leaks the correct value one character at a time. A mismatch returns 403 before any business logic runs, which stops a spoofed POST to your public URL from triggering downstream work. Only after the signature passes do you parse the JSON, read event_name from the payload and the folder from its data object, and branch on folder_executed. Return 200 promptly and move handle_completion to a task queue if it touches a database or calls an external API.

For bulk automation, the same API surface scales to thousands of agreements. Define a template once, supply per-recipient field pre-fill values for each signer, and the completion webhook fires individually per recipient, giving you fine-grained control over downstream actions without any polling.

Wrapping up

Authenticate once with the client-credentials grant, create a folder with your PDF and party definitions, choose how it goes out (sendNow for draft versus direct, createEmbeddedSigningSession for in-app signing), and let the folder_executed webhook drive everything that follows. The same patterns that work for a single NDA scale to bulk send campaigns with dynamic per-recipient field values, with no changes to the API surface.

To run these patterns against a live API before wiring them into production, Foxit eSign offers a free developer account with a full API playground, no credit card required. Create one at app.developer-api.foxit.com/sign-up and make your first envelope call in minutes. Which signing workflow has given you the most trouble to integrate? Share your experience in the comments.

Top comments (0)