How to Build Contract Lifecycle Workflows with an API

Most teams automate signatures and stop there, leaving drafting, approvals, and data entry manual. This guide shows you how to automate contract workflows with an API, chaining Foxit DocGen, eSign, and PDF Services through eSign webhooks. You get working Python code that takes every contract from template to structured data without a single manual hand-off.
Poor contract management costs the average organization an estimated 9.2% of annual revenue, according to World Commerce & Contracting research into common contracting pitfalls like slow negotiation cycles and missed post-award milestones. A lot of that leakage traces back to a specific integration gap.
Most engineering teams wire up an eSign API, get signature collection working, and call the contract lifecycle solved. Drafting still happens in a shared drive. Approval routing still happens over email. Nobody’s watching for the moment a contract actually gets executed, so the data inside it (dates, values, terms) sits locked in a PDF until someone opens it by hand.
This guide chains three Foxit APIs into one automated pipeline that closes that gap, using DocGen for template-based drafting, eSign for signature routing, and PDF Services for post-execution data extraction, connected end to end by eSign’s own webhook events rather than a separate orchestration product. By the end, you’ll have a contract that goes from template to structured, extracted data with no manual hand-off at any stage.
Prerequisites
Before running any of the code below, set up a local Python environment:
- Python 3.8+
- pip, Python’s package manager
- venv for an isolated project environment
- A code editor such as VS Code with its Python extension
- A free Foxit developer account, no credit card required
mkdir contract-lifecycle-demo && cd contract-lifecycle-demo
python3 -m venv .venv
source .venv/bin/activate
pip install requests flask Each of the three Foxit APIs in this guide authenticates with its own credential pair. DocGen and PDF Services both live behind the same fusion gateway and use a client_id/client_secret header pair, while eSign issues a separate client_id/client_secret pair that it exchanges for a bearer token. Generate all three pairs from the Foxit developer dashboard after signing up, and store them as environment variables (FOXIT_CLIENT_ID/FOXIT_CLIENT_SECRET for DocGen, FOXIT_ESIGN_CLIENT_ID/FOXIT_ESIGN_CLIENT_SECRET for eSign, FOXIT_PDF_CLIENT_ID/FOXIT_PDF_CLIENT_SECRET for PDF Services) rather than hardcoding them.
The contract lifecycle as an API call graph
Before writing code, it helps to map each stage of a contract’s life to the specific API that owns it:
- Drafting: the DocGen API merges structured data into a Word template and returns a populated document.
- Approval routing and signing: the eSign API creates a signing envelope, defines who signs in what order, and renders the signing UI (embedded in your app or via a redirect).
- Completion handoff: eSign fires a webhook event the moment every signature lands, which is the trigger for everything downstream.
- Post-execution extraction: the PDF Services API pulls structured data (party names, dates, values) back out of the now-signed contract.
Four API calls own the whole lifecycle. No fifth orchestration layer is required. eSign’s own party sequencing handles approval routing, and its own webhook subscriptions handle the handoff to downstream systems, which is the pattern this guide builds.
Generating the contract from a template
The DocGen API works from a Word .docx template with merge fields written as {{ field_name }}. This guide uses a Master Services Agreement template with fields for both parties’ names, addresses, signatories, the effective date, term, contract value, payment schedule, and governing law.
Before merging data into a template, call the Analyze Document endpoint to get back the exact list of tags the template actually contains. This step is not optional. Getting the field names wrong in your merge payload silently drops that field rather than throwing an error, so confirming the real tag list first is what catches a typo before it ships into a signed contract. GenerateDocumentBase64 also rejects .docx payloads larger than 4 MB once they’re base64-encoded, so a template that grows past that (usually from embedded images or fonts) needs slimming down before it reaches the API, either by compressing images in Word’s Picture Format tools, dropping embedded fonts and OLE objects, or splitting an oversized template into smaller pieces.
import os
import base64
import requests
BASE_URL = "https://na1.fusion.foxit.com"
HEADERS = {
"client_id": os.environ["FOXIT_CLIENT_ID"],
"client_secret": os.environ["FOXIT_CLIENT_SECRET"],
}
with open("contract_standard_signable.docx", "rb") as f:
template_b64 = base64.b64encode(f.read()).decode()
analyze_response = requests.post(
f"{BASE_URL}/document-generation/api/AnalyzeDocumentBase64",
headers={**HEADERS, "Content-Type": "application/json"},
json={"base64FileString": template_b64, "fileName": "contract_standard_signable.docx"},
)
analyze_response.raise_for_status()
tags = analyze_response.json()["singleTagsString"].split(",")
print(tags) Run against the sample template, this returns singleTagsString, a comma-separated list of every merge field the template contains. This is the exact key set your merge payload needs to match. Run this command from your terminal:
python3 analyze_tags.py The expected output should look like the following:
['effective_date', 'party_a_name', 'party_a_address_line1', 'party_a_address_city', 'party_a_address_state', 'party_a_signatory_name', 'party_a_signatory_title', 'party_b_name', 'party_b_address_line1', 'party_b_address_city', 'party_b_address_state', 'party_b_signatory_name', 'party_b_signatory_title', 'term_months', 'contract_value', 'payment_schedule', 'governing_law'] The endpoint’s own reference confirms the same request and response shape:
With the confirmed tag list in hand, call the generation endpoint with a documentValues object keyed to those tags:
document_values = {
"effective_date": "October 1, 2026",
"party_a_name": "Northwind Robotics, Inc.",
"party_a_address_line1": "200 Innovation Way",
"party_a_address_city": "Austin",
"party_a_address_state": "TX",
"party_a_signatory_name": "Jordan Lee",
"party_a_signatory_title": "VP of Operations",
"party_b_name": "Blue Harbor Analytics LLC",
"party_b_address_line1": "55 Market Street",
"party_b_address_city": "San Francisco",
"party_b_address_state": "CA",
"party_b_signatory_name": "Morgan Diaz",
"party_b_signatory_title": "Managing Director",
"term_months": 12,
"contract_value": 48000,
"payment_schedule": "Net 30, monthly",
"governing_law": "State of Delaware",
}
generate_response = requests.post(
f"{BASE_URL}/document-generation/api/GenerateDocumentBase64",
headers={**HEADERS, "Content-Type": "application/json"},
json={
"base64FileString": template_b64,
"fileName": "contract_standard_signable.docx",
"documentValues": document_values,
"outputFormat": "pdf",
},
)
generate_response.raise_for_status()
result = generate_response.json()
with open("generated_contract.pdf", "wb") as f:
f.write(base64.b64decode(result["base64FileString"])) This code sends the same base64-encoded template alongside the merge data and an outputFormat of pdf, and gets back a response containing the generated file as another base64 string, decoded and written to disk in the final step. The contract_value field, formatted in the template as {{ contract_value \# "$#,##0.00" }}, renders as $48,000.00 in the output without any currency formatting logic in the Python code; the formatting instruction lives entirely in the Word template.
Run against the sample data above, that call produces a fully merged contract, ready for the signature block eSign picks up next:
Routing for approval and signature
The generated PDF above already contains eSign Text Tags in its signature block, ${signfield:1:y:__________} for the first signer and ${signfield:2:y:__________} for the second, embedded directly in the Word template alongside matching ${datefield:...} tags. Text Tags and DocGen merge fields don’t conflict; DocGen only touches {{ }}-style tags, so the eSign tags pass through untouched and get parsed by eSign once the document reaches it.
Creating the envelope and defining who signs, and in what order, is one call:
esign_response = requests.post(
"https://na1.foxitesign.foxit.com/api/oauth2/access_token",
data={
"client_id": os.environ["FOXIT_ESIGN_CLIENT_ID"],
"client_secret": os.environ["FOXIT_ESIGN_CLIENT_SECRET"],
"grant_type": "client_credentials",
"scope": "read-write",
},
)
esign_response.raise_for_status()
esign_token = esign_response.json()["access_token"]
with open("generated_contract.pdf", "rb") as f:
contract_b64 = base64.b64encode(f.read()).decode()
folder_response = requests.post(
"https://na1.foxitesign.foxit.com/api/folders/createfolder",
headers={"Authorization": f"Bearer {esign_token}"},
json={
"folderName": "MSA - Northwind Robotics / Blue Harbor Analytics",
"sendNow": False,
"processTextTags": True,
"inputType": "base64",
"base64FileString": [contract_b64],
"fileNames": ["msa.pdf"],
"createEmbeddedSigningSession": True,
"embeddedSignersEmailIds": ["[email protected]", "[email protected]"],
"parties": [
{"firstName": "Jordan", "lastName": "Lee", "emailId": "[email protected]",
"permission": "FILL_FIELDS_AND_SIGN", "sequence": 1},
{"firstName": "Morgan", "lastName": "Diaz", "emailId": "[email protected]",
"permission": "FILL_FIELDS_AND_SIGN", "sequence": 2},
],
},
)
folder_response.raise_for_status()
folder = folder_response.json()
folder_id = folder["folder"]["folderId"]
sessions = folder["embeddedSigningSessions"] eSign authenticates separately from DocGen and PDF Services, with its own credential pair and its own OAuth2 token exchange (form-encoded, not JSON). The parties array is where approval routing actually lives. Each party’s sequence number controls signing order, here Jordan signs first and Morgan second, and the exact rules for how sequence numbers interact (whether two parties can share a sequence for parallel signing, for instance) are documented on the Parties reference rather than repeated here, since that’s the definitive source and it’s worth reading directly before you rely on the exact semantics. processTextTags: true tells eSign to parse the ${signfield:...} tags already embedded in the PDF into real, positioned signature fields, which is what makes the fields the reader sees next actually show up.
createEmbeddedSigningSession: true returns a ready-to-render embeddedSessionURL for each signer in the response, meaning the signing UI loads inside your own app in an iframe rather than sending the signer an email that takes them to a hosted Foxit page. This is the embedded option; the alternative is redirect-based delivery, where sendNow: true and no embedded session flag sends signers a standard email invitation instead. Embedded signing keeps the whole flow inside your product; redirect delivery is simpler to wire up but hands the signer off to a separate page.
Once every party has signed, the folder moves through SHARED, then PARTIALLY SIGNED as each signer completes their fields, then COMPLETED, and finally EXECUTED. That transition to EXECUTED is the event this guide’s next section listens for.
Confirming lands the signer on eSign’s own result page, shown below.
Once the folder reaches EXECUTED, retrieve the signed PDF with a single download call:
download_response = requests.get(
"https://na1.foxitesign.foxit.com/api/folders/download",
headers={"Authorization": f"Bearer {esign_token}"},
params={"folderId": folder_id},
)
download_response.raise_for_status()
with open("signed_contract.pdf", "wb") as f:
f.write(download_response.content) The response comes back as application/octet-stream rather than application/pdf, so write the raw bytes to disk without trying to sniff or validate the content type. The downloaded file contains every filled field value plus each signer’s certificate, which is the file the extraction step below reads from.
Extracting structured data from the signed contract
Once eSign’s completion webhook fires (covered in the next section), the signed PDF is ready for extraction. This reuses the same asynchronous pattern the PDF Services API uses everywhere, uploading the file, starting a task, polling it, then downloading the result.
pdf_headers = {
"client_id": os.environ["FOXIT_PDF_CLIENT_ID"],
"client_secret": os.environ["FOXIT_PDF_CLIENT_SECRET"],
}
with open("signed_contract.pdf", "rb") as f:
upload_response = requests.post(
f"{BASE_URL}/pdf-services/api/documents/upload",
headers=pdf_headers,
files={"file": ("signed_contract.pdf", f, "application/pdf")},
)
document_id = upload_response.json()["documentId"]
extract_response = requests.post(
f"{BASE_URL}/pdf-services/api/documents/pdf-structural-extract",
headers={**pdf_headers, "Content-Type": "application/json"},
json={"documentId": document_id},
)
task_id = extract_response.json()["taskId"] This code uploads the signed PDF to the PDF Services API and gets back a documentId, then calls the pdf-structural-extract endpoint with that id to kick off an asynchronous extraction task and get back a taskId to track its progress.
From here, poll the task status endpoint until its status field reads COMPLETED, then download the resulting ZIP and pull out StructureInfo.json:
import time
import io
import json
import zipfile
while True:
status_response = requests.get(
f"{BASE_URL}/pdf-services/api/tasks/{task_id}", headers=pdf_headers
)
status_response.raise_for_status()
status = status_response.json()
if status["status"] == "COMPLETED":
result_document_id = status["resultDocumentId"]
break
if status["status"] == "FAILED":
raise RuntimeError(f"Extraction task failed: {status}")
time.sleep(3)
zip_response = requests.get(
f"{BASE_URL}/pdf-services/api/documents/{result_document_id}/download",
headers=pdf_headers,
)
zip_response.raise_for_status()
with zipfile.ZipFile(io.BytesIO(zip_response.content)) as archive:
structure_info = json.loads(archive.read("StructureInfo.json"))
for element in structure_info["analyzeResult"]["elements"]:
if element["type"] in ("title", "head", "paragraph"):
text = " ".join(element["content"]["text"].split())
print(element["type"], text) The loop polls every three seconds until status reads COMPLETED, at which point resultDocumentId names the file to download as a ZIP archive. Inside that archive, StructureInfo.json holds the contract’s text broken into typed elements (titles, heads, paragraphs, and tables) under analyzeResult.elements, each carrying its content, position on the page, and a region.boundingBox polygon; the final loop walks the title, head, and paragraph elements and prints their normalized text, which is the starting point for mapping the output into a party_a/effective_date/contract_value schema. Table elements carry a different shape (a content.body grid keyed by row and column index rather than the region/content.text fields shown above), and mapping that full structure into a business schema is a big enough topic that it deserves its own treatment, which a companion article to this one covers. The full endpoint spec, including the task status values and the exact response shape, is documented in the Foxit API reference.
Run this command from your terminal:
python3 extract_structure.py The expected output should look like the following, run against this guide’s own signed contract:
title MASTER SERVICES AGREEMENT
paragraph This Master Services Agreement (the Agreement) is entered into as of October 1, 2026 by and between the parties listed below.
head Parties
paragraph Party A: Northwind Robotics, Inc.
paragraph Address: 200 Innovation Way, Austin, TX
paragraph Signatory: Jordan Lee, VP of Operations
paragraph Party B: Blue Harbor Analytics LLC
paragraph Address: 55 Market Street, San Francisco, CA
paragraph Signatory: Morgan Diaz, Managing Director
head Term and Payment
paragraph Effective Date: October 1, 2026
paragraph Term: 12 months (fixed term, no automatic renewal)
paragraph Contract Value: $48,000.00
paragraph Payment Schedule: Net 30, monthly
paragraph Governing Law: State of Delaware
head Termination
paragraph This Agreement expires at the end of the term stated above. The parties may extend the term only by signing a new written agreement.
head Signatures
paragraph Party A: ${signfield:1:y:__________} Date: 09/24/2026 ${datefield:1:y::__________} Signer ID: RQFWP6D419...
paragraph Party B
paragraph Signer ID: H9T1D10019...
paragraph ______} Date: 09/24/2026 ${datefield:2:y::__________} The signature block splits across several ragged elements rather than one clean paragraph, since the extractor treats the signed field’s rendered signature image and the surrounding text tags as separate content runs. That’s real extractor behavior worth planning for if you’re parsing this section specifically.
Connecting the chain to downstream systems with webhooks
None of this becomes an automated pipeline until something reacts the moment a contract gets executed. That’s the job of eSign’s own webhook subscriptions.
Registering a webhook is a portal step, not an API call. Log in to the eSign account as its owner and open API Settings, where a “Configure Webhooks” section takes a webhook URL, a webhook secret, and a set of event checkboxes to subscribe to (folder_completed is the one that matters here). This settings page is owner-only; a standard team-member login won’t see it.
Once registered, Foxit delivers each event as a POST to your URL with a signature query parameter, a base64-encoded HMAC-SHA-256 digest of the raw request body computed with your webhook secret. Verifying that signature before trusting the payload is the whole point of having a secret in the first place:
import hmac
import hashlib
import base64
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["FOXIT_ESIGN_WEBHOOK_SECRET"].encode()
def trigger_extraction_and_sync(folder_id):
print(f"[webhook] verified signature, folder_completed for folder {folder_id}, kicking off extraction")
# kick off the PDF Services extraction from the previous section and push the result to a CRM or ERP
@app.route("/webhook/foxit", methods=["POST"])
def foxit_webhook():
received_sig = request.args.get("signature", "")
raw_body = request.get_data()
computed_sig = base64.b64encode(
hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).digest()
).decode()
if not hmac.compare_digest(received_sig, computed_sig):
abort(403)
payload = request.json
event_name = payload.get("event_name")
folder_id = payload.get("data", {}).get("folder", {}).get("folderId")
if event_name == "folder_completed":
trigger_extraction_and_sync(folder_id)
return "", 200 This handler pulls the signature off the query string, recomputes the same HMAC digest server-side over the raw request body, and compares the two with hmac.compare_digest, a constant-time comparison that avoids leaking timing information an attacker could use to guess the correct signature one byte at a time. Only once that check passes does it parse the JSON payload and branch on event_name. A single folder_completed webhook fires once the envelope is fully executed, and by the time it arrives, its own data.folder.folderStatus field already reads EXECUTED, so there’s no separate event to wait for after this one. trigger_extraction_and_sync should still be written idempotently, since any webhook consumer needs to tolerate a duplicate delivery of the same event without double-processing it.
With the handler running locally, run this command from your terminal to send it a real folder_completed payload, correctly HMAC-signed with the same secret, for a folder that genuinely reached EXECUTED in this guide’s own signing run:
python3 -c "
import hmac, hashlib, base64, json, requests
WEBHOOK_SECRET = b'your_webhook_secret'
payload = {
'event_name': 'folder_completed',
'data': {'folder': {'folderId': 36099927, 'folderStatus': 'EXECUTED'}},
}
raw_body = json.dumps(payload).encode()
sig = base64.b64encode(hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).digest()).decode()
r = requests.post(f'http://127.0.0.1:5055/webhook/foxit?signature={sig}', data=raw_body, headers={'Content-Type': 'application/json'})
print(r.status_code, r.text)
" The expected output should look like the following, in the terminal running the Flask app:
[webhook] verified signature, folder_completed for folder 36099927, kicking off extraction
127.0.0.1 - - [24/Sep/2026 17:45:04] "POST /webhook/foxit?signature=uK1HvZU6dmH9oH4cGhsm8sNeEBZA1hremgqaDYzMZyc= HTTP/1.1" 200 - Before wiring trigger_extraction_and_sync to a production system, register a real webhook URL and send a real envelope through the whole chain to confirm the callback arrives and verifies. That first live run is also where the handful of mistakes below tend to surface.
Common mistakes
A few failure modes show up repeatedly when building this pipeline, and each one is easy to avoid once you know to look for it.
- Tag names in the merge payload don’t match the template exactly.
GenerateDocumentBase64doesn’t error on an unrecognized key indocumentValues; it silently drops that field from the output. Always confirm the real tag list withAnalyzeDocumentBase64first rather than trusting a remembered field name, and diff the payload keys against the returnedsingleTagsStringbefore sending a merge request. - Word’s autocorrect turns straight quotes into smart quotes inside a merge field. A field typed as
{{ party_a_name }}can end up with a curly apostrophe or quote character if autocorrect touches it while editing the template, which breaks the tag silently the same way a misspelled name does. Paste field names in rather than retyping them, and re-runAnalyzeDocumentBase64after any manual template edit to confirm the tag count hasn’t changed. - A Text Tag with a space instead of underscores never becomes a signature field.
${signfield:1: }is parsed by eSign as inert literal text rather than a fillable field, so the signer sees the placeholder text but nothing to sign. The working form always uses underscores for the fill width, as in${signfield:1:y:__________}. If a signing session reports fewer required fields than the template has tags, this is the first thing to check. - The template’s
.docxfile exceeds the 4 MB DocGen cap after base64 encoding. Embedded images and fonts are the usual cause.GenerateDocumentBase64rejects the payload outright rather than truncating it, so compress images, strip embedded fonts and OLE objects, or split the template before it reaches that size.
Frequently asked questions
What is a contract lifecycle management API?
A contract lifecycle management (CLM) API is a programmatic interface, or in this case a small set of them working together, for automating each stage of a contract’s life (drafting, review, signing, storage, and post-execution data extraction) without manual hand-offs between stages.
How do you automate contract workflows with an API?
Chain the stages together by generating a document from a template, routing it for signing with role-based sequencing, capturing the completion event through a webhook rather than polling, and extracting structured data from the executed document asynchronously.
What is the difference between an eSign API and a CLM API?
An eSign API handles the signature step, creating envelopes, defining signers, and collecting signatures. A CLM API (or, as this guide shows, a composed set of APIs) spans the full lifecycle, from drafting through post-execution data extraction, with the eSign step as one stage inside a larger pipeline.
How do you extract data from a signed PDF contract programmatically?
Upload the signed PDF to get a documentId, call the structural extraction endpoint to start an asynchronous task and get back a taskId, then poll the task status endpoint until the result is ready and download the structured JSON.
Get started
The chain built in this guide, DocGen for drafting, eSign for signing and approval routing, PDF Services for extraction, and eSign’s own webhooks for the handoff between them, covers a contract’s full lifecycle with four API calls and no separate orchestration product. What it unlocks once it’s running unattended includes an audit trail that doesn’t depend on someone remembering to check a shared drive, renewal automation triggered off the actual execution date rather than a calendar reminder, and CRM or ERP sync that happens the moment a contract closes instead of whenever someone gets around to data entry.
Start building with a free Foxit developer account, no credit card required, using the same DocGen, eSign, and PDF Services endpoints shown above.
How to Extract Parties, Dates, and Key Terms from Contracts Using the Foxit API

