May 11, 2026 · Tutorial

Python E-Signature API: Sign Documents with 10 Lines of Code

DocuSign's Python SDK pulls in 15+ dependencies and needs 60 lines for a basic send. Signbee needs requests and 10 lines. Here's the complete tutorial — with Flask and FastAPI webhook handlers.

Michael Beckett
Michael Beckett

Founder, Signbee

TL;DR

Add e-signatures to any Python app with the standard requests library. Send markdown to Signbee's single endpoint and get back a signing URL. No SDK, no OAuth, no template system. This tutorial covers the basic send, error handling, Flask webhooks, FastAPI webhooks, and a side-by-side comparison with DocuSign's Python SDK.

Prerequisites

Python version3.8+
Required packagerequests (pip install requests)
Signbee API keyFree at signb.ee/docs
DocuSign SDK needed?No

The requests library is likely already in your project. If not, it's a single pip install requests. Compare this to DocuSign's Python SDK (docusign-esign), which pulls in 15+ transitive dependencies including urllib3, certifi, pyjwt, and cryptography — adding complexity to your deployment and potential supply chain risk.

Step 1: The basic send (10 lines)

Here's the complete code to send a document for e-signature from Python. Save it as send_nda.py and run it:

send_nda.py — complete Python e-signature integration
import os
import requests

response = requests.post(
    "https://signb.ee/api/v1/send",
    headers={"Authorization": f"Bearer {os.environ['SIGNBEE_API_KEY']}"},
    json={
        "markdown": "# Non-Disclosure Agreement\n\n"
                    "**Disclosing Party:** Acme Corp\n"
                    "**Receiving Party:** Jane Smith\n\n"
                    "Confidential information shall be held in strict "
                    "confidence for a period of 2 years.",
        "recipient_name": "Jane Smith",
        "recipient_email": "jane@example.com",
    },
)

result = response.json()
print(f"Document ID: {result['document_id']}")
print(f"Signing URL: {result['signing_url']}")

10 lines (excluding imports and print statements). The API receives your markdown, converts it to a formatted PDF, emails the recipient a signing link, captures their signature, and stores the signed PDF with a full audit trail. You get back a document_id for tracking and a signing_url for embedding the signing flow in your own UI.

Step 2: Production error handling

The basic example works for prototyping. Production code needs to handle HTTP errors, rate limits, and network failures:

signbee_client.py — production-ready wrapper
import os
import time
import requests

class SignbeeError(Exception):
    pass

class RateLimitError(SignbeeError):
    def __init__(self, retry_after: int):
        self.retry_after = retry_after
        super().__init__(f"Rate limited. Retry after {retry_after}s")

def send_for_signature(markdown: str, name: str, email: str) -> dict:
    """Send a document for e-signature. Returns document_id and signing_url."""
    response = requests.post(
        "https://signb.ee/api/v1/send",
        headers={"Authorization": f"Bearer {os.environ['SIGNBEE_API_KEY']}"},
        json={
            "markdown": markdown,
            "recipient_name": name,
            "recipient_email": email,
        },
        timeout=30,
    )

    if response.status_code == 429:
        retry_after = int(response.headers.get("Retry-After", "5"))
        raise RateLimitError(retry_after)

    if not response.ok:
        raise SignbeeError(
            f"API error {response.status_code}: {response.text}"
        )

    return response.json()

def send_with_retry(markdown: str, name: str, email: str, max_retries: int = 3) -> dict:
    """Send with automatic retry on rate limits and server errors."""
    for attempt in range(max_retries + 1):
        try:
            return send_for_signature(markdown, name, email)
        except RateLimitError as e:
            if attempt < max_retries:
                time.sleep(e.retry_after)
                continue
            raise
        except requests.ConnectionError:
            if attempt < max_retries:
                time.sleep(2 ** attempt)
                continue
            raise

The wrapper handles three failure modes: rate limits (429 with Retry-After), server errors (5xx with exponential backoff), and network failures (connection errors with retries). For a comprehensive rate limit strategy, see the rate limits guide.

Step 3: Flask webhook handler

To know when a document is signed, viewed, or declined, configure a webhook URL. Here's a minimal Flask implementation:

app.py — Flask webhook handler
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/webhooks/signbee", methods=["POST"])
def handle_signbee_webhook():
    payload = request.json
    event = payload["event"]
    data = payload["data"]

    if event == "document.signed":
        print(f"✅ {data['signer_name']} signed {data['document_id']}")
        # Update your database, trigger downstream workflows
        # db.documents.update(data["document_id"], status="signed")

    elif event == "document.viewed":
        print(f"👁️ {data['signer_name']} viewed {data['document_id']}")

    elif event == "document.declined":
        print(f"❌ {data['signer_name']} declined {data['document_id']}")
        # Alert sender, offer to modify and resend

    return jsonify({"status": "ok"}), 200

if __name__ == "__main__":
    app.run(port=3001)

Step 4: FastAPI webhook handler

