Attio & Twenty CRM + Signbee: Deal-Stage E-Signature Automation (2026)
High-velocity revenue teams are migrating off bloated Salesforce and HubSpot instances in favor of flexible, developer-first platforms like Attio and Twenty CRM. Here is the complete engineering blueprint for wiring deal-stage webhooks to Signbee so sales orders compile, send, and close autonomously.
Quick Answer (TL;DR)
Zero-Touch Quote-to-Cash • Attio REST API v2 & Twenty CRM GraphQL
To automate e-signatures in Attio or Twenty CRM, subscribe a serverless webhook listener to deal stage transitions (record.updated in Attio or opportunity.updated in Twenty). When a deal enters the Contract Sent stage, query the CRM API for the linked company, primary contact, line items, and payment terms. Interpolate those attributes into a CommonMark Markdown order form and send a single HTTP request to POST https://signb.ee/api/v1/send. When the prospect signs, Signbee fires a document.signed webhook containing the SHA-256 certificate hash, allowing your handler to automatically flip the deal to Closed Won.
1. The 2026 CRM Unbundling: Why RevOps Teams Are Replacing Monoliths with Attio & Twenty
For over a decade, B2B revenue operations teams accepted a painful architectural compromise: if you wanted automated quote-to-cash and electronic signature workflows, you had to pay an enterprise tax to both a monolithic CRM (Salesforce or HubSpot Enterprise) and a legacy e-signature vendor (DocuSign or Adobe Sign). As we documented in our breakdown of DocuSign API pricing and hidden seat charges, pairing a legacy CRM with a per-envelope signature contract routinely costs mid-market engineering organizations $35,000 to $90,000 annually before a single line of integration code is deployed.
In 2026, two developer-centric platforms have broken this monopoly:
- Attio: The AI-native, real-time relational CRM built around custom objects, enriched company graphs, programmable lists, and a strongly typed REST API v2 with granular webhook subscriptions.
- Twenty CRM: The leading open-source Salesforce alternative (backed by Y Combinator and over 25,000 GitHub stars), powered by TypeScript, NestJS, PostgreSQL, and a unified GraphQL + REST metadata layer that teams can self-host on Docker or run on Twenty Cloud.
However, moving to a fast, programmable CRM creates an immediate bottleneck if your contract execution layer remains stuck in 2015. Neither Attio nor Twenty should be bogged down by clunky iframe widgets that force Account Executives to manually drag signature boxes onto uploaded Word documents. When evaluating the best e-signature APIs for developers in 2026, revenue engineers consistently prioritize Markdown-to-PDF compilation, stateless JSON payloads, and bidirectional webhook synchronization.
“In Attio and Twenty, every deal is already a structured JSON graph of pricing tiers, seat counts, billing terms, and decision-maker metadata. Converting that structured graph into a static Word doc just to manually drop x/y signature coordinates is an anti-pattern. Contracts should compile straight from CRM attributes.”— Michael Beckett, Founder at Signbee
2. Architecture Comparison: Attio vs. Twenty CRM vs. Legacy CRM E-Signing
Before writing the webhook handlers, it is essential to understand how Attio and Twenty model deals, relationships, and outbound events differently from legacy platforms. While teams exploring an open-source e-signature API comparison often self-host Twenty CRM on Kubernetes or Railway alongside internal microservices, Attio teams frequently deploy lightweight Cloudflare Workers or Next.js App Router endpoints to orchestrate their deal pipelines.
| Architectural Dimension | Attio CRM + Signbee | Twenty CRM + Signbee | Salesforce + DocuSign Legacy |
|---|---|---|---|
| Core Data Model | Objects (deals, companies) + Lists | Standard & Custom Objects (opportunity) | Heavy sObjects + CPQ Managed Packages |
| Primary API Protocol | REST API v2 (api.attio.com/v2) | GraphQL (/graphql) & REST (/rest) | SOAP / Apex Triggers / Bulk XML |
| Deal Stage Webhook Event | record.updated / list-entry.updated | opportunity.updated | Apex Outbound Message / Platform Event |
| Contract Template Engine | CommonMark Markdown via Signbee API | CommonMark Markdown via Signbee API | Proprietary DOCX Anchor Tag Coordinates |
| End-to-End Dispatch Latency | 310ms – 480ms | 280ms – 420ms | 4,500ms – 12,000ms |
| E-Signature Cost Structure | $39/mo flat (Unlimited envelopes) | $39/mo flat (Unlimited envelopes) | $480+/yr per user + $3.50–$6.00/envelope |
3. Reference Architecture: The Zero-Touch Deal-to-Signature State Machine
A resilient CRM-to-e-signature integration must function as a deterministic state machine. Whether your sales team works inside Attio's Kanban views or Twenty CRM's Opportunity board, moving a card into the Contract Sent column should trigger a four-stage asynchronous workflow that requires zero manual PDF uploads:
Stage Transition Detection & Idempotency Check
The CRM fires an outbound webhook when the deal status attribute changes to Contract Sent. The webhook receiver validates the HMAC signature and inspects whether the deal record already holds an active signbee_document_id to prevent duplicate dispatches if a user accidentally drags a card twice.
Relational Hydration (Company, Signer & Commercial Terms)
Webhook payloads only carry shallow record deltas. Your integration queries Attio's GET /v2/objects/deals/records/{id} or Twenty's GraphQL opportunity(filter: ...) query to resolve the linked Company legal name, registered address, primary decision-maker email, annual contract value (ACV), payment frequency, and custom SLA terms.
CommonMark Order Form Compilation & Signbee Dispatch
The hydrated CRM attributes are injected into a typed TypeScript template that outputs clean CommonMark Markdown—complete with dynamically calculated pricing tables, multi-year ramp schedules, and governing law clauses. One POST https://signb.ee/api/v1/send call compiles the PDF, binds Party A (your company), and dispatches the signing link to the buyer.
Cryptographic Verification & Closed-Won Synchronization
When Party B signs on mobile or desktop, Signbee appends the A4 Certificate of Completion with a tamper-evident SHA-256 hash and sends a document.signed webhook back to your server. Your handler updates the CRM stage to Closed Won, attaches the public verification URL, and logs an audit note on the timeline.
4. Step-by-Step Implementation: Attio REST API v2 + Webhooks to Signbee
In Attio, deals are typically stored either as records on the standard deals object (with a stage status attribute) or as entries inside a workspace List. Attio's v2 API represents attribute values as arrays of typed objects (for example, values.stage[0].status.title for status attributes and values.value[0].currency_value for currency attributes).
Before deploying the handler, add two custom attributes to your Attio deals object:
signbee_document_id(Text attribute): Stores the unique Signbee document identifier for idempotency and status correlation.signbee_verify_url(Text/URL attribute): Stores the immutable cryptographic verification URL (https://signb.ee/verify/[id]) once executed.
Below is a complete, production-hardened Next.js App Router webhook endpoint (app/api/webhooks/attio/route.ts) that listens for Attio record.updated events, hydrates the linked primary contact and company record via Attio REST API v2, builds a dynamic SaaS Order Form in CommonMark, and dispatches it via Signbee:
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
const ATTIO_API_KEY = process.env.ATTIO_API_KEY!;
const ATTIO_WEBHOOK_SECRET = process.env.ATTIO_WEBHOOK_SECRET!;
const SIGNBEE_API_KEY = process.env.SIGNBEE_API_KEY!;
function verifyAttioSignature(rawBody: string, signatureHeader: string | null): boolean {
if (!signatureHeader) return false;
const expected = crypto
.createHmac("sha256", ATTIO_WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}
async function attioFetch(path: string, options: RequestInit = {}) {
const res = await fetch(`https://api.attio.com/v2${path}`, {
...options,
headers: {
Authorization: `Bearer ${ATTIO_API_KEY}`,
"Content-Type": "application/json",
...(options.headers || {}),
},
});
if (!res.ok) {
throw new Error(`Attio API error (${res.status}): ${await res.text()}`);
}
return res.json();
}
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const signature = req.headers.get("Attio-Signature");
if (!verifyAttioSignature(rawBody, signature)) {
return NextResponse.json({ error: "Invalid Attio webhook signature" }, { status: 401 });
}
const payload = JSON.parse(rawBody);
const event = payload.events?.[0];
if (!event || event.event_type !== "record.updated") {
return NextResponse.json({ skipped: true });
}
const recordId = event.id?.record_id;
const objectId = event.id?.object_id;
// 1. Hydrate the full Deal record from Attio REST API v2
const dealRes = await attioFetch(`/objects/${objectId}/records/${recordId}`);
const values = dealRes.data.values;
const currentStage = values.stage?.[0]?.status?.title;
const existingSignbeeId = values.signbee_document_id?.[0]?.value;
// Guard: Only trigger when stage is 'Contract Sent' and no envelope exists yet
if (currentStage !== "Contract Sent" || existingSignbeeId) {
return NextResponse.json({ status: "ignored", currentStage, existingSignbeeId });
}
const dealName = values.name?.[0]?.value ?? "Enterprise Subscription Order";
const dealAmountCents = values.value?.[0]?.currency_value ?? 2400000;
const formattedAnnualValue = (dealAmountCents / 100).toLocaleString("en-US", {
style: "currency",
currency: values.value?.[0]?.currency_code ?? "USD",
});
// 2. Resolve associated Company & Primary Contact Record IDs
const companyRef = values.associated_company?.[0]?.target_record_id;
const contactRef = values.primary_contact?.[0]?.target_record_id;
const [companyRes, contactRes] = await Promise.all([
companyRef ? attioFetch(`/objects/companies/records/${companyRef}`) : null,
contactRef ? attioFetch(`/objects/people/records/${contactRef}`) : null,
]);
const companyName = companyRes?.data?.values?.name?.[0]?.value ?? "Client Organization";
const recipientName =
contactRes?.data?.values?.name?.[0]?.full_name ?? "Authorized Signatory";
const recipientEmail =
contactRes?.data?.values?.email_addresses?.[0]?.email_address;
if (!recipientEmail) {
return NextResponse.json(
{ error: "Primary contact on Attio deal is missing an email address" },
{ status: 422 }
);
}
// 3. Compile CommonMark Order Form with Dynamic Markdown Pricing Table
const orderFormMarkdown = `# Commercial Order Form — ${companyName}
**Order Reference:** ATTIO-${recordId.slice(0, 8).toUpperCase()}
**Effective Date:** ${new Date().toISOString().split("T")[0]}
**Customer Legal Entity:** ${companyName}
**Primary Billing Contact:** ${recipientName} (${recipientEmail})
---
## 1. Subscription Services & Commercial Schedule
| Subscription Tier | Included Seats | Billing Cadence | Annual Contract Value |
|---|---|---|---|
| Enterprise Platform License | 50 Seats | Annual Upfront | ${formattedAnnualValue} |
| Dedicated Webhook & API SLA | Unlimited | Included | $0.00 |
| **Total Annual Commitment** | **50 Seats** | **Net 30** | **${formattedAnnualValue}** |
## 2. Governing Terms & Execution
This Order Form incorporates by reference the Master SaaS Subscription Agreement. By executing this Order Form electronically via Signbee, both parties agree to be legally bound by the commercial terms set forth above.`;
// 4. Dispatch via Signbee POST /api/v1/send
const signbeeRes = await fetch("https://signb.ee/api/v1/send", {
method: "POST",
headers: {
Authorization: `Bearer ${SIGNBEE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: `Order Form — ${companyName} (${dealName})`,
markdown: orderFormMarkdown,
sender_name: "Revenue Operations",
sender_email: "revops@acmecloud.io",
recipient_name: recipientName,
recipient_email: recipientEmail,
expires_in_days: 14,
webhook_url: `https://crm-bridge.acmecloud.io/api/webhooks/signbee?crm=attio&dealId=${recordId}`,
}),
});
const signbeeData = await signbeeRes.json();
// 5. Write Signbee Document ID back to Attio for idempotency
await attioFetch(`/objects/${objectId}/records/${recordId}`, {
method: "PATCH",
body: JSON.stringify({
data: {
values: {
signbee_document_id: [{ value: signbeeData.id }],
},
},
}),
});
return NextResponse.json({ dispatched: true, signbeeDocumentId: signbeeData.id });
}Notice how clean this architecture is compared to legacy CPQ plugins. There are no brittle PDF field coordinates or multi-step envelope upload calls. If you need an affordable e-signature API integration that scales from 10 deals a month to 10,000 automated renewals without per-envelope fees, compiling CommonMark directly from Attio's JSON graph eliminates both engineering maintenance and vendor lock-in.
5. Step-by-Step Implementation: Twenty CRM (Open-Source) GraphQL + Webhooks to Signbee
Twenty CRM has rapidly become the default choice for engineering-led companies that want full ownership of their customer data in PostgreSQL alongside a modern React + GraphQL interface. In Twenty CRM, pipeline deals are modeled using the standard opportunity object, which links directly to company and pointOfContact (person) records.
In Twenty's Settings → Developers → Webhooks menu, create an outbound webhook listening to opportunity.updated. Whenever a sales rep moves an Opportunity card to CONTRACT_SENT, Twenty POSTs the updated opportunity record to your listener. Your listener then executes a single GraphQL query against https://your-twenty-instance.com/graphql to fetch the nested company and contact fields, dispatches the Signbee agreement, and mutates the opportunity record with the generated document ID:
import { NextRequest, NextResponse } from "next/server";
const TWENTY_GRAPHQL_URL = process.env.TWENTY_GRAPHQL_URL!; // e.g. https://api.twenty.com/graphql
const TWENTY_API_KEY = process.env.TWENTY_API_KEY!;
const SIGNBEE_API_KEY = process.env.SIGNBEE_API_KEY!;
async function twentyGraphQL<T>(query: string, variables: Record<string, unknown>): Promise<T> {
const response = await fetch(TWENTY_GRAPHQL_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${TWENTY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
});
const json = await response.json();
if (json.errors?.length) {
throw new Error(`Twenty GraphQL Error: ${JSON.stringify(json.errors)}`);
}
return json.data as T;
}
const GET_OPPORTUNITY_DETAILS = `
query GetOpportunityForContract($id: UUID!) {
opportunity(filter: { id: { eq: $id } }) {
id
name
stage
amount {
amountMicros
currencyCode
}
closeDate
signbeeDocumentId
company {
id
name
domainName
address
}
pointOfContact {
id
name {
firstName
lastName
}
emails {
primaryEmail
}
}
}
}
`;
const UPDATE_OPPORTUNITY_SIGNBEE_ID = `
mutation LockOpportunityEnvelope($id: UUID!, $signbeeDocumentId: String!) {
updateOpportunity(
id: $id
data: { signbeeDocumentId: $signbeeDocumentId }
) {
id
signbeeDocumentId
}
}
`;
export async function POST(req: NextRequest) {
const body = await req.json();
if (body.eventName !== "opportunity.updated") {
return NextResponse.json({ ignored: true });
}
const opportunityId = body.record?.id;
if (!opportunityId || body.record?.stage !== "CONTRACT_SENT") {
return NextResponse.json({ status: "stage_not_contract_sent" });
}
// 1. Query Twenty CRM GraphQL API for full relational context
const data = await twentyGraphQL<{ opportunity: any }>(GET_OPPORTUNITY_DETAILS, {
id: opportunityId,
});
const opp = data.opportunity;
if (!opp || opp.signbeeDocumentId) {
return NextResponse.json({ status: "already_dispatched" });
}
const amountMajor = (Number(opp.amount?.amountMicros ?? 0) / 1_000_000).toLocaleString("en-US", {
style: "currency",
currency: opp.amount?.currencyCode || "USD",
});
const signerFullName = `${opp.pointOfContact?.name?.firstName ?? ""} ${
opp.pointOfContact?.name?.lastName ?? ""
}`.trim();
const signerEmail = opp.pointOfContact?.emails?.primaryEmail;
if (!signerEmail) {
return NextResponse.json({ error: "Missing pointOfContact primaryEmail" }, { status: 422 });
}
// 2. Dispatch Master Services Agreement + Order Schedule to Signbee
const signbeeResponse = await fetch("https://signb.ee/api/v1/send", {
method: "POST",
headers: {
Authorization: `Bearer ${SIGNBEE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: `MSA & Order Schedule — ${opp.company?.name ?? opp.name}`,
markdown: `# Master Services Agreement & Order Schedule
**Opportunity ID:** ${opp.id}
**Client Organization:** ${opp.company?.name} (${opp.company?.domainName})
**Registered Address:** ${opp.company?.address ?? "On File"}
**Total Contract Value:** ${amountMajor}
## 1. Commercial Scope
| Line Item | Description | Total Fee |
|---|---|---|
| ${opp.name} | Annual Platform & API Access | ${amountMajor} |
## 2. Binding Electronic Execution
Executed electronically pursuant to the US ESIGN Act, UETA, and UK Electronic Communications Act 2000.`,
sender_name: "Legal & Deal Desk",
sender_email: "deals@yourdomain.com",
recipient_name: signerFullName || "Authorized Signatory",
recipient_email: signerEmail,
expires_in_days: 10,
webhook_url: `https://crm-bridge.yourdomain.com/api/webhooks/signbee?crm=twenty&dealId=${opp.id}`,
}),
});
const createdDoc = await signbeeResponse.json();
// 3. Persist Signbee Document ID back into Twenty CRM via GraphQL Mutation
await twentyGraphQL(UPDATE_OPPORTUNITY_SIGNBEE_ID, {
id: opp.id,
signbeeDocumentId: createdDoc.id,
});
return NextResponse.json({ dispatched: true, documentId: createdDoc.id });
}6. Closing the Loop: Handling Signbee's document.signed Webhook
Sending the contract is only half of the quote-to-cash cycle. When the prospect opens their signing link—or if you embed the ceremony directly inside your customer onboarding portal using our white-label e-signature API patterns—Signbee executes a two-pass cryptographic seal. It appends an A4 Certificate of Completion recording both parties' IP addresses, UTC timestamps, and the SHA-256 hash of the final PDF bytes, then fires a document.signed webhook to the webhook_url you supplied at dispatch time.
Here is the unified callback handler (app/api/webhooks/signbee/route.ts) that receives the completion payload from Signbee and automatically transitions either an Attio Deal or a Twenty CRM Opportunity to Closed Won while appending a cryptographic audit note to the record timeline:
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const { searchParams } = new URL(req.url);
const crmType = searchParams.get("crm"); // "attio" | "twenty"
const dealId = searchParams.get("dealId");
const event = await req.json();
if (event.event !== "document.signed" || !dealId) {
return NextResponse.json({ received: true });
}
const { document_id, signed_at, sha256_hash, verify_url, recipient_email } = event.data;
if (crmType === "attio") {
// 1. Update Attio Deal stage to 'Closed Won' and save verification link
await fetch(`https://api.attio.com/v2/objects/deals/records/${dealId}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.ATTIO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
values: {
stage: "Closed Won",
signbee_verify_url: [{ value: verify_url }],
},
},
}),
});
// 2. Append an immutable audit note to the Attio Deal timeline
await fetch("https://api.attio.com/v2/notes", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ATTIO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
parent_object: "deals",
parent_record_id: dealId,
title: "Contract Executed via Signbee (SHA-256 Verified)",
format: "plaintext",
content: `Signed by ${recipient_email} at ${signed_at}.\nDocument ID: ${document_id}\nSHA-256 Hash: ${sha256_hash}\nVerification Certificate: ${verify_url}`,
},
}),
});
} else if (crmType === "twenty") {
// Update Twenty CRM Opportunity stage to CLOSED_WON via GraphQL
await fetch(process.env.TWENTY_GRAPHQL_URL!, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
mutation MarkOpportunityClosedWon($id: UUID!, $verifyUrl: String!) {
updateOpportunity(
id: $id
data: {
stage: CLOSED_WON
signbeeVerifyUrl: $verifyUrl
}
) {
id
stage
}
}
`,
variables: {
id: dealId,
verifyUrl: verify_url,
},
}),
});
}
return NextResponse.json({ synced: true, crmType, dealId, document_id });
}7. Field-by-Field CRM Attribute Mapping & Edge Cases
When wiring CRM objects to Signbee in production, data hygiene edge cases inevitably surface: multi-currency deals, missing middle names on Person records, or deals with multiple billing contacts. Use the following cross-platform schema mapping table to standardize your adapter layer across Attio and Twenty CRM:
| Signbee API Parameter | Attio v2 Record Path | Twenty CRM GraphQL Path | Fallback & Validation Rule |
|---|---|---|---|
| title | values.name[0].value | opportunity.name | Prefix with company name; max 200 chars |
| recipient_name | values.name[0].full_name | pointOfContact.name.firstName + lastName | Block dispatch or default to “Authorized Signatory” |
| recipient_email | values.email_addresses[0].email_address | pointOfContact.emails.primaryEmail | Reject with HTTP 422 and post Slack/CRM alert if null |
| markdown (Contract Value) | values.value[0].currency_value / 100 | opportunity.amount.amountMicros / 1e6 | Format via Intl.NumberFormat using ISO currency code |
| expires_in_days | values.close_date[0].value | opportunity.closeDate | Clamp between 1 and 90 days (default 14 days) |
| Idempotency Lock | values.signbee_document_id[0].value | opportunity.signbeeDocumentId | Abort send if populated unless status is EXPIRED |
Notice the numeric unit difference between the two CRMs: Attio stores currency values in minor units (cents, dividing by 100), whereas Twenty CRM stores monetary fields in micros (amountMicros, dividing by 1,000,000). Standardizing this conversion inside a shared helper prevents costly decimal-point errors in generated sales orders.
8. Agentic Deal Desk: Pairing Attio/Twenty MCP Servers with Signbee MCP
While webhook automation handles standard self-serve and mid-market deals with zero human intervention, enterprise deals frequently involve bespoke redlines, custom security addenda, or quarterly payment schedules discussed during live negotiation calls.
By configuring the Signbee MCP server (npx -y signbee-mcp) alongside your Attio or Twenty CRM MCP server inside Claude Desktop, Cursor, or an internal Slack deal-desk agent, your revenue team can run conversational deal-desk workflows without ever opening a PDF editor. As highlighted in our 2026 e-signature API pricing comparison, Signbee includes full MCP and REST API access on every tier—and you can prototype your entire Attio or Twenty webhook pipeline today using our free e-signature API sandbox tier (3 free documents per month, no credit card required).
{
"mcpServers": {
"signbee": {
"command": "npx",
"args": ["-y", "signbee-mcp"],
"env": {
"SIGNBEE_API_KEY": "signbee_live_98a7bc65d43e21f0..."
}
},
"attio-crm": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-attio"],
"env": {
"ATTIO_API_KEY": "attio_v2_secret_..."
}
},
"twenty-crm": {
"command": "npx",
"args": ["-y", "twenty-mcp-server"],
"env": {
"TWENTY_API_KEY": "twenty_jwt_...",
"TWENTY_BASE_URL": "https://api.twenty.com"
}
}
}
}Frequently Asked Questions: Attio & Twenty CRM E-Signature Automation
How do I automatically send an e-signature contract when a deal moves to Contract Sent in Attio?
To automate contract dispatch in Attio, register a v2 webhook subscription targeting the record.updated or list-entry.updated event filtered on your Deals object or Sales Pipeline list. When an Account Executive drags a deal card into the Contract Sent status column, your Next.js or Cloudflare Worker endpoint verifies the Attio HMAC-SHA256 signature, fetches the associated Company and Person records via the Attio REST API v2, interpolates the deal value, billing cadence, and line items into a CommonMark markdown template, and calls POST https://signb.ee/api/v1/send. Signbee compiles the PDF, binds Party A, and emails the counterparty their signing link within 350ms.
How does Twenty CRM integrate with Signbee for open-source deal automation?
Twenty CRM exposes a unified GraphQL API (/graphql) and native outbound webhooks for object lifecycle events such as opportunity.updated. When an Opportunity stage changes to CONTRACT_SENT inside a self-hosted or cloud Twenty CRM workspace, Twenty dispatches a JSON webhook payload containing the opportunity ID and stage transition. Your webhook receiver queries Twenty's GraphQL API to retrieve the linked Company, Point of Contact, ARR amount, and custom fields, renders the commercial Order Form in Markdown, and submits it to Signbee. When the customer signs, Signbee's document.signed webhook updates the Twenty Opportunity stage to CLOSED_WON via a GraphQL mutation.
Why do revenue operations teams choose Signbee over legacy e-signature tools for Attio and Twenty CRM?
Legacy platforms like DocuSign and PandaDoc were archected for monolithic CRMs like Salesforce and HubSpot, charging $40 to $125 per seat plus envelope overages while forcing developers to maintain brittle PDF coordinate tags (x, y, page) whenever a pricing table changes height. Signbee is purpose-built for programmatic, developer-first CRMs like Attio and Twenty. Instead of uploading static Word templates and manually placing anchor strings, revenue engineers generate dynamic CommonMark Markdown tables directly from CRM attributes and dispatch contracts via a single JSON POST request for a flat $39/month with unlimited documents and zero per-seat taxation.
How do I prevent duplicate contracts from being sent if a sales rep moves a deal card back and forth?
Production CRM webhook handlers must enforce idempotency at two layers. First, store the Signbee document_id directly on a custom attribute inside Attio (e.g., signbee_document_id) or Twenty CRM (e.g., signbeeDocumentId) immediately after POST /api/v1/send succeeds. Before dispatching any new contract on a record.updated event, inspect whether signbee_document_id is already populated and in a PENDING or SIGNED state. Second, compute a deterministic idempotency key from the Deal ID, stage name, and deal amount so concurrent webhook retries from Attio or Twenty never generate duplicate envelopes.
Can AI agents inside Attio or Claude Desktop trigger Signbee contracts via the Model Context Protocol (MCP)?
Yes. Both Attio and Twenty CRM have strong MCP ecosystems that can be paired directly with the Signbee MCP server (npx -y signbee-mcp). A revenue operator working in Claude Desktop, Cursor, or an internal AI sales agent can run a single prompt that queries the CRM MCP server for all deals in the Proposal Approved stage, verifies that billing addresses and decision-maker emails are populated, synthesizes tailored Master Services Agreements with custom SLA clauses in CommonMark, and invokes Signbee's send_document MCP tool—logging the resulting verification URL and document ID back to the CRM timeline automatically.
How does Signbee attach the signed PDF and cryptographic certificate back into Attio and Twenty CRM?
When the counterparty completes the interactive signing ceremony, Signbee seals the PDF with a two-pass SHA-256 cryptographic Signing Certificate and fires an HTTP POST webhook (event: document.signed) to your listener endpoint. The webhook payload includes the document_id, signed_at timestamp, signer IP address, cryptographic sha256_hash, and the public verification URL (https://signb.ee/verify/[id]). Your handler then calls Attio's POST /v2/notes or attribute update endpoint—or Twenty CRM's updateOpportunity GraphQL mutation—to transition the deal to Closed Won, record the tamper-evident hash, and trigger downstream Stripe invoicing.
Automate Your Attio & Twenty CRM Deal Pipeline Today
Replace manual Word templates and per-seat DocuSign fees with a single JSON POST request. Start free with 3 documents/month or unlock unlimited automated contracts for $39/month.