Renewal dates slip past when contract terms live in PDFs and spreadsheets. This tutorial shows you how to pull parties, dates, notice periods, and liability caps from a signed contract with the Foxit PDF Services API, then add the signing audit trail from Foxit eSign. You end up with one JSON record per contract, ready for a CLM, CRM, or renewal-alert script.
Most teams still handle contract metadata the same way they did a decade ago. Someone opens the signed PDF, copies the effective date and renewal terms into a spreadsheet, and hopes nobody forgets to check back before the notice period closes. When the process breaks down, it usually breaks down quietly. A regex script built for one contract template chokes on the next one, and a renewal date slips past unnoticed.
This tutorial builds a two-stage pipeline that gets the same information out of a contract programmatically. The first stage uses Foxit’s PDF Services API to extract structured content metadata (party names, dates, notice periods, liability terms) from a signed PDF. The second stage calls the Foxit eSign API’s activity history endpoint to pull the signing audit trail, so the final record captures both what the contract says and how it got signed. By the end, you’ll have one JSON object per contract that’s ready to feed into a CLM tool, a CRM, or a renewal-alert script, built from a real API response rather than a screen-scrape.
What contract metadata looks like in practice
Before writing any code, it helps to know what you’re building toward. The two API calls this tutorial makes run independently against the same signed PDF and merge into one record, as shown below.
The goal isn’t a wall of extracted text. It’s a small, typed record with the fields that actually drive downstream decisions:
{
"party_a": "Northwind Robotics",
"party_b": "Blue Harbor Analytics LLC",
"effective_date": "March 3, 2026",
"renewal_date": "March 3, 2028",
"termination_notice_days": 30,
"liability_cap": "$50,000"
} That’s the schema this tutorial targets. Everything downstream, a renewal-alert cron job, a query against a CLM database, a BI dashboard, consumes a record shaped like this. The contract’s prose is the source of truth, but the schema is what actually gets used. A structural extraction API gets you from one to the other without hand-writing a parser for every contract template your organization uses.
Prerequisites
You’ll need the following before starting:
- Python 3.8 or later, with pip for installing packages.
- A virtual environment tool such as venv (included with Python) to isolate this project’s dependencies.
- The requests library for making HTTP calls.
- A code editor. VS Code with the Python extension works well for this walkthrough.
- A free Foxit developer account. Sign up here. No credit card is required.
Set up your workspace:
mkdir contract-extraction && cd contract-extraction
python3 -m venv .venv
source .venv/bin/activate
pip install requests Those four commands create the project folder, create and activate an isolated virtual environment inside it, and install the one library this tutorial’s code depends on.
Authenticating with the Foxit PDF Services API
Once you’ve signed up for a free developer account, the sign-up flow provisions API credentials immediately. There’s no separate approval step.
When you sign in, your dashboard has a client_id and client_secret. The PDF Services API and DocGen API authenticate with these two values sent as request headers, with no OAuth token exchange required.
export FOXIT_CLIENT_ID="your_client_id"
export FOXIT_CLIENT_SECRET="your_client_secret" Foxit eSign, which this tutorial’s second stage uses to pull signing metadata, runs on a separate host with its own token-based authentication, and needs a one-time activation step from your dashboard (look for “Get started with eSign”) before its endpoints will accept calls. Activate it now so it’s ready by the time you reach that section; the eSign section below covers the token exchange itself.
For the full authentication reference, see the Foxit API docs, or try a call directly against the interactive API reference before writing any code.
Uploading a contract and calling the extraction endpoint
The extraction flow uses two calls, uploading the PDF to get a document ID, then posting that document ID to the structural extraction endpoint.
This tutorial uses a short sample NDA between two fictional companies, with a purpose clause, a term-and-renewal clause, a termination clause, a liability cap, and a key-dates summary table. It’s built to mirror the clause structure of a real commercial NDA.
Step 1: Upload the document
import os
import requests
BASE_URL = "https://na1.fusion.foxit.com"
HEADERS = {
"client_id": os.environ["FOXIT_CLIENT_ID"],
"client_secret": os.environ["FOXIT_CLIENT_SECRET"],
}
with open("nda_sample.pdf", "rb") as f:
upload_response = requests.post(
f"{BASE_URL}/pdf-services/api/documents/upload",
headers=HEADERS,
files={"file": ("nda_sample.pdf", f, "application/pdf")},
)
upload_response.raise_for_status()
document_id = upload_response.json()["documentId"]
print(document_id) In this code, you open the local PDF, POST it as multipart form data to the upload endpoint, and read back a documentId. That ID is a handle to the file inside Foxit’s system; every subsequent call in this pipeline references the document by this ID rather than re-uploading the file.
Run this command from your terminal:
python3 step1_upload.py The expected output should look like the following:
6ab519b2c7ca700694bf3174 Step 2: Call the structural extraction endpoint
The extraction call returns immediately with a taskId, and the client polls the task endpoint every two seconds until the status flips to COMPLETED, as shown below.
import time
extract_response = requests.post(
f"{BASE_URL}/pdf-services/api/documents/pdf-structural-extract",
headers={**HEADERS, "Content-Type": "application/json"},
json={"documentId": document_id},
)
extract_response.raise_for_status()
task_id = extract_response.json()["taskId"]
while True:
task_response = requests.get(f"{BASE_URL}/pdf-services/api/tasks/{task_id}", headers=HEADERS)
task = task_response.json()
print(task)
if task["status"] == "COMPLETED":
result_document_id = task["resultDocumentId"]
break
if task["status"] == "FAILED":
raise RuntimeError(task.get("error"))
time.sleep(2)
download_response = requests.get(
f"{BASE_URL}/pdf-services/api/documents/{result_document_id}/download",
headers=HEADERS,
)
with open("extraction_result.zip", "wb") as f:
f.write(download_response.content) This code starts the extraction as an asynchronous task, since structural analysis takes longer than a typical request-response cycle. It posts the documentId, gets back a taskId, then polls the task status endpoint every two seconds until the status flips to COMPLETED or FAILED. Once complete, the task response includes a resultDocumentId pointing at a ZIP archive, downloaded in the final step. On the sample NDA, this whole flow (upload, extract, poll, download) completes in well under ten seconds.
Run this command from your terminal:
python3 step2_extract.py The expected output should look like the following:
{'taskId': '6ab519d30a17a67b90dea85d', 'status': 'IN_PROGRESS', 'progress': 20}
{'taskId': '6ab519d30a17a67b90dea85d', 'status': 'COMPLETED', 'progress': 100, 'resultDocumentId': '6ab519d5316b427cfaec5212'} Unzip extraction_result.zip and, on this run, you get exactly two files. Run this command from your terminal:
unzip extraction_result.zip && ls The expected output should look like the following:
page_p0.pdf_0.png
StructureInfo.json That JSON is the real output of this article. Here’s an abbreviated version of what it actually contains, taken from the same live run against the sample NDA (the full StructureInfo.json from this run is available if you want to see every element):
{
"analyzeResult": {
"elements": [
{
"type": "title",
"content": { "text": "Mutual Non-Disclosure Agreement" },
"region": { "page": 1, "boundingBox": [142, 70, 470, 70, 470, 99, 142, 99] },
"id": "title1"
},
{
"type": "paragraph",
"content": {
"text": "This Mutual Non-Disclosure Agreement (the \"Agreement\") is entered into as of March 3, 2026 (the \"Effective Date\"), by and between Northwind Robotics, Inc., ... (\"Party A\"), and Blue Harbor Analytics LLC, ... (\"Party B\")."
},
"id": "paragraph1"
},
{
"type": "head",
"content": { "text": "2. Term and Renewal" },
"id": "head2",
"level": 1
},
{
"type": "table",
"content": {
"body": {
"rowCount": 6,
"columnCount": 2,
"cells": [
{ "paragraph": { "content": { "text": "Effective Date" } }, "rowIndex": 1, "columnIndex": 0 },
{ "paragraph": { "content": { "text": "March 3, 2026" } }, "rowIndex": 1, "columnIndex": 1 }
]
}
}
}
]
}
} The response is a flat list of elements, each carrying a type (title, head, paragraph, or table, among others) and the element’s content.text. For title, head, and paragraph elements, a region.boundingBox gives its position on the page. table elements don’t follow that shape, since their region is an empty object, and position instead lives in a plural regions array (alongside a plural scores array), so code that reads element["region"]["boundingBox"] generically across every element type raises a KeyError the moment it reaches a table. Paragraphs carry plain text; tables carry a content.body.cells array with rowIndex/columnIndex coordinates instead of a 2D array, since that’s the shape that survives merged cells and irregular tables cleanly. Every field name here comes directly from a real response; none of it is asserted from documentation alone.
Mapping the raw extraction response to a typed schema
StructureInfo.json describes the document’s structure. It doesn’t hand you party_a and effective_date directly, so the next step is a small amount of post-processing that turns the element list into the schema from the introduction.
import json
import re
import zipfile
with zipfile.ZipFile("extraction_result.zip") as z:
with z.open("StructureInfo.json") as f:
structure = json.load(f)
elements = structure["analyzeResult"]["elements"]
def normalize(text):
return " ".join(text.split())
paragraphs = [normalize(el["content"]["text"]) for el in elements if el["type"] == "paragraph"]
full_text = " ".join(paragraphs)
record = {}
match = re.search(r'([A-Z][\w\s,.]+?)\s*\("Party A"\)', full_text)
if match:
record["party_a"] = match.group(1).split(",")[0].strip()
match = re.search(r'([A-Z][\w\s,.]+?)\s*\("Party B"\)', full_text)
if match:
record["party_b"] = match.group(1).split(",")[0].strip()
match = re.search(r'as of\s+([A-Z][a-z]+ \d{1,2}, \d{4})', full_text)
if match:
record["effective_date"] = match.group(1)
match = re.search(r'expiring on\s+([A-Z][a-z]+ \d{1,2}, \d{4})', full_text)
if match:
record["renewal_date"] = match.group(1)
match = re.search(r'upon \w+ \((\d+)\) days.{0,10}prior written notice', full_text)
if match:
record["termination_notice_days"] = int(match.group(1))
match = re.search(r'\$([\d,]+)\)?\s*\(the "Liability Cap"\)', full_text)
if match:
record["liability_cap"] = f"${match.group(1)}"
print(json.dumps(record, indent=2)) This code reads the ZIP directly without extracting it to disk, then joins every paragraph element’s text into one searchable string. Because the raw text carries mid-string line breaks where a sentence wraps across a line in the source PDF, the normalize helper collapses any run of whitespace down to a single space before the string gets used, which is what keeps the regular expressions below from silently failing on a hidden newline. From there, a handful of targeted patterns pull out the fields this NDA’s clause language reliably contains, including the parenthetical ("Party A")/("Party B") labels this template uses to tag each signer, the “as of” and “expiring on” date phrases, the notice-period clause, and the dollar figure tagged (the "Liability Cap") in the liability clause. The liability clause writes the figure as fifty thousand dollars ($50,000) (the "Liability Cap"), so the regex allows an optional closing parenthesis between the digits and the label rather than assuming the label follows the number directly. Run against the sample NDA, this produces exactly the record shown in the introduction.
Run this command from your terminal:
python3 map_schema.py The expected output should look like the following:
{
"party_a": "Northwind Robotics",
"party_b": "Blue Harbor Analytics LLC",
"effective_date": "March 3, 2026",
"renewal_date": "March 3, 2028",
"termination_notice_days": 30,
"liability_cap": "$50,000"
} A production version of this would likely walk the table element’s cells too, since the sample NDA’s key-dates table restates the same fields in a much more parseable, label-value shape, but the paragraph-text approach shown here works even for contracts that don’t include a summary table.
Enriching the record with eSign audit metadata
The extraction above tells you what the contract says. It doesn’t tell you whether it’s actually been signed, or by whom, or when. That’s what Foxit eSign’s activity history endpoint adds, closing the loop between “here’s what the contract promises” and “here’s proof it was executed.”
eSign authenticates separately from PDF Services, with its own client ID and secret and its own token exchange. Find this pair by signing in to the eSign portal as the account owner, opening the account’s API settings page, and clicking “View All Credentials” (this is owner-gated and triggers a one-time email confirmation, so a non-owner login won’t see the button). Export them the same way you exported the PDF Services pair:
export FOXIT_ESIGN_CLIENT_ID="your_esign_client_id"
export FOXIT_ESIGN_CLIENT_SECRET="your_esign_client_secret" With those in place, exchange them for a bearer token:
esign_response = requests.post(
"https://na1.foxitesign.foxit.com/api/oauth2/access_token",
data={
"client_id": os.environ["FOXIT_ESIGN_CLIENT_ID"],
"client_secret": os.environ["FOXIT_ESIGN_CLIENT_SECRET"],
"grant_type": "client_credentials",
"scope": "read-write",
},
)
esign_response.raise_for_status()
esign_token = esign_response.json()["access_token"] This posts your eSign credentials as form-encoded data (not JSON) and gets back a bearer token, which every subsequent eSign call uses in an Authorization header. This is a separate credential pair and a separate token from the PDF Services calls above; see the eSign activation note in the “Authenticating with the Foxit PDF Services API” section above if you haven’t turned this on yet.
Sending the same credentials as a JSON body instead of data= is a real, reproducible mistake. Run this command from your terminal to reproduce it:
python3 -c "
import os, requests
r = requests.post(
'https://na1.foxitesign.foxit.com/api/oauth2/access_token',
json={
'client_id': os.environ['FOXIT_ESIGN_CLIENT_ID'],
'client_secret': os.environ['FOXIT_ESIGN_CLIENT_SECRET'],
'grant_type': 'client_credentials',
'scope': 'read-write',
},
)
print(r.status_code, r.reason)
" The expected output should look like the following:
415 Unsupported Media Type Sending the NDA for signature
The activity history endpoint only has something to report once a contract has actually gone out for signature, so this tutorial sends the same sample NDA through eSign to get a real folder ID to query. POST /api/folders/createfolder accepts the file directly as base64, which sidesteps hosting it somewhere with a public URL first:
import base64
with open("nda_sample.pdf", "rb") as f:
encoded_pdf = base64.b64encode(f.read()).decode("utf-8")
create_folder_response = requests.post(
"https://na1.foxitesign.foxit.com/api/folders/createfolder",
headers={"Authorization": f"Bearer {esign_token}"},
json={
"folderName": "nda_sample",
"inputType": "base64",
"base64FileString": [encoded_pdf],
"fileNames": ["nda_sample.pdf"],
"parties": [
{"partyNumber": 1, "name": "Alex Rivera", "email": "[email protected]"}
],
"sendNow": True,
},
)
create_folder_response.raise_for_status()
folder_id = create_folder_response.json()["folder"]["folderId"]
print(folder_id) This code base64-encodes the same nda_sample.pdf used in the extraction step and posts it to the folder-creation endpoint, along with inputType: "base64" (required whenever the file comes as base64 rather than a fileUrls array) and a single party to sign. sendNow: true dispatches the invitation immediately instead of leaving the folder in DRAFT, and the response nests the new folder’s ID at folder.folderId, which is the folder_id the next call reads.
Once a contract has actually been sent for signature, its activity history is available by folder ID:
history_response = requests.get(
f"https://na1.foxitesign.foxit.com/api/folders/viewActivityHistory?folderId={folder_id}",
headers={"Authorization": f"Bearer {esign_token}"},
)
history_response.raise_for_status()
activity = history_response.json()["details"]
print(json.dumps(activity, indent=2)) Called against a real folder right after sending it for signature, this is the actual response:
{
"folderId": 36096051,
"enclosedDocuments": "nda_sample",
"envelopeRecipients": "Alex Rivera",
"author": "annika annika",
"status": "SHARED",
"dateCreated": "24 Sep 2026, 02:48:35, PDT",
"dateSent": "24 Sep 2026, 02:48:35, PDT",
"activities": [
{
"activity": "annika annika created this folder",
"folderStatus": "CREATED",
"action": "Created",
"time": "24 Sep 2026, 02:48:35, PDT",
"source": "API"
},
{
"activity": "Invitation sent to Alex Rivera ( [email protected])",
"folderStatus": "SHARED",
"action": "Invitation Sent",
"time": "24 Sep 2026, 02:48:35, PDT",
"source": "Web"
}
]
} The endpoint refers to the contract’s signing container as a “folder” in the API’s own field names (folderId, folderStatus), even though the eSign portal’s UI labels the same thing an “envelope.” Both terms describe the same object; this article uses “folder” since that’s what the API actually returns. details.status tracks the folder’s lifecycle (SHARED once sent, moving through PARTIALLY SIGNED or COMPLETED as signers act, ending at EXECUTED once every signature is applied and finalized), and activities is an ordered log of every event with a timestamp and a source (API or Web, depending on how the action was triggered). Before you build this into a pipeline, know that this endpoint only works once a folder has actually been shared or sent. Calling it against a folder still in DRAFT status returns an error rather than an empty activity list, so don’t rely on it as a general-purpose status check before the folder is sent. Run this command from your terminal, pointed at a folder still in DRAFT status:
python3 view_activity_history.py The expected output should look like the following:
{"result": "error", "error_description": "logs of a non-shared folder can not be viewed."}
The final step merges this into the schema dict built earlier:
record["folder_status"] = activity["status"]
record["signing_activity"] = [
{"event": a["action"], "time": a["time"]} for a in activity["activities"]
]
print(json.dumps(record, indent=2)) That gives you one complete record per contract, its extracted terms and its signing history, in a single dict. Run this command from your terminal:
python3 merge_record.py The expected output should look like the following, using the same folder queried above:
{
"party_a": "Northwind Robotics",
"party_b": "Blue Harbor Analytics LLC",
"effective_date": "March 3, 2026",
"renewal_date": "March 3, 2028",
"termination_notice_days": 30,
"liability_cap": "$50,000",
"folder_status": "SHARED",
"signing_activity": [
{ "event": "Created", "time": "24 Sep 2026, 02:48:35, PDT" },
{ "event": "Invitation Sent", "time": "24 Sep 2026, 02:48:35, PDT" }
]
} With these three steps, you can turn any signed contract into a single record your downstream system can use: run the extraction, pull the audit trail, and merge the two.
Common mistakes
- Sending JSON to the eSign token endpoint : it requires form-encoded data (
data=, notjson=inrequests). A JSON body returns a 415 error. - Calling the activity history endpoint on a draft folder : it only works once the folder has been shared or sent; a draft returns an error instead of an empty result.
- Assuming
region.boundingBoxis a four-number rectangle : it’s an eight-number polygon (four corner point pairs), not[x, y, width, height]. - Reading
element["region"]["boundingBox"]on every element type without checkingtypefirst :tableelements carry an emptyregionobject and expose position throughregionsandscores(both plural) instead, so a generic per-element loop raises aKeyErrorthe moment it reaches a table. - Treating table cells as a 2D array : table content lives in
content.body.cells, a flat list withrowIndex/columnIndexon each cell, not a nestedrowsarray. - Skipping whitespace normalization before regex matching : paragraph text preserves the source PDF’s line wraps as literal characters mid-sentence, which breaks patterns that assume a single space between words.
Get started
The pipeline in this tutorial extracts what a contract says and proves how it was signed, in two API calls plus a lightweight mapping step, no regex-per-template maintenance burden and no manual copy-paste. Once you have a structured record per contract, the obvious next steps are automated renewal alerts before a notice period closes, obligation tracking against the liability and termination terms, and pushing the merged record straight into a BI dashboard or CLM system.
Create a free Foxit developer account, no credit card required, and run this extraction against one of your own contracts using the interactive API reference.
Contract metadata extraction FAQs
What is contract metadata extraction?
Contract metadata extraction is the process of pulling key details out of a contract, such as party names, effective dates, renewal dates, notice periods, and liability caps, and turning them into structured data. Instead of copying terms into a spreadsheet by hand, you get a typed record that feeds a CLM tool, a CRM, or a renewal-alert script.
What contract fields can I extract with the Foxit API?
You can extract any text the contract contains, including parties, effective and renewal dates, termination notice periods, and liability caps. The Foxit PDF Services API returns the document’s titles, headings, paragraphs, and tables as structured JSON. A short mapping step then turns that output into the exact fields your downstream system needs.
Do I need a separate parser for every contract template?
No. The structural extraction API returns the same JSON format for every PDF, so you don’t need to parse raw layouts for each template. You still map fields to your schema, usually with patterns tuned to your clause language. Contracts with a key-dates summary table are easier, since the table cells come back as label-value pairs.
How do I get the signing audit trail for a contract?
Call the Foxit eSign activity history endpoint with the contract’s folder ID. It returns the folder’s status and a timestamped log of every event, such as when the contract was created and when invitations went out. The endpoint only works after the folder has been shared or sent. A draft folder returns an error.
Can I try the Foxit API for free?
Yes. A free Foxit developer account gives you live API credentials right away, with no credit card and no approval step. The free plan includes 500 credits per year, which is enough to build and test a full contract metadata extraction pipeline. You can also test calls in the interactive API reference before writing code.
Embedded Signing with the Foxit eSign API: From Envelope Creation to In-App iFrame in One Session

