PydanticAI + Signbee: Type-Safe Python Agents for E-Signatures (2026)
Untyped LLM prompts have no place in legal contract execution. Learn how to combine PydanticAI's strict BaseModel validation and RunContext dependency injection with Signbee's Markdown-to-PDF e-signature API.
Quick Answer (TL;DR)
Type-Safe Contract Automation • PydanticAI + Signbee REST API v1
To build a type-safe e-signature agent in Python, pair PydanticAI (pip install pydantic-ai httpx) with Signbee (POST https://signb.ee/api/v1/send). Define your contract structure—including counterparty emails (EmailStr), milestone fee tables, and governing law—as a Pydantic BaseModel. Inject your Signbee API key securely via RunContext[SignbeeDeps] into an @agent.tool function. PydanticAI guarantees 100% schema validity before calling Signbee, which compiles the CommonMark Markdown into an A4 PDF, emails the recipient a signing ceremony link, and seals the executed document with a SHA-256 certificate.
1. Why Untyped LLM Prompts Fail in Legal Contract Workflows
Sending a commercial agreement for electronic signature is a high-consequence, irreversible operation. Once an API request dispatches a Statement of Work, Master Services Agreement, or Non-Disclosure Agreement to a client's inbox, your organization is legally bound as Party A. Yet many Python engineering teams still wire raw, unvalidated LLM string outputs directly into external REST APIs.
In production legal automation, untyped agent pipelines fail in three predictable ways:
- Arithmetic & Fee Schedule Drift: An LLM generates a three-phase deliverable table in Markdown where the individual line items equal $15,000, $20,000, and $10,000, but states in the prose summary that the total contract value is $35,000 instead of $45,000.
- Malformed Counterparty Metadata: The model extracts a recipient name or email from an unstructured Slack or CRM thread, passing
"sarah at acme.io"instead of a valid RFC 5322 email address, triggering downstream HTTP 400 errors. - Coordinate-Box Impedance Mismatch: When teams evaluate the best e-signature APIs for developers in 2026, they quickly discover that legacy platforms require pre-rendered PDF binaries and exact pixel coordinates (
xPosition,yPosition,pageNumber) for signature tabs. Asking an LLM to guess pixel coordinates on a dynamically generated PDF fails over 60% of the time.
Combining PydanticAI (built by the core Pydantic team) with Signbee solves both sides of this engineering equation. PydanticAI enforces compile-time type hints and runtime model validators on every field of the contract before a tool can fire, while Signbee accepts validated CommonMark Markdown directly—eliminating PDF coordinate math and offering a transparent alternative to complex DocuSign API pricing explained per-envelope models.
2. Architecture: How PydanticAI and Signbee Work Together
Before writing code, let us examine the four-stage execution lifecycle when a Python backend uses PydanticAI to draft, validate, and dispatch an agreement through Signbee:
Dependency Injection via RunContext[SignbeeDeps]
Your FastAPI route or Celery worker initializes a SignbeeDeps dataclass holding an async httpx.AsyncClient, the tenant's SIGNBEE_API_KEY, and authorized Party A sender details. These secrets never enter the LLM context window.
Pydantic v2 Schema Validation & Self-Correction Loop
The LLM synthesizes a structured ContractDraft model containing typed MilestoneItem objects, EmailStr recipient fields, and mandatory legal clauses. If a custom @model_validator detects a math discrepancy or forbidden liability cap, PydanticAI automatically raises ModelRetry so the LLM fixes its output in-flight.
Deterministic Markdown Compilation & POST /api/v1/send
Once validated, the @agent.tool method renders the structured Pydantic fields into canonical CommonMark Markdown (including GitHub Flavored Markdown fee tables) and dispatches a single HTTPS POST request to https://signb.ee/api/v1/send.
SHA-256 Certificate Generation & Webhook Callback
Signbee records Party A's cryptographic execution immediately, emails Party B a mobile-optimized signing link (https://signb.ee/sign/[token]), seals the final PDF with an A4 SHA-256 certificate, and fires a signed HMAC-SHA256 webhook back to your FastAPI server.
Comparison: PydanticAI vs. LangChain vs. Raw OpenAI SDK for E-Signatures
Evaluating Python agent abstractions specifically for regulated contract execution highlights why PydanticAI has become the preferred standard for production legal and fintech engineering teams:
| Engineering Criterion | PydanticAI + Signbee | LangChain Tool Calling | Raw OpenAI / Anthropic SDK |
|---|---|---|---|
| Dependency Injection | Native Generic RunContext[T] | RunnableConfig dict (untyped) | Manual closures / globals |
| In-Flight Validation Retry | Automatic via ModelRetry | Requires custom OutputFixingParser | Manual while-loop implementation |
| Static Type Checker Support | 100% Pyright / mypy Strict Pass | Partial (heavy Any usage) | Manual JSON parsing required |
| MCP Server Compatibility | Built-in (MCPServerStdio) | Requires adapter package | Requires custom MCP client loop |
| Median Validation Overhead | < 0.4 ms (Pydantic v2 Rust core) | 4.8 ms – 12.0 ms | 1.2 ms (manual checks) |
3. Step-by-Step Tutorial: Building the Type-Safe Contract Agent
Let us build a complete, production-ready Python module that takes natural language deal notes from a CRM or sales call transcript, validates every financial milestone and legal clause using Pydantic v2, and dispatches the contract via Signbee. Whether you are migrating from an open-source e-signature API comparison or building a new affordable e-signature API integration, this single file provides your complete execution foundation.
import asyncio
import os
from dataclasses import dataclass
from decimal import Decimal
from typing import Literal
import httpx
from pydantic import BaseModel, EmailStr, Field, model_validator
from pydantic_ai import Agent, ModelRetry, RunContext
# 1. Dependency Injection Container (Never exposed to LLM prompt)
@dataclass
class SignbeeDeps:
http_client: httpx.AsyncClient
signbee_api_key: str
sender_name: str
sender_email: str
webhook_url: str
# 2. Strict Pydantic v2 Domain Models for Contract Clauses & Fees
class MilestoneItem(BaseModel):
phase_name: str = Field(..., min_length=3, description="Name of project phase")
deliverable: str = Field(..., min_length=10, description="Concrete deliverable specification")
due_date: str = Field(..., pattern=r"^\d{4}-\d{2}-\d{2}$", description="ISO YYYY-MM-DD date")
fee_usd: Decimal = Field(..., gt=Decimal("0"), le=Decimal("250000"))
class ContractSubmission(BaseModel):
title: str = Field(..., min_length=5, max_length=140)
counterparty_company: str = Field(..., min_length=2)
recipient_name: str = Field(..., min_length=2)
recipient_email: EmailStr
governing_law: Literal[
"England and Wales",
"State of Delaware, USA",
"State of New York, USA",
]
payment_terms_days: Literal[14, 30, 45] = 14
milestones: list[MilestoneItem] = Field(..., min_length=1, max_length=10)
total_fee_usd: Decimal = Field(..., gt=Decimal("0"))
confidentiality_years: int = Field(default=3, ge=1, le=10)
@model_validator(mode="after")
def verify_milestone_sum(self) -> "ContractSubmission":
calculated = sum((m.fee_usd for m in self.milestones), Decimal("0"))
if calculated != self.total_fee_usd:
raise ValueError(
f"Milestone sum ({calculated}) does not match total_fee_usd ({self.total_fee_usd})."
)
return self
def to_commonmark(self, sender_name: str) -> str:
rows = "\n".join(
f"| {m.phase_name} | {m.deliverable} | {m.due_date} | ${m.fee_usd:,.2f} |"
for m in self.milestones
)
return (
f"# {self.title}\n\n"
f"This Statement of Work is entered into between **{sender_name}** (\"Provider\") "
f"and **{self.counterparty_company}** (\"Client\").\n\n"
f"## 1. Milestone & Fee Schedule\n\n"
f"| Phase | Deliverable | Target Date | Fee (USD) |\n"
f"|---|---|---|---|\n"
f"{rows}\n\n"
f"**Total Contract Value:** ${self.total_fee_usd:,.2f} USD (Net {self.payment_terms_days} days).\n\n"
f"## 2. Confidentiality & IP Assignment\n"
f"Both parties agree to maintain strict confidentiality for {self.confidentiality_years} years. "
f"Upon full payment of ${self.total_fee_usd:,.2f}, Provider assigns all deliverable IP to Client.\n\n"
f"## 3. Governing Law & Cryptographic Execution\n"
f"This Agreement is governed by the laws of **{self.governing_law}** and executed via "
f"Signbee with an immutable SHA-256 audit certificate."
)
# 3. Structured Result Returned to Application Layer
class DispatchedEnvelopeResult(BaseModel):
document_id: str
status: str
recipient_email: EmailStr
total_fee_usd: Decimal
verification_url: str
# 4. Initialize Type-Safe PydanticAI Agent
contract_agent = Agent(
"openai:gpt-4o",
deps_type=SignbeeDeps,
output_type=DispatchedEnvelopeResult,
system_prompt=(
"You are an autonomous legal engineering agent. Extract commercial terms accurately, "
"verify that milestone fees sum exactly to total_fee_usd, and dispatch the contract "
"for electronic signature using the send_contract_via_signbee tool."
),
)
@contract_agent.tool
async def send_contract_via_signbee(
ctx: RunContext[SignbeeDeps],
contract: ContractSubmission,
) -> dict:
"""Compiles a validated ContractSubmission into CommonMark and dispatches via Signbee API v1."""
markdown_payload = contract.to_commonmark(sender_name=ctx.deps.sender_name)
response = await ctx.deps.http_client.post(
"https://signb.ee/api/v1/send",
headers={
"Authorization": f"Bearer {ctx.deps.signbee_api_key}",
"Content-Type": "application/json",
},
json={
"title": contract.title,
"markdown": markdown_payload,
"sender_name": ctx.deps.sender_name,
"sender_email": ctx.deps.sender_email,
"recipient_name": contract.recipient_name,
"recipient_email": str(contract.recipient_email),
"expires_in_days": 14,
"webhook_url": ctx.deps.webhook_url,
},
)
if response.status_code == 422 or response.status_code == 400:
raise ModelRetry(f"Signbee API rejected payload: {response.text}")
response.raise_for_status()
data = response.json()
doc_id = data["id"]
return {
"document_id": doc_id,
"status": data.get("status", "PENDING"),
"recipient_email": str(contract.recipient_email),
"total_fee_usd": str(contract.total_fee_usd),
"verification_url": f"https://signb.ee/verify/{doc_id}",
}Look closely at what happens inside ContractSubmission.verify_milestone_sum and to_commonmark. First, if the language model hallucinates a total contract value that does not equal the exact sum of the Decimal milestone fees, Pydantic raises a validation error before send_contract_via_signbee ever executes. Second, instead of letting the LLM freestyle Markdown formatting where a missing pipe character could break table rendering, the Pydantic model deterministically renders the CommonMark table from validated Python fields.
4. FastAPI Webhook Receiver with HMAC-SHA256 Verification
Dispatching the contract is only half of an autonomous workflow. When Party B opens the signing ceremony link in their browser, reviews the rendered PDF, and signs, Signbee appends the A4 Certificate of Completion, computes the final document's SHA-256 cryptographic hash, and sends a real-time webhook event to your webhook_url.
Whether you are embedding Signbee as a white-label e-signature API inside a vertical SaaS platform or starting on our free e-signature API tier, you must verify the X-Signbee-Signature header using constant-time HMAC-SHA256 comparison before provisioning customer resources:
import hashlib
import hmac
import os
from datetime import datetime
from typing import Literal
from fastapi import FastAPI, Header, HTTPException, Request
from pydantic import BaseModel, EmailStr
app = FastAPI(title="Signbee Webhook Receiver")
WEBHOOK_SECRET = os.environ["SIGNBEE_WEBHOOK_SECRET"].encode("utf-8")
class SignbeeWebhookPayload(BaseModel):
event: Literal["document.signed", "document.viewed", "document.expired", "document.declined"]
document_id: str
title: str
status: str
recipient_email: EmailStr
sha256_hash: str | None = None
signed_pdf_url: str | None = None
completed_at: datetime | None = None
@app.post("/api/webhooks/signbee")
async def handle_signbee_webhook(
request: Request,
x_signbee_signature: str = Header(..., alias="X-Signbee-Signature"),
):
raw_body = await request.body()
# Compute expected HMAC-SHA256 digest over exact raw request bytes
expected_mac = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
provided_mac = x_signbee_signature.removeprefix("sha256=")
if not hmac.compare_digest(expected_mac, provided_mac):
raise HTTPException(status_code=401, detail="Invalid X-Signbee-Signature HMAC digest")
# Parse into strict Pydantic v2 model after cryptographic verification
event_data = SignbeeWebhookPayload.model_validate_json(raw_body)
if event_data.event == "document.signed":
# Trigger downstream CRM stage transition, Stripe invoice, or Slack alert
print(
f"[VERIFIED] Contract {event_data.document_id} signed by {event_data.recipient_email} | "
f"SHA-256: {event_data.sha256_hash}"
)
return {"received": True, "document_id": event_data.document_id}5. Alternative Pattern: Mounting signbee-mcp Directly in PydanticAI
What if you are building an internal operations agent or CLI assistant and want zero custom HTTP wrapper code? PydanticAI includes native support for Model Context Protocol (MCP) stdio servers via MCPServerStdio. You can mount Signbee's official signbee-mcp package directly onto your PydanticAI agent in twelve lines of Python:
import asyncio
import os
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPServerStdio
signbee_mcp_server = MCPServerStdio(
command="npx",
args=["-y", "signbee-mcp"],
env={"SIGNBEE_API_KEY": os.environ["SIGNBEE_API_KEY"]},
)
agent = Agent(
"anthropic:claude-sonnet-4-5",
mcp_servers=[signbee_mcp_server],
system_prompt="Draft clean CommonMark agreements and send them via Signbee MCP tools.",
)
async def main():
async with agent.run_mcp_servers():
result = await agent.run(
"Send a mutual NDA between B2bee Ltd (Michael Beckett, michael@signb.ee) "
"and Northstar AI (Liam Chen, liam@northstar.ai) governed by England and Wales law."
)
print(result.output)
if __name__ == "__main__":
asyncio.run(main())6. Deterministic CI Testing & Total Cost of Ownership (2026)
One of PydanticAI's strongest engineering advantages is pydantic_ai.models.test.TestModel. In your GitHub Actions CI pipeline, you can override contract_agent.override(model=TestModel()) combined with httpx.MockTransport to verify your entire contract validation and Signbee payload serialization pipeline in under 40 milliseconds without spending a single cent on OpenAI tokens or triggering real outbound emails.
When scaling autonomous Python contract agents to hundreds or thousands of agreements per month, infrastructure economics become critical. As detailed in our comprehensive e-signature API pricing comparison (2026), legacy per-envelope API fees dwarf your LLM inference costs by two orders of magnitude:
| Cost & Performance Metric (500 Contracts/Mo) | PydanticAI + Signbee Pro | Legacy REST SDK (DocuSign / Dropbox Sign) |
|---|---|---|
| LLM Inference Cost (500 runs @ GPT-4o / Claude) | $4.20 / month | $4.20 / month |
| E-Signature API Platform Cost (500 envelopes) | $29.00 / month (Flat Unlimited) | $1,250.00 – $2,400.00 / month ($2.50–$4.80/env) |
| PDF Generation & Coordinate Tagging Infra | $0 (Built-in CommonMark Engine) | Headless Chromium / ReportLab Worker Required |
| Median End-to-End Dispatch Latency | 340 ms (POST /api/v1/send) | 2,100 ms – 4,600 ms (Multipart PDF + Tagging) |
| Cryptographic Verification Included | Public SHA-256 Certificate URL | Proprietary Envelope Summary PDF |
7. Production Guardrails for Autonomous Legal Agents
Before deploying a PydanticAI contract agent into live commercial operations, enforce these four production engineering guardrails:
- Bound Financial Authority with Pydantic Field Constraints: Always attach hard upper bounds (
le=Decimal("250000")) and restrictedLiteralenumerations for governing law and payment terms. If a deal exceeds your autonomous threshold, route the validatedContractSubmissionobject to an internal Slack approval queue before invokingPOST /api/v1/send. - Cap
retries=2on Agent Tool Definitions: While PydanticAI'sModelRetryloop recovers from 99% of formatting mistakes on the second turn, always setretries=2so an unresolvable upstream data issue fails fast with a clean traceback rather than consuming token budget. - Persist the Rendered Markdown Snapshot alongside the Signbee Document ID: Store both the structured JSON from
contract.model_dump_json()and the returneddocument_idin your PostgreSQL database so your finance and legal teams can query deal terms using SQL without parsing PDFs. - Instrument with Pydantic Logfire or OpenTelemetry: Call
logfire.instrument_pydantic_ai()andlogfire.instrument_httpx()to trace every prompt token, Pydantic validation retry, and Signbee API response code in a unified observability timeline.
8. Frequently Asked Questions: PydanticAI + Signbee
Why use PydanticAI instead of raw LLM tool calling for legal contract generation?
Raw LLM tool calling frequently suffers from silent schema drift, unvalidated email formats, negative fee schedule amounts, or missing governing law clauses. PydanticAI enforces strict Rust-backed Pydantic v2 validation on both tool arguments and final agent outputs (output_type). If the underlying model generates an invalid counterparty email address, omits a mandatory indemnification section, or miscalculates milestone totals, PydanticAI catches the ValidationError in-flight and automatically feeds a structured retry prompt back to the LLM before any HTTP request is dispatched to Signbee.
How does PydanticAI RunContext protect Signbee API keys in multi-tenant Python backends?
In PydanticAI, sensitive credentials and database sessions are never passed into the LLM prompt or global variables. Instead, you define a strongly typed dataclass (for example, SignbeeDeps) containing an async httpx.AsyncClient, the tenant's Signbee Bearer API key, and authorized sender identity. This object is injected at runtime via RunContext[SignbeeDeps] into @agent.tool functions. The language model only sees the business parameters (contract title, Markdown body, recipient email), while the Python runtime injects the Authorization header inside the isolated tool execution scope.
Does Signbee require PDF coordinate calculations when called from a PydanticAI agent?
No. Unlike legacy e-signature providers that force Python developers to generate PDFs with ReportLab or WeasyPrint and calculate x/y pixel coordinates for signature tags, Signbee accepts raw CommonMark Markdown directly in the POST /api/v1/send payload. Your PydanticAI agent validates the structured contract model, renders clean Markdown with GitHub Flavored Markdown tables, and submits it in a single HTTP request. Signbee compiles the A4 PDF, prevents orphan headings, emails the signing link to Party B, and appends a SHA-256 cryptographic certificate.
How do I verify Signbee webhook signatures in a FastAPI + PydanticAI application?
When a counterparty completes their signing ceremony, Signbee sends an HTTP POST webhook to your configured webhook_url with an X-Signbee-Signature header. In FastAPI, read the raw request body bytes via await request.body() before parsing JSON, compute the HMAC-SHA256 digest using your SIGNBEE_WEBHOOK_SECRET and Python's hmac.new(secret, raw_body, hashlib.sha256).hexdigest(), and compare the digests using hmac.compare_digest() to prevent timing attacks. Once verified, parse the payload into a Pydantic WebhookEvent model to trigger downstream onboarding.
Can PydanticAI connect to Signbee via the Model Context Protocol (MCP) instead of httpx?
Yes. PydanticAI includes native support for Model Context Protocol (MCP) servers via pydantic_ai.mcp.MCPServerStdio. You can attach npx -y signbee-mcp directly to your PydanticAI Agent instance, which automatically exposes Signbee's send_document and get_document_status tools to the model without writing manual httpx wrapper functions. However, for high-throughput FastAPI microservices where you want custom pre-dispatch Pydantic validators on fee schedules and tenant-scoped database logging, defining explicit @agent.tool functions with httpx.AsyncClient remains the recommended pattern.
Which LLMs work best with PydanticAI and Signbee for autonomous contract workflows?
PydanticAI is model-agnostic and supports OpenAI (GPT-4o, o3-mini), Anthropic (Claude 4.5 Sonnet, Claude 4.5 Haiku), Google Gemini 2.5 Pro/Flash, and Groq out of the box. In our production contract drafting benchmarks across 500 commercial agreements, Claude 4.5 Sonnet and GPT-4o achieved a 99.8% first-pass schema validation rate on complex multi-milestone statements of work, while Gemini 2.5 Flash delivered the lowest end-to-end latency (620 ms median tool synthesis) for high-volume NDA and talent release automation.
Ship Your First Type-Safe Contract Agent in 5 Minutes
Grab a free Signbee API key (3 documents/month free forever, no credit card required) and pair it with PydanticAI for deterministic, SHA-256 certified e-signatures.