1. Freeze the graph at the approval point
With LetsPingCheckpointer, you treat the approval step as a checkpoint in the graph. When the agent reaches a risky tool, it calls lp.defer() instead of the tool directly. LetsPing encrypts the state, stores it, and returns a request id.
import { StateGraph, START } from "@langchain/langgraph";
import { LetsPing } from "@letsping/sdk";
import { LetsPingCheckpointer } from "@letsping/sdk/integrations/langgraph";
const lp = new LetsPing(process.env.LETSPING_API_KEY!);
const checkpointer = new LetsPingCheckpointer(lp);
const builder = new StateGraph({ channels: { thread_id: null, step: null, tool_payload: null } })
.addNode("dangerous_tool", async (state) => {
if (state.step === "START") {
const { id } = await lp.defer({
service: "billing-agent",
action: "stripe:refund",
priority: "high",
payload: state.tool_payload,
state_snapshot: { thread_id: state.thread_id },
});
return { ...state, step: "NEEDS_APPROVAL", request_id: id };
}
return state;
})
.addEdge(START, "dangerous_tool");
export const graph = builder.compile({ checkpointer });2. Resume after approval and call the tool safely
When the human approves in the LetsPing dashboard, a webhook is fired. Your webhook handler verifies the signature, decrypts the state, and resumes the graph with the approved or patched payload.
import { NextRequest, NextResponse } from "next/server";
import { LetsPing } from "@letsping/sdk";
import { graph } from "@/lib/langgraph-graph";
const lp = new LetsPing();
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const sig = req.headers.get("x-letsping-signature") || "";
const { data, state_snapshot } = await lp.webhookHandler(
rawBody,
sig,
process.env.WEBHOOK_SIGNING_SECRET!
);
const threadId = state_snapshot?.thread_id as string | undefined;
if (!threadId) {
return NextResponse.json({ error: "missing_thread_id" }, { status: 400 });
}
const safePayload = data.patched_payload ?? data.payload;
await graph.invoke(
{ step: "NEEDS_APPROVAL", tool_payload: { overwrite: safePayload } },
{ configurable: { thread_id: threadId } }
);
return NextResponse.json({ ok: true });
}3. How this satisfies production and InfoSec requirements
- Every risky tool call has an explicit approval or rejection recorded with actor id.
- Graph state is encrypted before persistence; no plaintext state in your database.
- Webhooks are signed and bounded by a five-minute replay window.
For a deeper architectural deep dive, see The 2026 Guide to Securing LangGraph in Production.