This guide walks through the Foxit eSign API end to end: authenticate, create a folder, generate an embedded session URL, and render the signing experience in an iFrame your users never leave.
Most embedded signing tutorials hand you a three-step abstraction: get a token, create an envelope, open a recipient view. The implementation details live somewhere else, usually mapped to a different API’s object model that doesn’t quite match what you’re working with.
This tutorial covers the Foxit eSign API embedded signing mechanics from start to finish. Authenticate once, create a “folder” (Foxit’s term for what other platforms call an envelope), receive an embedded session URL in that same response, and render it in an iFrame your users never leave. No separate field-placement API call, no client-side SDK to install.
What You’ll Build
By the end of this guide, you’ll have a working embedded signing session: a PDF loaded into your app via iFrame, signature fields defined by Text Tags, a webhook handler that verifies completion events, and a post-signing redirect that keeps users inside your product. Every step runs against the Foxit eSign sandbox with credentials you can generate in under five minutes.
1. Prerequisites and Auth Setup
Before any code runs, make sure you have the following installed and configured:
venv for project isolation
The requests library
A code editor: VS Code with the Python extension is a solid choice. PyCharm, Sublime Text, and JetBrains Fleet work equally well.
A Foxit eSign developer account, available at account.foxit.com/site/sign-up with no credit card required
Set up your workspace:
mkdir foxit-esign-tutorial && cd foxit-esign-tutorial
python3 -m venv .venv && source .venv/bin/activate
pip install requests The Foxit eSign API Is a Separate Portal from PDF Services
Developers who already use Foxit tools hit this wall first: the Foxit eSign API runs at its own base host, with its own API Key and API Secret. These credentials don’t work with the client_id and client_secret from the PDF Services developer portal at developer-api.foxit.com. The eSign documentation lives at developersguide.foxitesign.foxit.com, separate from docs.developer-api.foxit.com. The API Playground handles PDF Services sandbox testing; eSign sandbox calls go to the eSign portal directly.
The NA environment base host is https://na1.foxitesign.foxit.com. All subsequent code samples reference this as {HOST_NAME}.
Generating an OAuth 2.0 Access Token
Set your credentials as environment variables before running anything:
export FOXIT_ESIGN_CLIENT_ID="your_api_key"
export FOXIT_ESIGN_CLIENT_SECRET="your_api_secret"
Then generate a Bearer token via the OAuth 2.0 client credentials flow:
curl -X POST "https://na1.foxitesign.foxit.com/api/oauth2/access_token" \
-d "client_id=$FOXIT_ESIGN_CLIENT_ID" \
-d "client_secret=$FOXIT_ESIGN_CLIENT_SECRET" \
-d "grant_type=client_credentials" \
-d "scope=read-write" The response carries an access_token field. Pass it as Authorization: Bearer {token} on every subsequent call, and store it server-side so your credentials never travel to the browser.
2. Creating an Envelope Programmatically with /api/folders/createfolder
Foxit eSign calls what other platforms call an “envelope” a folder. If you’re coming from DocuSign or PandaDoc, that naming difference will catch you on the first read of the docs. The endpoint is POST {HOST_NAME}/api/folders/createfolder. A template-based variant also exists at POST {HOST_NAME}/api/templates/createFolder (note the camelCase) for assembling envelopes from saved templates. This tutorial focuses on the direct document-upload flow.
Defining Signature Fields with Text Tags
Foxit eSign reads signature field definitions from the PDF itself at upload time. You embed them as Text Tags directly in the document, and the API parses the tags on ingest and converts them to interactive fields. The tag syntax follows this structure: ${fieldtype:party_number:required:field_name:width}. Here, y marks a field as required and n marks it as optional. The party number maps to the signing sequence for that recipient. Width is expressed as underscores.
A minimal set covering the four field types required for a real signing flow:
${s:1: } # signature field, party 1
${i:1:______} # initials field, party 1
${d:1:n::____} # optional date field, party 1
${t:1:y:Full_Name:__________} # required text field, party 1, named "Full_Name" The full set of supported tag types includes signfield (or s), initialfield (or i), datefield (or d), textfield (or t), textboxfield (or tb), checkboxfield (or c), radiobuttonfield (or rb), securedfield (or sc), attachmentfield (or a), imagefield (or img), accept (or ab), decline (or db), payfield (or pf), and formulafield (or ff).
To hide tags in production, set the tag font color to match the page background color. Foxit eSign converts the tags to fields but does not strip them from the rendered document.
Use this sample PDF with Text Tags pre-embedded to follow along. It includes a signature field, initials, a date field, and a text input, all mapped to party 1.
Submitting the createfolder Request
You can supply the PDF two ways against the same endpoint. In URL mode you pass a fileUrls array of publicly reachable PDF links alongside a matching fileNames array. In base64 mode you set "inputType": "base64" and pass a base64FileString array of base64-encoded PDF bytes, again with a matching fileNames array. The API rejects a request that supplies neither, returning fileUrls or base64FileString cannot be empty.
One parameter is easy to miss and breaks the whole flow when omitted. Set processTextTags to true so the API parses the Text Tags embedded in the PDF and converts them into interactive fields. Leave it out and the folder still gets created successfully, but the tags stay inert literal text on the page, the signing UI reports zero required fields, and a signer can reach Finish without ever signing. If your source PDF carries native AcroForm fields instead of Text Tags, the companion processAcroFields flag handles those.
URL-based submission via cURL, pointing at the hosted sample PDF:
curl -X POST "https://na1.foxitesign.foxit.com/api/folders/createfolder" \
-H "Authorization: Bearer $FOXIT_ESIGN_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"folderName": "Service Agreement",
"sendNow": false,
"processTextTags": true,
"createEmbeddedSigningSession": true,
"embeddedSignersEmailIds": ["[email protected]"],
"fileUrls": ["https://github.com/lucienchemaly/foxit-demo-templates/raw/main/esign/sample-text-tags.pdf"],
"fileNames": ["sample-text-tags.pdf"],
"parties": [
{
"firstName": "Alex",
"lastName": "Rivera",
"emailId": "[email protected]",
"permission": "FILL_FIELDS_AND_SIGN",
"sequence": 1
}
]
}'
In this request you ask Foxit eSign to fetch the tagged PDF from its public URL, hold the folder as a draft instead of emailing it by setting sendNow to false, and mint an embedded signing session for the party identified in embeddedSignersEmailIds. The single signer is defined in the parties array with a name, email, the FILL_FIELDS_AND_SIGN permission, and a signing sequence.
Base64 upload via Python, which avoids needing a public URL by sending the file bytes inline:
import os
import base64
import requests
HOST = "https://na1.foxitesign.foxit.com"
TOKEN = os.environ["FOXIT_ESIGN_ACCESS_TOKEN"]
with open("sample-text-tags.pdf", "rb") as pdf:
encoded = base64.b64encode(pdf.read()).decode()
payload = {
"folderName": "Service Agreement",
"sendNow": False,
"processTextTags": True,
"inputType": "base64",
"base64FileString": [encoded],
"fileNames": ["sample-text-tags.pdf"],
"createEmbeddedSigningSession": True,
"embeddedSignersEmailIds": ["[email protected]"],
# Recipient defined in the body overrides any tag-defined party
"parties": [
{
"firstName": "Alex",
"lastName": "Rivera",
"emailId": "[email protected]",
"permission": "FILL_FIELDS_AND_SIGN",
"sequence": 1,
}
],
}
response = requests.post(
f"{HOST}/api/folders/createfolder",
headers={"Authorization": f"Bearer {TOKEN}"},
json=payload,
)
print(response.json())
In this code, you read the local PDF, base64-encode its bytes, and place the result inside the base64FileString array with inputType set to base64 so the API knows to decode it rather than fetch a URL. The rest of the payload mirrors the cURL example, sending the folder as a draft and requesting an embedded session for the listed signer, after which you print the JSON response to read back the folder.folderId and the session URL. When both the PDF’s Text Tags and the API body define recipient parties, the body values take precedence, so you can reuse a tagged PDF template and swap in different signers at request time without touching the document.
3. Generating the Embedded Signing Session and Rendering the iFrame
Setting createEmbeddedSigningSession: true in the createfolder body, paired with an embeddedSignersEmailIds array naming which parties sign in your app, gives you a signed session URL in the same response. No second API call, no separate “recipient view” endpoint. The response carries an embeddedSigningSessions array, and each entry holds the signer email in emailIdOfSigner, the raw token in embeddedToken, and the ready-to-render link in embeddedSessionURL. That URL follows this format, where eetid is the URL-encoded embedded token:
https://{HOST_NAME}/embedded/embeddedsign?eetid={URL-ENCODED-EMBEDDED-TOKEN}
If you omit embeddedSignersEmailIds, the API returns email id of embedded signer(s) not submitted, so always list the embedded signers explicitly. For multi-party workflows you can set createEmbeddedSigningSessionForAllParties: true so every recipient signs in an embedded session rather than over email. When you need each signer’s live URL, request it per signer through the regenerate endpoint described below.
The full lifecycle runs from token request through webhook delivery:
Injecting the Session URL into an iFrame
The signing UI renders entirely inside the iFrame with no additional JavaScript library required.
function launchSigningSession(embeddedSessionURL) {
const iframe = document.createElement("iframe");
// These five sandbox permissions are the minimum required for the signing UI
iframe.setAttribute(
"sandbox",
"allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation",
);
iframe.src = embeddedSessionURL;
iframe.style.width = "100%";
iframe.style.height = "700px";
iframe.style.border = "none";
iframe.onload = function () {
console.log("Signing session ready");
};
document.getElementById("signing-container").appendChild(iframe);
} The sandbox attribute matters here. Remove allow-popups or allow-top-navigation and the signing UI breaks in ways that produce no obvious error. The five attributes above are the minimum viable set. Don’t strip them without testing the complete signing flow.
To verify the flow without wiring this into your app first, download the ready-to-run iFrame test page from the demo repo, open it in a browser, paste the embeddedSessionURL from your createfolder response into the input box, and click Load. It applies the same five sandbox permissions shown above. A correctly tagged document renders with the signing controls active, as shown below.
The sample document loaded in the iFrame with processTextTags enabled. The header shows “Required Fields Left: 2” and a “Next Required Field” button, confirming the Text Tags became interactive fields. If that counter reads zero or the page shows the raw ${...} tag text with no input boxes, recheck that processTextTags was set to true on the createfolder request.
Session URLs are short-lived. Generate the URL at request time and pass it directly to the client. Don’t cache it. If a user returns to an incomplete workflow after the session expires, call POST {HOST_NAME}/api/embedded/regenerateEmbeddedSigningSession with the folder ID and the signer email to get a fresh URL. The response mirrors a single embedded session entry, returning emailIdOfSigner, embeddedToken, and the new embeddedSessionURL.
4. White-Labeling the Signing Experience
Foxit eSign exposes branding control at several levels. You can apply a custom logo to the signing UI and outgoing notification emails, set application colors to match your product’s visual design, and configure a personalized sender name so recipients see your company name rather than a generic Foxit sender identity.
For logo and color configuration, manage these settings through the eSign Portal’s branding section. The portal publishes canonical limits on file size and supported formats. Check the branding settings in your account for current specifications rather than relying on numbers printed here that may have changed.
Configuring Post-Signing Redirect URLs
Custom redirect URLs keep users inside your application after they sign, decline, defer, or hit an error. Pass them as parameters in the createfolder body:
payload = {
"folderName": "Service Agreement",
"sendNow": False,
"processTextTags": True,
"createEmbeddedSigningSession": True,
"embeddedSignersEmailIds": ["[email protected]"],
# Return the user to your confirmation page after a successful signature
"signSuccessUrl": "https://app.example.com/contracts/signed",
# Return to a dedicated page when the signer declines
"signDeclineUrl": "https://app.example.com/contracts/declined",
# Return here when the signer chooses to finish later
"signLaterUrl": "https://app.example.com/contracts/later",
# Return here if the signing session errors out
"signErrorUrl": "https://app.example.com/contracts/error",
"parties": [ ... ],
"fileUrls": [ ... ],
"fileNames": [ ... ],
} Foxit eSign appends two query parameters to your success URL when it redirects, namely folderId for the folder that was signed and event, whose value is signing_success on a completed signature or signing_declined when the signer declines. Without these URLs, signers land on Foxit’s default confirmation page. With them, your application controls the entire post-signing navigation experience.
Tailoring Signer Instructions for Regulated Industries
Regulated workflows often need specific disclosure language in front of signers, which matters for ESIGN Act or eIDAS compliance scenarios where your legal team controls the wording. The createfolder body accepts a signerInstructionId and a confirmationInstructionId that reference instruction templates configured in your account, and you can drop explicit accept and decline button fields into the document itself using the accept (or ab) and decline (or db) Text Tag types. The eSign Developers Guide at developersguide.foxitesign.foxit.com documents these parameters.
5. Handling Webhook Callbacks for Completion Events
Foxit eSign fires HTTP POST requests to your registered endpoint when these lifecycle events occur: folder_sent, folder_viewed, folder_signed, folder_cancelled, folder_completed, folder_executed, and folder_deleted. Register your callback URL in the eSign Portal under API Settings. Make sure the endpoint is publicly reachable over HTTPS before you start testing with the sandbox.
Verifying the Webhook Signature
Every webhook POST includes a signature query parameter. It’s a base64-encoded HMAC-SHA-256 digest of the raw request body, computed using your webhook secret. Recompute the same digest server-side and compare before doing any processing. An unverified webhook is an open door.
import os
import hmac
import hashlib
import base64
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["FOXIT_ESIGN_WEBHOOK_SECRET"].encode()
@app.route("/webhook/foxit", methods=["POST"])
def foxit_webhook():
# Step 1: Pull the signature from the query string
received_sig = request.args.get("signature", "")
# Step 2: Recompute HMAC-SHA-256 over the raw request body
raw_body = request.get_data()
computed_sig = base64.b64encode(
hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).digest()
).decode()
# Step 3: Constant-time comparison guards against timing attacks
if not hmac.compare_digest(received_sig, computed_sig):
abort(403)
payload = request.json
event_name = payload.get("event_name")
folder_id = payload.get("data", {}).get("folder", {}).get("folderId")
if event_name in ("folder_completed", "folder_executed"):
handle_completion(folder_id)
elif event_name == "folder_cancelled":
handle_cancellation(folder_id)
# A non-2xx response triggers Foxit's automatic retry logic.
# Return 200 once verification and basic parsing succeed.
return "", 200
The payload structure is consistent across all events, carrying a top-level event_name, an event_date timestamp, and a data object whose folder field holds the full folder record. The folder identifier lives at data.folder.folderId, alongside the rest of the envelope-level metadata such as folderName and folderStatus.
Downstream Actions Triggered from folder_completed
When folder_completed or folder_executed fires, two actions cover the majority of production workflows. Fetch the signed document via the documents endpoint using the folder ID and store the result in your document storage layer. For contracts that require sequential agreements (a master services agreement followed by a statement of work, for example), fire the next createfolder call as part of the completion handler.
Audit history is available programmatically via GET {HOST_NAME}/api/folders/viewActivityHistory?folderId={FOLDER_ID}, which returns the full signing log once a folder has been shared or sent. This is a GET-only endpoint, and a folder still in DRAFT returns logs of a non-shared folder can not be viewed.
6. Common Mistakes and Troubleshooting
Text Tag Syntax Breaks on Copy-Paste
Smart-quote autocorrect in Word, Google Docs, and many other editors replaces straight ASCII brackets and quote characters with typographic equivalents. Tag parsing fails silently when this happens. Always paste tags into a plain-text editor first and verify the bracket characters are straight ASCII. The eSign Developers Guide writes every field-type notation in lowercase, such as signfield or s and textfield or t, so author your tags in lowercase to match the documented syntax rather than experimenting with capitalized variants.
Visible Text Tags Reaching Production
Foxit eSign converts embedded tags to fields but does not remove them from the document. If you ship a PDF without setting the tag text color to match the page background, signers see the raw ${...} strings on the page. Build the color-hide step into your PDF preparation pipeline before it becomes a support ticket.
Body-Level parties Overriding Tag-Defined Recipients
Tags define field layout and recipient assignment and must be embedded in the PDF itself, while recipient definitions in the API request body override tag-defined recipient metadata. If you’re seeing the wrong signer name or email appear, check whether a body-level parties definition is overriding the tag.
Expired Session URLs
Embedded session URLs are short-lived. Caching one and reusing it on the next page load will fail. Call POST {HOST_NAME}/api/embedded/regenerateEmbeddedSigningSession with the folder ID and signer email each time a returning user needs access to an incomplete session.
Over-Restrictive sandbox on the iFrame
The five required sandbox permissions are allow-scripts, allow-same-origin, allow-forms, allow-popups, and allow-top-navigation. If the UI loads but behaves unexpectedly, check your sandbox attributes first.
Credential Confusion Between eSign and PDF Services
The API Key and API Secret from the eSign Portal are specific to the eSign API, and the OAuth 2.0 flows also differ between the two. Using PDF Services credentials against the eSign /api/oauth2/access_token endpoint returns an authentication error. Keep the two credential sets separate and named clearly in your environment configuration.
Skipping Webhook Signature Verification
Always verify the signature query parameter before processing any payload. Return a 200-class status code once verification and basic parsing succeed, because a non-2xx response causes Foxit to retry delivery, which can create duplicate processing if your handler is not idempotent.
Embedded Signing FAQ
Can I regenerate an expired embedded signing session?
Yes. Call POST {HOST_NAME}/api/embedded/regenerateEmbeddedSigningSession with the folder ID and signer email. Foxit eSign returns a fresh embeddedSessionURL for the same envelope without resetting the signing state.
Do I need a separate Foxit account if I’m already using Foxit PDF Services?
Yes. The Foxit eSign API operates from a separate portal at na1.foxitesign.foxit.com with its own credentials. The two portals don’t share API keys, secrets, or authentication tokens.
Can I use an existing template instead of uploading a PDF?
Yes. Use POST {HOST_NAME}/api/templates/createFolder to assemble the envelope from a saved template rather than a raw document upload.
Does the embedded signing session work on mobile browsers?
Yes. The iFrame renders responsively on modern mobile browsers without additional configuration.
Is the audit trail accessible programmatically?
Yes. GET {HOST_NAME}/api/folders/viewActivityHistory?folderId={FOLDER_ID} returns the full signing activity log for any shared or sent envelope, including timestamps for each event. A folder still in DRAFT has no shared history to return.
What is the difference between folder_completed and folder_executed?
Both events signal that the folder has been completed with all required parties’ signatures, and the eSign Developers Guide describes them in the same terms, each delivering the folder record in data.folder. Listen for either to trigger downstream retrieval of the signed document, and make your handler idempotent so receiving both for the same folder does not double-process it.
Next Step
Activate your free Foxit eSign developer account at account.foxit.com/site/sign-up, no credit card required. Generate your OAuth token, fire a POST /api/folders/createfolder request with createEmbeddedSigningSession: true against the sandbox, and verify the returned URL loads in a local iFrame. From account creation to a working embedded signing session takes under 30 minutes.