Build a Slack and Microsoft Teams Contract Approval Flow (2026)
Eliminate context switching and email bottlenecks by embedding contract generation and e-signature execution directly into your enterprise chat workspace. Learn how to trigger Signbee from Slack Block Kit and Microsoft Teams Adaptive Cards with secure webhook callbacks.
Founder, Signbee
Bring document execution directly to where your engineers and account executives collaborate. Instead of forcing stakeholders to monitor crowded email inboxes or navigate legacy e-sign dashboards, deploy an automated interactive chat flow: a single click inside Slack or Teams initiates contract assembly, dispatches the document via Signbee's REST API, posts the secure signing link into the channel thread, and automatically updates the card with a verified SHA-256 seal upon execution.
The Case for ChatOps Contract Approvals
Modern technical organizations execute their operational workflows in enterprise messaging platforms: incidents are resolved in Slack channels, pull requests are reviewed via bot alerts, and deployments are triggered via slash commands. Yet contract management frequently remains trapped in legacy email loops, where signature requests sit unread for days.
By migrating contract approvals to ChatOps, companies achieve rapid turnaround times:
| Workflow Metric | Legacy Email Workflow | ChatOps (Slack / Teams) |
|---|---|---|
| Average Time to Approval | 24 – 72 hours | < 14 minutes |
| Context Switching | Leaves chat → Opens portal → Logs in | Zero (One-click in-thread execution) |
| Audit Visibility | Siloed in individual employee inboxes | Public in deal / project channel |
| Integration Surface | Heavy proprietary enterprise SDKs | Lightweight REST API + Webhooks |
Platform Comparison: Slack Block Kit vs Teams Adaptive Cards
When designing cross-platform interactive workflows, engineering teams must account for key architectural differences between Slack's Block Kit specification and Microsoft's Adaptive Cards schema:
| Specification Feature | Slack (Block Kit v2) | Microsoft Teams (Adaptive Cards v1.5) |
|---|---|---|
| Payload Structure | JSON Block Array (header, section, actions) | Adaptive Card Schema (Container, ColumnSet) |
| Synchronous ACK Deadline | 3,000 milliseconds | 2,000 milliseconds |
| Asynchronous Update Mechanism | POST to ephemeral response_url | PATCH to conversation activity ID |
| Action Handler Type | button / static_select block_actions | Action.Execute / Action.Submit |
| Authentication Verification | HMAC-SHA256 (v0=timestamp:body) | Azure Bot Framework Bearer Token / HMAC |
The End-to-End ChatOps State Machine
The contract execution lifecycle operates across five deterministic stages:
- Channel Trigger: An account executive or bot posts an approval card in Slack or Teams.
- Interactive Dispatch: The approving manager clicks "Approve & Generate Agreement".
- Instant Acknowledgment: The backend validates the cryptographic signature and acknowledges within 2 seconds.
- Signbee API Generation: The backend asynchronously calls
POST /api/v1/sendwith contract Markdown and recipient details. - Webhook Finalization: When the recipient signs, Signbee emits
document.signed, triggering an in-place message update with the certified PDF link and SHA-256 seal.
Step 1: Timing-Safe Slack Signature Verification
To prevent spoofing attacks, Slack signs every outbound webhook with an HMAC-SHA256 signature using your application's Signing Secret. Your receiver must verify this signature before processing actions:
import crypto from "crypto";
export function verifySlackRequest(
slackSigningSecret: string,
signatureHeader: string | undefined,
timestampHeader: string | undefined,
rawBody: string | Buffer
): boolean {
if (!signatureHeader || !timestampHeader) return false;
const timestamp = parseInt(timestampHeader, 10);
const currentTime = Math.floor(Date.now() / 1000);
// Reject replay attacks older than 5 minutes
if (Math.abs(currentTime - timestamp) > 300) {
return false;
}
const sigBaseString = `v0:${timestamp}:${rawBody.toString()}`;
const mySignature = "v0=" + crypto
.createHmac("sha256", slackSigningSecret)
.update(sigBaseString, "utf8")
.digest("hex");
const sigBuf = Buffer.from(mySignature, "utf8");
const headerBuf = Buffer.from(signatureHeader, "utf8");
if (sigBuf.length !== headerBuf.length) {
return false;
}
return crypto.timingSafeEqual(sigBuf, headerBuf);
}Step 2: Slack Interactive Action Handler
When the manager clicks the approval button, Slack dispatches a POST request containing a URL-encoded payload. We immediately return an HTTP 200 response, then trigger Signbee asynchronously:
import express, { Request, Response } from "express";
import { verifySlackRequest } from "./slack_security";
const app = express();
app.use(express.urlencoded({ extended: true }));
const SLACK_SIGNING_SECRET = process.env.SLACK_SIGNING_SECRET!;
const SIGNBEE_API_KEY = process.env.SIGNBEE_API_KEY!;
app.post("/slack/actions", async (req: Request, res: Response) => {
const signature = req.headers["x-slack-signature"] as string;
const timestamp = req.headers["x-slack-request-timestamp"] as string;
// 1. Immediately acknowledge Slack within 3,000ms
res.status(200).send();
// 2. Perform background processing asynchronously
try {
const payload = JSON.parse(req.body.payload);
const { actions, response_url, user, channel } = payload;
const actionValue = actions[0]?.value; // e.g. "deal_982_nda"
// 3. Dispatch document via Signbee REST API
const signbeeRes = await fetch("https://signb.ee/api/v1/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${SIGNBEE_API_KEY}`
},
body: JSON.stringify({
title: "Mutual NDA — Enterprise Vendor",
markdown: `# Mutual Non-Disclosure Agreement\n\nApproved by ${user.name} for deal ${actionValue}...\n\n| Clause | Description |\n| :--- | :--- |\n| Confidentiality | 3 Years Strict |\n| Jurisdiction | State of Delaware |\n`,
recipient_name: "John Doe",
recipient_email: "john.doe@client.com",
webhook_url: "https://api.yourdomain.com/webhooks/signbee"
})
});
const doc = await signbeeRes.json();
// 4. Update the Slack channel thread with signing link
await fetch(response_url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `⚡ *Contract Dispatched:* <${doc.signing_url}|Click here to review & sign>`,
replace_original: false
})
});
} catch (err) {
console.error("Slack interaction error:", err);
}
});Step 3: Microsoft Teams Adaptive Card JSON Schema
For organizations running Microsoft 365, you can render an identical interactive experience using Microsoft Teams Adaptive Cards v1.5:
{
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5",
"body": [
{
"type": "TextBlock",
"text": "📄 Contract Approval Required",
"weight": "Bolder",
"size": "Large"
},
{
"type": "FactSet",
"facts": [
{ "title": "Client:", "value": "Acme Global Corp" },
{ "title": "Agreement:", "value": "Master Services Agreement" },
{ "title": "Contract Value:", "value": "$125,000 / year" }
]
}
],
"actions": [
{
"type": "Action.Execute",
"title": "✅ Approve & Send Contract",
"verb": "approve_contract",
"data": {
"deal_id": "deal_982_msa",
"recipient_email": "procurement@acme.com"
}
}
]
}Step 4: Signbee Webhook Callback to Update Chat Threads
When the recipient executes their signature, Signbee emits a document.signed webhook. We capture this event and push an immediate celebratory update with the verifiable cryptographic certificate:
import express, { Request, Response } from "express";
import crypto from "crypto";
const app = express();
const WEBHOOK_SECRET = process.env.SIGNBEE_WEBHOOK_SECRET!;
app.post(
"/webhooks/signbee",
express.raw({ type: "application/json" }),
async (req: Request, res: Response) => {
const signature = req.headers["x-signbee-signature"] as string;
// 1. Verify Signbee HMAC
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(req.body.toString("utf8"));
if (payload.event === "document.signed") {
// 2. Look up original Slack response_url from internal database
const channelWebhookUrl = await getSlackResponseUrl(payload.document_id);
if (channelWebhookUrl) {
await fetch(channelWebhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `🎉 *Agreement Executed!*\nDocument: *${payload.title}*\nRecipient: ${payload.recipient_name}\nVerified PDF: <${payload.signed_pdf_url}|Download PDF>\nSHA-256 Seal: `${payload.signature_hash.slice(0, 16)}...``
})
});
}
}
res.status(200).json({ received: true });
}
);Enterprise Chat Security Hardening: Role Guards & Ephemeral Previews
Allowing employees to trigger legal document dispatches from chat applications introduces security considerations that must be addressed before rolling out to production workspaces:
1. Interactive User Authorization Check
When the "Approve & Send for Signing" button is clicked, Slack sends the clicking user's ID (payload.user.id). Never blindly trust channel presence. Verify that the user belongs to an authorized managers group in your directory (e.g., Okta SCIM or Azure AD) before executing the Signbee API request. If unauthorized, use Slack's response_url with response_type: "ephemeral" to notify them privately without cluttering the public channel.
2. Ephemeral Document Previews
Contract terms often contain sensitive financial or compensation details. Rather than uploading raw unencrypted PDF attachments into channel file repositories where they persist indefinitely across workspace backups, provide time-expiring signing links that require authenticated counterparty verification.
3. Chat Audit Trail Reconciliation
When creating the document via Signbee's REST API, embed the Slack approver's email and timestamp inside the metadata object:metadata: { approved_by: user.email, slack_channel: channel.id, triggered_at: new Date().toISOString() }. This permanently binds internal ChatOps approval records directly to the cryptographic Certificate of Completion.
Frequently Asked Questions
How do you securely handle authentication between Slack, Microsoft Teams, and Signbee?
Authentication operates across a dual-perimeter zero-trust boundary. Inbound requests from Slack are cryptographically validated by hashing the request timestamp and raw body using your Slack Signing Secret and comparing it against the X-Slack-Signature header with crypto.timingSafeEqual. Inbound Microsoft Teams requests are validated via Azure Bot Framework JWT tokens or HMAC shared secrets. Once the chat request is verified, your backend securely authenticates with Signbee using a server-side environment variable (SIGNBEE_API_KEY). The API key is never exposed to client-side chat manifests, Block Kit JSON payloads, or public webhooks.
What are the response time requirements for chat interactive action webhooks?
Both Slack and Microsoft Teams enforce strict synchronous acknowledgment deadlines. Slack requires your backend to return an HTTP 200 OK within 3,000 milliseconds (3 seconds), while Microsoft Teams mandates an acknowledgment within 2,000 milliseconds. If your backend fails to respond within this strict window, the user interface displays persistent operational warning banners (such as 'Dispatch failed'). To ensure compliance, your endpoint must acknowledge the request immediately with an empty 200 OK, spin off the contract generation and Signbee API call asynchronously, and subsequently update the original chat thread using the provided response_url.
Can we pass dynamic markdown variables into Signbee from Slack modal inputs?
Yes. Signbee generates legally binding contracts directly from standard markdown text payloads without requiring pre-configured GUI templates. When an employee or manager completes an interactive Slack modal (inputting deal value, payment terms, contractor names, and project deliverables), your backend maps those modal view state values directly into a Markdown template string. The resulting markdown string is dispatched to POST /api/v1/send, instantly producing a tailored agreement with vector typography and embedded audit trails.
How does the chat channel get updated when the recipient finishes signing?
When the recipient finishes signing the document, Signbee dispatches an outbound webhook containing the document.signed event. Your backend receives this webhook, verifies the X-Signbee-Signature HMAC header using your webhook_secret, and looks up the stored Slack response_url or Microsoft Teams conversation reference ID stored during the initial dispatch. Your backend then POSTs an updated Block Kit or Adaptive Card message to the chat thread, announcing that the agreement has been executed and displaying the permanent SHA-256 verification seal.
Start Automating ChatOps Contract Signing
Get started with Signbee's single-endpoint REST API. 5 free documents per month, 100 requests/minute, and instant markdown rendering.