How to integrate e-signatures into a Python application

An e-signature integration lets your Python service own the full signing loop instead of sending people out to another product. That keeps the workflow inside the app your users already know, and it removes the extra tabs and handoffs that usually slow the document signing process down.
In this blog post, we'll show you how to add e-signing to a Django or Flask application using Formable. Formable offers a Python SDK and detailed documentation to make the integration process as easy as possible. See Formable's developer APIs and SDKs for the complete set of embedded signing and redlining tools.
What you'll need:
- Python 3.9 or newer
- Django 4.2+ or Flask 2.2+
- A Formable account (paid or free sandbox)
- An API key from Formable account settings
Table of Contents
- Why an e-signature API from Python?
- Step 1: installing the Formable Python SDK
- Step 2: creating the client
- Step 3: create a template from your document
- Step 4: choose a signing flow
- Step 5: embed signing
- Step 6: handle webhooks
- Step 7: download the signed PDF
- Error handling and test mode
- Conclusion
- FAQs
Why an e-signature API from Python?
Say you write Django or Flask for a procurement platform. Each vendor still needs an MSA signed by their counsel and yours. When that lives in email attachments and a shared drive, the latest file goes missing, nobody can tell who has signed, and onboarding slips. By integrating with Formable, all those manual signing processes can be automated within the platform.
Step 1: installing the Formable Python SDK
pip install formable-sdk
Note:
For more details and advanced usage, you can also reference the Official Formable Python SDK Documentation.
Step 2: creating the client
The client is a sync HTTP wrapper. Create it once and reuse it. In Django, read the key from settings. In Flask, construct it inside create_app so each gunicorn worker gets its own HTTP pool, and tests can pass a fake key.
Django
# settings.py
import os
FORMABLE_API_KEY = os.environ["FORMABLE_API_KEY"]
FORMABLE_WEBHOOK_SECRET = os.environ["FORMABLE_WEBHOOK_SECRET"]
# signing/client.py
from django.conf import settings
from formable import Formable
formable = Formable(api_key=settings.FORMABLE_API_KEY)
Flask
# app.py
import os
from flask import Flask
from formable import Formable
from signing.routes import signing_bp
from signing.webhooks import webhooks_bp
def create_app():
app = Flask(__name__)
app.config["FORMABLE_API_KEY"] = os.environ["FORMABLE_API_KEY"]
app.config["FORMABLE_WEBHOOK_SECRET"] = os.environ["FORMABLE_WEBHOOK_SECRET"]
app.extensions["formable"] = Formable(
api_key=app.config["FORMABLE_API_KEY"]
)
app.register_blueprint(signing_bp)
app.register_blueprint(webhooks_bp)
return app
# signing/__init__.py
from flask import current_app
from formable import Formable
def client() -> Formable:
return current_app.extensions["formable"]
FastAPI can call the same HTTP API with AsyncFormable so you do not block the event loop.
Step 3: create a template from your document
A template is the re-usable document where you or your users place the fields for the signers to fill in.
Signer roles are optional for single signers. If you need to support multi-party signing, they are required as you need to specify which role needs to fill out which field.
import os
from formable import Formable
formable = Formable(api_key=os.environ["FORMABLE_API_KEY"])
with open("msa.pdf", "rb") as handle:
created = formable.templates.create(
file=handle.read(),
filename="msa.pdf",
signer_roles=[
{"name": "Vendor", "order": 0},
{"name": "Counsel", "order": 1},
],
)
template_id = created["templateId"]
print(created["editTemplateAccess"]["editUrl"])
Open the edit URL, place at least one required signature field, assign it Vendor. The URL expires after a day:
edit = formable.templates.create_edit_url(template_id)
print(edit["editUrl"])
Save template_id and reuse it for later signature requests. In Django, store it on a model.
Step 4: choose a signing flow
You can choose to send a signing link via email to your user, or have your user sign within your own application within an iFrame.
If choosing email delivery, implement using the following code snippet.
request = formable.signature_requests.create(
template_id=template_id,
signers=[
{"email": "jane@example.com", "name": "Jane Doe", "role": "Vendor"},
{"email": "counsel@yourco.com", "name": "Alex Chen", "role": "Counsel"},
],
test_mode=True,
)
print(request["signatureRequestId"]) # save this
If you want your users to sign inside your application, the next section will show you how to implement the iframe path.
Step 5: embed signing
Embedded signing requires two server calls.
First: Create the signature request.
Second: Mint a short-lived signing URL using the id from the signature request.
Django view
# signing/views.py
import json
from django.http import JsonResponse
from django.views.decorators.http import require_GET, require_POST
from signing.client import formable
@require_POST
def create_signature_request(request):
body = json.loads(request.body)
signer = body["signer"]
created = formable.signature_requests.create_embedded(
template_id=body["templateId"],
signers=[
{
"email": signer["email"],
"name": signer["name"],
"role": "Vendor",
}
],
test_mode=True,
)
return JsonResponse(
{
"signatureRequestId": created["signatureRequestId"],
"recipientSignatureId": created["signers"][0]["recipientSignatureId"],
}
)
@require_GET
def signing_url(request):
signing = formable.signature_requests.create_signing_url(
request.GET["recipientSignatureId"]
)
return JsonResponse(
{
"signingUrl": signing["signingUrl"],
"expiresAt": signing["expiresAt"],
}
)
Flask blueprint
# signing/routes.py
from flask import Blueprint, jsonify, request
from signing import client
signing_bp = Blueprint("signing", __name__)
@signing_bp.post("/api/signature-requests")
def create_signature_request():
body = request.get_json(force=True)
signer = body["signer"]
created = client().signature_requests.create_embedded(
template_id=body["templateId"],
signers=[
{
"email": signer["email"],
"name": signer["name"],
"role": "Vendor",
}
],
test_mode=True,
)
return jsonify(
signatureRequestId=created["signatureRequestId"],
recipientSignatureId=created["signers"][0]["recipientSignatureId"],
)
@signing_bp.get("/api/signing-url")
def signing_url():
signing = client().signature_requests.create_signing_url(
request.args["recipientSignatureId"]
)
return jsonify(
signingUrl=signing["signingUrl"],
expiresAt=signing["expiresAt"],
)
On the client, fetch the URL from your Python service and load it in an iframe. Formable posts onSigningComplete to the parent window:
const { signingUrl } = await fetch(
`/api/signing-url?recipientSignatureId=${recipientSignatureId}`
).then((res) => res.json());
document.getElementById("signing-frame").src = signingUrl;
window.addEventListener("message", (event) => {
if (event.origin !== "https://app.formabledocs.com") return;
if (event.data?.type === "onSigningComplete") {
// close the iframe or show a success state
}
});
<iframe
id="signing-frame"
width="100%"
height="800"
allow="fullscreen"
style="border: none;"
></iframe>
Step 6: handle webhooks
Register the path in Settings and keep the secret. Hash the raw request body. Hashing a re-encoded JSON object will fail verification.
Django
Formable will not send a CSRF cookie, so this view must be @csrf_exempt. Hash request.body before anything else. request.POST can consume the stream.
# signing/webhooks.py
import base64
import hashlib
import hmac
import json
from django.conf import settings
from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from signing.client import formable
@csrf_exempt
@require_POST
def formable_webhook(request):
secret = base64.b64decode(settings.FORMABLE_WEBHOOK_SECRET)
expected = base64.b64encode(
hmac.new(secret, request.body, hashlib.sha256).digest()
).decode()
received = request.headers.get("Content-Sha256", "")
if not hmac.compare_digest(expected, received):
return HttpResponseForbidden()
payload = json.loads(request.body)
if payload["event"]["event_type"] == "document_completed":
envelope = formable.signature_requests.get_signed_envelope(
payload["signing"]["signature_request_id"]
)
_url = envelope["signedEnvelopePresignedUrl"]
# download, store the PDF, mark the record complete
return HttpResponse(status=200)
# signing/urls.py
from django.urls import path
from signing import views, webhooks
urlpatterns = [
path("api/signature-requests", views.create_signature_request),
path("api/signing-url", views.signing_url),
path("webhooks/formable", webhooks.formable_webhook),
]
Flask
Read the body with request.get_data() before you parse JSON. Do not call request.get_json() first.
# signing/webhooks.py
import base64
import hashlib
import hmac
import json
from flask import Blueprint, abort, current_app, request
from signing import client
webhooks_bp = Blueprint("webhooks", __name__)
@webhooks_bp.post("/webhooks/formable")
def formable_webhook():
raw_body = request.get_data()
secret = base64.b64decode(current_app.config["FORMABLE_WEBHOOK_SECRET"])
expected = base64.b64encode(
hmac.new(secret, raw_body, hashlib.sha256).digest()
).decode()
received = request.headers.get("Content-Sha256", "")
if not received or not hmac.compare_digest(expected, received):
abort(401)
payload = json.loads(raw_body)
if payload["event"]["event_type"] == "document_completed":
envelope = client().signature_requests.get_signed_envelope(
payload["signing"]["signature_request_id"]
)
_url = envelope["signedEnvelopePresignedUrl"]
# download, store the PDF, mark the record complete
return ("", 200)
document_completed is the terminal event. Once you receive this event, this means all parties have signed the document, and you may now fetch the signed document.
Alternatively, if you don't want to expose a webhook endpoint, you can poll for signature request status updates.
from datetime import datetime, timedelta, timezone
current = formable.signature_requests.get(signature_request_id)
if current["status"] == "Completed":
# safe to download
pass
recent = formable.signature_requests.list(
updated_since=datetime.now(timezone.utc) - timedelta(days=1)
)
Step 7: download the signed PDF
The signed envelope URL is a short-lived presigned link.
import httpx
envelope = formable.signature_requests.get_signed_envelope(signature_request_id)
pdf = httpx.get(envelope["signedEnvelopePresignedUrl"]).content
In Django, save the bytes on a FileField so later downloads go through your app, not Formable:
from django.core.files.base import ContentFile
job.signed_pdf.save(f"{job.pk}.pdf", ContentFile(pdf), save=True)
In Flask, write the bytes to disk or your object store:
from pathlib import Path
Path(f"{signature_request_id}.pdf").write_bytes(pdf)
The signed envelope includes the Formable audit trail of all relevant events.
Error handling and test mode
Non-2xx responses raise a FormableError. A 409 from get_signed_envelope means the document is not finished:
from formable import FormableError
try:
formable.signature_requests.get_signed_envelope(signature_request_id)
except FormableError as error:
if error.status == 409:
# wait for document_completed
return
raise
400 is usually a field_id that is not on the template. 401 is the API key. 404 is an unknown id.
Set test_mode=True while you integrate. Test documents are watermarked, not legally binding, and do not count toward billing. Clear the flag for production.
Conclusion
Your Python service now owns signing end to end: a reusable template, email or embedded signing, a verified webhook, and a signed PDF with an audit trail stored on your side. The same SDK methods work from a Django view, a Flask blueprint, a management command, or a Celery task.
To learn more about the Formable API, take a look at our API documentation. We support SDKs for a wide variety of languages, and are always looking to support more in the near future.
If you have questions, email matt@formabledocs.com. We are always happy to assist you!
FAQs
How do I add e-signatures to a Python application?
Install formable-sdk, create a template from your PDF or DOCX, then create a signature request. Formable can email signers, or you can generate a signing URL and embed it in an iframe. A webhook tells you when to download the signed PDF.
Does the SDK depend on Django or Flask?
No. It is a sync HTTP client. The examples use Django views and Flask blueprints. A management command or Celery task can call the same methods. FastAPI can use AsyncFormable so you do not block the event loop. The framework-specific pieces are the webhook: Django needs @csrf_exempt and must hash request.body; Flask must hash request.get_data() before parsing JSON.
How do I know when a document has been signed?
Handle document_completed on your webhook, or poll get until status is Completed.
Can I test without sending legally binding documents?
Yes. Set test_mode=True on signature requests. Test documents are watermarked, not legally binding, and do not count toward billing.
Does the signed PDF include an audit trail?
Yes. Every signed document includes an appended audit trail with created, sent, signed, and completed events, including timestamps, actors, and IP addresses, which supports enforceability under ESIGN and UETA.




