All articles

A working architecture for an agent that drafts and routes contracts: the MCP server for the conversational half, @signvoy/node and webhooks for the deterministic half.

Signvoy Team2026年7月21日

Most "AI agent sends contracts" demos are one prompt and one tool call. That works in a screenshot and falls apart the moment a template has two recipient roles, or a recipient has no email address, or the same request arrives twice.

This is a working architecture instead. The core idea: an agent that sends contracts has a conversational half and a deterministic half, and putting either one in the other's place is where these systems go wrong.

The split

Conversational — figuring out what the user wants, which template applies, who the recipients are, and getting a human to approve. Genuinely hard to specify in code, and exactly what a model is good at. This half goes through the MCP server.

Deterministic — sending, retrying on failure, deduplicating repeats, reacting to signature events, storing the signed PDF. Fully specifiable, and a model asked to decide whether to retry a 500 is a worse retry policy than a for loop. This half goes through the REST API with @signvoy/node.

A useful test for which side a step belongs on: if you can write the rule down, write the rule down.

Step 1 — Let the model gather intent

Connect the MCP server and the model can drive the whole discovery sequence itself. No integration code:

json
{
  "mcpServers": {
    "signvoy": {
      "url": "https://api.signvoy.com/mcp",
      "transport": "streamable-http"
    }
  }
}

Given "send the contractor NDA to Priya at priya@example.com," the model calls signvoy_list_templates, then signvoy_get_template to read the recipient role keys. Role keys are the detail that breaks naive implementations: a template defines keys like signer or witness, and every recipient must match one. The model reads them rather than guessing.

Then signvoy_create_document — and it stops, with a draft and a review_url.

Step 2 — Put the approval outside the model

The most important design decision, and it is a negative one: the model does not own the approval step.

typescript
// Your orchestration layer, not the model's decision
const draft = await mcp.call("signvoy_create_document", {
  template_id: templateId,
  recipients: [{ roleKey: "signer", name, email }],
});

if (draft.document.send_blockers?.length) {
  return { status: "needs_fix", blockers: draft.document.send_blockers };
}

// Human sees the actual document, not a "Send doc abc-123?" prompt
await notifyForApproval({
  reviewUrl: draft.document.review_url,
  recipients: draft.document.recipients,
});
// Sending happens in a separate request, after a real click.

Two things make this work.

The protocol enforces the boundary. Creating and sending are separate tools on Signvoy's server, so an agent that misreads "draft an NDA" as "send an NDA" produces a draft you delete. If your provider exposes a single create-and-send tool, this boundary exists only in your prompt — and a prompt is not a boundary. That matters most when the agent reads untrusted input; the threat model is in human-in-the-loop AI contract signing.

Approval means reading the document. review_url points at the real thing. A confirmation dialog showing a UUID is not a control.

Step 3 — Handle blockers instead of failing

signvoy_create_document returns a draft even when something is wrong, with a send_blockers array explaining what. The most common case is a recipient with no email address.

Let the model fix it — this is a conversational problem:

Priya's email is priya.sharma@example.com.

It calls signvoy_update_document. One sharp edge to know about: passing recipients replaces the entire list rather than merging, so a two-signer document requires both entries even when one is unchanged. Models get this right when the tool description says so, but if you are calling the tool from code, read that sentence twice.

Step 4 — Send deterministically

Once a human has approved, leave the model behind. This is a specifiable operation:

typescript
import Signvoy from "@signvoy/node";

const signvoy = new Signvoy({
  apiKey: process.env.SIGNVOY_API_KEY!,
  workspaceId: process.env.SIGNVOY_WORKSPACE_ID!,
});

await signvoy.documents.send(documentId);

Backend services have no user to sit through a consent screen, so this half uses a workspace-scoped API key rather than OAuth — which is the case where a static key is the right primitive rather than a shortcut.

Send is idempotent on both surfaces: calling it on an already-sent document is a no-op, not a duplicate email to your counterparty. That makes a naive retry safe, which matters because a network timeout on a send is otherwise the scariest failure in the system.

Step 5 — Webhooks, not polling

The reflex is to have the agent poll for status. Resist it: polling burns rate limit — 120 requests per 60 seconds on the MCP server — and adds latency for nothing.

typescript
// POST /webhooks/signvoy
import { createHmac, timingSafeEqual } from "node:crypto";

export async function POST(req: Request) {
  const raw = await req.text();
  const signature = req.headers.get("signvoy-signature")!;
  const expected = createHmac("sha256", process.env.SIGNVOY_WEBHOOK_SECRET!)
    .update(raw)
    .digest("hex");

  if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return new Response("invalid signature", { status: 401 });
  }

  const eventId = req.headers.get("signvoy-event-id")!;
  if (await alreadyProcessed(eventId)) return new Response("ok");

  const event = JSON.parse(raw);
  if (event.type === "document.completed") {
    await onContractSigned(event.data.documentId);
  }

  await markProcessed(eventId);
  return new Response("ok");
}

Three details that separate a demo from something you can leave running: verify the HMAC with a timing-safe comparison, deduplicate on Signvoy-Event-Id because retries will happen, and return 200 quickly — do the real work in a background job. The webhooks guide covers the full event list and the retry semantics.

Step 6 — Signing inside your own product

If your agent lives in your application rather than in a chat client, the signature should too.

typescript
const session = await signvoy.embed.createSession({
  documentId,
  documentRecipientId,
  expiresInSeconds: 3600,
});
// session.url renders in an iframe or <SignvoyEmbed />

Mint this server-side only. The URL is a bearer credential — anyone holding it can sign as that recipient — so never log it, never put it in a chat transcript, and never let an agent echo it into a summary.

The MCP equivalent is signvoy_create_signing_link. Two constraints worth knowing before you design around it: the document must be in status exactly sent, and expires_in_seconds is currently accepted but not applied, so the URL inherits the recipient's existing signing token expiry. If you need a specific TTL, use the REST endpoint. Embedding e-signatures in your app covers the client side.

The whole shape

User intent
  → MCP: list templates, read roles, create draft   (model)
  → send_blockers? MCP: update draft                (model)
  → review_url → human approval                     (person)
  → REST: documents.send                            (code)
  → webhook: document.completed                     (code)
  → REST: fetch signed PDF, store it                (code)

The model appears twice, in both cases doing something underspecified, and never at the moment something irreversible happens.

What to get right

  • Publish your templates. Draft-only templates cannot create documents, and this is the most common first failure.
  • Read role keys, never assume them. signvoy_get_template before signvoy_create_document, always.
  • Treat every document ID as untrusted. List resources rather than letting a model reconstruct a UUID from memory.
  • Store the audit trail reference. MCP-originated actions are tagged actorSource: "mcp", which is what lets you answer "did an agent do this?" six months later.
  • Scope credentials per environment. Separate workspaces for staging and production; a test agent should be incapable of emailing a real counterparty.

Get started

Create a free Signvoy account, publish a template, connect the MCP server, and read the API quickstart for the deterministic half. If you are still deciding how much of the work belongs on each surface, MCP or REST API draws the line.

署名の催促をやめよう

Signvoyを使って取引を成立させ、クライアントをオンボーディングし、契約書に署名している数千のチームに参加しましょう — より速く。

クレジットカード不要 · 永久無料プラン · いつでもキャンセル可能