Integrating SignedApproval with LangChain, CrewAI, and OpenAI Agents
Every agent framework has a moment where the agent decides to take an action. Before that action executes, you need a gate: “Is a human okay with this?” This guide shows how to insert that gate using SignedApproval, with concrete code for LangChain, CrewAI, OpenAI Agents, and Claude Code via MCP.
The pattern is always the same:
- Agent wants to take an action
- Agent calls SignedApproval API with a description of the action
- Human gets a push notification, authenticates with passkey/TOTP/Face ID
- Agent receives the decision (approved or rejected) with an Ed25519 signature
- Agent proceeds or aborts based on the decision
Prerequisites
Before integrating, you’ll need:
- A SignedApproval API key (get one here)
- The SignedApproval iOS app installed with push notifications enabled, or TOTP configured in your dashboard settings
- Node.js SDK:
npm install @signedapproval/sdk - Python SDK:
pip install signedapproval
LangChain
LangChain’s tool system lets you wrap any function as a tool the agent can call. The cleanest approach is to create a “gated tool” pattern — a wrapper that requires SignedApproval before executing the underlying tool:
import { SignedApproval } from "@signedapproval/sdk";
import { DynamicTool } from "@langchain/core/tools";
const sa = new SignedApproval({ apiKey: process.env.SA_API_KEY });
function gatedTool(name, description, fn) {
return new DynamicTool({
name,
description,
func: async (input) => {
// Request human approval before executing
const result = await sa.requestApproval({
action: `${name}: ${input}`,
ttl: 300,
});
if (result.decision !== "approved") {
return `Action rejected by human approver. Reason: ${
result.decision
}. Do not retry this action.`;
}
// Approval verified — execute the tool
return fn(input);
},
});
}
// Usage: wrap any dangerous tool
const deployTool = gatedTool(
"deploy_to_production",
"Deploy a version to the production environment",
async (version) => {
await deployService(version, "production");
return `Deployed ${version} to production successfully`;
}
);The agent sees the tool like any other. When it decides to deploy, the approval gate fires transparently. If the human rejects, the agent receives a clear message and can adjust its plan.
CrewAI
CrewAI uses a similar tool pattern. Here is a Python example using the SignedApproval Python SDK to gate a financial transfer tool:
from crewai import Agent, Task, Crew
from crewai.tools import tool
from signedapproval import SignedApproval
sa = SignedApproval(api_key=os.environ["SA_API_KEY"])
@tool("Transfer Funds")
def transfer_funds(recipient: str, amount: float, currency: str = "USD") -> str:
"""Transfer funds to a recipient. Requires human approval."""
result = sa.request_approval(
action=f"Transfer {currency} {amount:,.2f} to {recipient}",
ttl=300,
)
if result.decision != "approved":
return f"Transfer rejected by human approver."
# Execute the transfer
execute_transfer(recipient, amount, currency)
return f"Transferred {currency} {amount:,.2f} to {recipient}"
# The agent uses the tool naturally
finance_agent = Agent(
role="Treasury Agent",
goal="Process approved payment requests",
tools=[transfer_funds],
verbose=True,
)
task = Task(
description="Process the pending invoice from Acme Corp for $2,400",
agent=finance_agent,
)
crew = Crew(agents=[finance_agent], tasks=[task])
crew.kickoff()OpenAI Agents (Function Calling)
OpenAI’s function calling lets the model decide when to invoke functions you define. Add SignedApproval as a gate in the function handler:
import OpenAI from "openai";
import { SignedApproval } from "@signedapproval/sdk";
const openai = new OpenAI();
const sa = new SignedApproval({ apiKey: process.env.SA_API_KEY });
// Define tools for the model
const tools = [
{
type: "function",
function: {
name: "delete_user_data",
description: "Permanently delete all data for a user",
parameters: {
type: "object",
properties: {
user_id: { type: "string", description: "The user ID" },
reason: { type: "string", description: "Reason for deletion" },
},
required: ["user_id", "reason"],
},
},
},
];
// Handle the function call
async function handleFunctionCall(name, args) {
if (name === "delete_user_data") {
// Gate with SignedApproval
const approval = await sa.requestApproval({
action: `Delete all data for user ${args.user_id}. Reason: ${args.reason}`,
ttl: 600,
});
if (approval.decision !== "approved") {
return JSON.stringify({
error: "Human approver rejected this action",
decision: approval.decision,
});
}
// Proceed with deletion
await deleteUserData(args.user_id);
return JSON.stringify({
success: true,
approval_signature: approval.signature,
});
}
}Notice that we return the approval_signature in the response. This lets you store cryptographic proof alongside the action for audit purposes. Months later, you can prove that this specific deletion was authorized.
Claude Code via MCP
SignedApproval ships an MCP (Model Context Protocol) server that integrates directly with Claude Code and other MCP-compatible tools. The MCP server bootstraps its own API key on first run — no manual configuration needed.
Setup
Add the SignedApproval MCP server to your Claude Code configuration:
// ~/.claude/claude_desktop_config.json
{
"mcpServers": {
"signedapproval": {
"command": "npx",
"args": ["-y", "@signedapproval/mcp"],
"env": {
"SIGNEDAPPROVAL_EMAIL": "your@email.com"
}
}
}
}On first use, the MCP server will send a bootstrap consent request to your phone. Once you approve, the server caches its API key locally and all subsequent requests work automatically.
Usage
Once configured, Claude Code can call the request_approval tool whenever it needs human authorization for a dangerous action:
// Claude Code calls this tool automatically when needed:
//
// Tool: signedapproval.request_approval
// Input: {
// "action": "Run database migration: ALTER TABLE users ADD COLUMN ...",
// "ttl": 300
// }
//
// You receive a push notification on your phone.
// Authenticate with Face ID.
// Claude Code continues (or stops if you reject).Direct HTTP (any language, any framework)
If you’re not using one of the above frameworks, or you prefer direct HTTP, the API is two endpoints:
# 1. Create an approval request
curl -X POST https://signedapproval.net/api/v1/approvals/request \
-H "Authorization: Bearer sa_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "Delete all staging data",
"ttl_seconds": 300,
"metadata": { "env": "staging", "requestedBy": "cleanup-agent" }
}'
# Response:
# { "id": "req_abc123...", "status": "pending", "expires_at": "..." }# 2. Poll for decision (or use webhooks)
curl https://signedapproval.net/api/v1/approvals/req_abc123
# Response when approved:
# {
# "id": "req_abc123...",
# "status": "approved",
# "decision": {
# "payload": { "v": 1, "rid": "...", ... },
# "signature": "xK9m2p...",
# "publicKey": "MCow..."
# }
# }Webhook delivery
Polling works, but for production use you’ll want webhooks. Configure a webhook URL in your dashboard settings and SignedApproval will POST the decision to your endpoint as soon as the human acts. Webhook payloads are signed with HMAC-SHA256 using your webhook secret, so you can verify they came from us:
// Verify webhook signature
import crypto from "crypto";
function verifyWebhook(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
return `sha256=${expected}` === signature;
}
// In your webhook handler:
app.post("/webhooks/signedapproval", (req, res) => {
const sig = req.headers["x-signedapproval-signature"];
if (!verifyWebhook(req.rawBody, sig, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const { decision, approval_id } = req.body;
// Process the decision...
});Best practices
Write clear action descriptions
The action text is what the human sees on their phone. “Execute task” tells them nothing. “Transfer $2,400 from operating account to Acme Corp (Invoice #4821)” tells them everything they need to make a decision.
Set appropriate TTLs
A 30-second TTL for a deploy approval means the human has to be staring at their phone. A 24-hour TTL for a fund transfer means the approval might be stale by the time it executes. Match the TTL to the urgency and risk of the action.
Store the signature
When the agent gets an approval, save the complete credential (payload, signature, public key) alongside the action record. This is your audit trail. You can prove authorization months or years later.
Handle rejections gracefully
When the human rejects, return a clear message to the agent explaining that the action was not authorized. Most agent frameworks will replan when a tool returns an error-like response.
Questions about integration? Check the full API documentation or reach out at support. The free tier includes 200 approvals per month — more than enough to build and test your integration.
Related posts
Ready to add cryptographic approvals to your agents?
200 free approvals per month. No credit card required.
Get your API key