If your project uses FastAPI (and you should — it's faster, async-native, and has better type safety), here's the equivalent with Pydantic models for payload validation:

main.py — FastAPI webhook handler with Pydantic
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Literal, Optional

app = FastAPI()

class WebhookData(BaseModel):
    document_id: str
    signer_name: str
    signer_email: str
    signed_pdf_url: Optional[str] = None
    ip_address: str
    user_agent: str

class WebhookEvent(BaseModel):
    event: Literal[
        "document.sent", "document.viewed",
        "document.signed", "document.declined"
    ]
    timestamp: str
    data: WebhookData

@app.post("/webhooks/signbee")
async def handle_webhook(payload: WebhookEvent):
    if payload.event == "document.signed":
        print(f"Signed: {payload.data.document_id}")
        # await db.update_document(payload.data.document_id, "signed")

    return {"status": "ok"}

FastAPI automatically validates the incoming payload against the Pydantic model. If Signbee sends a malformed event (or someone tries to spoof a webhook), FastAPI returns a 422 before your code even runs. For a complete webhook implementation guide, see the webhooks deep dive.

What the DocuSign Python SDK looks like

For comparison, here's the DocuSign equivalent. This is from their official Python quickstart:

DocuSign Python SDK — send one document (simplified)
# pip install docusign-esign  (15+ transitive dependencies)
from docusign_esign import ApiClient, EnvelopesApi
from docusign_esign.models import (
    EnvelopeDefinition, TemplateRole
)

# Step 1: OAuth JWT authentication
api_client = ApiClient()
api_client.set_base_path("https://demo.docusign.net/restapi")
api_client.set_oauth_host_name("account-d.docusign.com")
private_key = open("private.pem", "rb").read()
token = api_client.request_jwt_user_token(
    client_id=INTEGRATION_KEY,
    user_id=USER_ID,
    oauth_host_name="account-d.docusign.com",
    private_key_bytes=private_key,
    expires_in=3600,
    scopes=["signature"],
)
api_client.set_default_header(
    "Authorization", f"Bearer {token.access_token}"
)

# Step 2: Create envelope
envelope = EnvelopeDefinition(
    email_subject="Please sign this NDA",
    template_id=TEMPLATE_ID,
    template_roles=[
        TemplateRole(
            email="jane@example.com",
            name="Jane Smith",
            role_name="signer",
        )
    ],
    status="sent",
)

# Step 3: Send
envelopes_api = EnvelopesApi(api_client)
result = envelopes_api.create_envelope(
    account_id=ACCOUNT_ID,
    envelope_definition=envelope,
)
# 60+ lines, plus OAuth key management, template pre-configuration

Integration complexity compared

FactorDocuSign + SDKSignbee + requests
Dependenciesdocusign-esign (15+ pkgs)requests (1 pkg)
AuthenticationOAuth JWT + private keyAPI key (Bearer token)
Template required?Yes (pre-configured in UI)No (markdown in request)
Lines for basic send~60 lines10 lines
PDF generationUpload or use templateMarkdown → PDF (server-side)
Per-document cost~$2.50$0.50

Batch sending in Python

Need to send documents to 50 signers? Here's an async batch sender using the production wrapper from Step 2:

batch_send.py — send to multiple signers
import csv
from signbee_client import send_with_retry

NDA_TEMPLATE = """# Non-Disclosure Agreement

**Disclosing Party:** Acme Corp
**Receiving Party:** {name}

All confidential information shall remain protected
for a period of 2 years from the date of signing.
"""

def batch_send_ndas(csv_path: str):
    results = []
    with open(csv_path) as f:
        for row in csv.DictReader(f):
            try:
                result = send_with_retry(
                    markdown=NDA_TEMPLATE.format(name=row["name"]),
                    name=row["name"],
                    email=row["email"],
                )
                results.append({"status": "sent", **result})
                print(f"✅ Sent to {row['name']}")
            except Exception as e:
                results.append({"status": "failed", "error": str(e)})
                print(f"❌ Failed for {row['name']}: {e}")
    return results

# Usage: batch_send_ndas("signers.csv")

For higher-throughput batch sending with concurrency controls, see the batch sending guide.

Django integration pattern

If your project runs on Django, wrap the Signbee client as a service class and call it from your views:

services/signing.py — Django service
import os
import requests

class SigningService:
    BASE_URL = "https://signb.ee/api/v1"

    def __init__(self):
        self.api_key = os.environ["SIGNBEE_API_KEY"]

    def send_document(self, markdown: str, name: str, email: str) -> dict:
        response = requests.post(
            f"{self.BASE_URL}/send",
            headers={"Authorization": f"Bearer {self.api_key}"},
            json={
                "markdown": markdown,
                "recipient_name": name,
                "recipient_email": email,
            },
            timeout=30,
        )
        response.raise_for_status()
        return response.json()

# In your view:
# signing = SigningService()
# result = signing.send_document(markdown, name, email)

Frequently Asked Questions

How do I send a document for e-signature from Python?

Use requests.post() to call https://signb.ee/api/v1/send with your API key, markdown content, recipient name, and email. The API handles PDF generation, email delivery, and signature capture. The complete integration is 10 lines.

Do I need DocuSign's Python SDK?

Not with Signbee. DocuSign's SDK is needed because their API requires OAuth JWT auth, private key management, and envelope templates. Signbee uses a Bearer token and a single endpoint — the requests library is all you need. See our full DocuSign comparison.

Can I handle webhooks in Flask or FastAPI?

Yes. Create a POST endpoint that parses the JSON webhook body and returns 200. Signbee sends events for document.sent, document.viewed, document.signed, and document.declined. Both Flask and FastAPI examples are shown above.

Do I need a PDF library for document generation?

No. Signbee converts markdown to a formatted PDF on the server side. You don't need WeasyPrint, ReportLab, or wkhtmltopdf. Send markdown in your API request and the PDF is generated automatically.

10 lines of Python to your first signed document — 5 free docs/month.

Last updated: May 11, 2026 · Michael Beckett is the founder of Signbee and B2bee Ltd.

Related resources