ChatOps & AutomationUpdated September 4, 2026 · 11 min read

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.

Michael Beckett
Michael Beckett

Founder, Signbee

Executive Overview

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 MetricLegacy Email WorkflowChatOps (Slack / Teams)
Average Time to Approval24 – 72 hours< 14 minutes
Context SwitchingLeaves chat → Opens portal → Logs inZero (One-click in-thread execution)
Audit VisibilitySiloed in individual employee inboxesPublic in deal / project channel
Integration SurfaceHeavy proprietary enterprise SDKsLightweight 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 FeatureSlack (Block Kit v2)Microsoft Teams (Adaptive Cards v1.5)
Payload StructureJSON Block Array (header, section, actions)Adaptive Card Schema (Container, ColumnSet)
Synchronous ACK Deadline3,000 milliseconds2,000 milliseconds
Asynchronous Update MechanismPOST to ephemeral response_urlPATCH to conversation activity ID
Action Handler Typebutton / static_select block_actionsAction.Execute / Action.Submit
Authentication VerificationHMAC-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:

  1. Channel Trigger: An account executive or bot posts an approval card in Slack or Teams.
  2. Interactive Dispatch: The approving manager clicks "Approve & Generate Agreement".
  3. Instant Acknowledgment: The backend validates the cryptographic signature and acknowledges within 2 seconds.
  4. Signbee API Generation: The backend asynchronously calls POST /api/v1/send with contract Markdown and recipient details.
  5. 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:

slack_security.ts — Timing-Safe HMAC Validator
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:

server.ts — Express Interactive Action Endpoint
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:

teams_card.json — Adaptive Card Schema
{
  "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:

webhook_handler.ts — Push Signed Status to Chat
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.

Related resources