May 3, 2026 · Automation Guide

Automate Retainer Agreements: Send Recurring Contracts via API

Agencies, law firms, and consultants spend hours every month on retainer paperwork. Here's how to automate the entire flow — generate, send, sign, archive — with one API call on a cron job or Stripe Checkout success webhook.

Michael Beckett
Michael Beckett

Founder, Signbee

TL;DR

Put retainer agreements on autopilot. Build a touchless contract pipeline by linking Stripe Checkout success webhooks with the Signbee e-signature API. When client payments succeed, dynamically compile a Markdown contract with payment metadata, dispatch it for electronic signature, and update client records upon completion. Fully secure, compliant with eIDAS and the ESIGN Act, and 100% automated.

The problem with manual retainer operations

For service businesses — including marketing agencies, law firms, CPAs, and IT consultants — retainer agreements are the financial lifelines that guarantee monthly recurring revenue (MRR). However, executing these agreements is often a manual, high-friction process. Every month or quarter, account managers spend hours copying and pasting client names, billing addresses, customized service scopes, monthly hours, and fee structures into document templates.

This manual workflow is not only tedious; it is prone to human error and delays onboarding. Contracts get sent late, client names get misspelled, and services are frequently delivered before agreements are signed. Worse, tracking who has signed, following up on outstanding contracts, and manually archiving completed files adds an unnecessary layer of administrative overhead.

By automating the contract flow, you eliminate these bottlenecks. Using a REST API to send documents instantly when a customer pays or signs up guarantees that no service begins without a legally binding agreement in place.

Who needs automated retainers?

IndustryRetainer typeTypical frequency
Marketing agenciesMonthly service retainerMonthly
Law firmsLegal retainer / engagementQuarterly / Annual
IT consultantsManaged services agreementMonthly
Design studiosCreative retainerMonthly
Bookkeepers / CPAsAccounting services retainerAnnual

The integration pattern: Stripe Checkout and Signbee

The most robust way to automate retainers is to couple your billing system directly with your document signing pipeline. In this guide, we use Stripe Checkout to handle the initial recurring subscription or fixed fee payment, and Signbee to instantly generate and dispatch the retainer agreement for signature.

The sequence is straightforward. The customer initiates a checkout flow on your web portal. During Checkout Session creation, your backend embeds details about the contract (like hours, scope of work, and billing rates) within Stripe's metadata dictionary. When the customer completes the payment, Stripe fires a webhook to your server. Your application intercepts this event, parses the metadata, compiles a dynamic Markdown contract, and POSTs it directly to the Signbee API, which immediately emails the client a signing link.

Client to Webhook Sequence Diagram

The following sequence diagram outlines the flow of information across the client, Stripe, your webhook receiver, and the Signbee API:

+--------+           +----------------+         +-----------------+         +-------------+
| Client |           | Stripe Gateway |         | Webhook Handler |         | Signbee API |
+--------+           +----------------+         +-----------------+         +-------------+
    |                        |                           |                         |
    | 1. Completes Checkout  |                           |                         |
    |----------------------->|                           |                         |
    |    (metadata payload)  |                           |                         |
    |                        | 2. Webhook Event:         |                         |
    |                        |    checkout.session.comp  |                         |
    |                        |-------------------------->|                         |
    |                        |                           | 3. Verify Stripe event  |
    |                        |                           |    & extract metadata   |
    |                        |                           |                         |
    |                        |                           | 4. Compile dynamic      |
    |                        |                           |    Markdown agreement   |
    |                        |                           |                         |
    |                        |                           | 5. POST /api/v1/send    |
    |                        |                           |------------------------>|
    |                        |                           |    (Dispatch contract)  |
    |                        |                           |                         |
    |                        |                           | 6. Return response      |
    |                        |                           |<------------------------|
    |                        |                           |    (document_id, status)|
    |                        |                           |                         |
    | 7. Receives email with |                           |                         |
    |    Signbee signing link|                           |                         |
    |<-----------------------------------------------------------------------------|
    |                        |                           |                         |

Structuring Stripe metadata

Stripe allows you to store custom key-value pairs (up to 50 keys, with values up to 500 characters) on sessions, customers, and subscriptions. This metadata is key to our stateless webhook architecture. Instead of querying a database to find out what service package the client bought, your webhook handler receives everything it needs directly from the Stripe event payload.

For a typical retainer agreement, you should record the following fields in the metadata during session creation:

Metadata KeyPurposeExample Value
agency_nameThe service provider nameApex Digital Ltd
client_companyThe official entity of the clientAcme Corp
monthly_feeThe retainer amount billed recurringly3500
monthly_hoursAllocated service hours per month25
hourly_rateBilling rate for any overage hours150
scope_of_workComma-separated list of deliverablesSEO audits, Link building, Blog writing

Stripe webhooks implementation

Below, we provide complete, production-ready webhook handlers in both Node.js (Next.js App Router style) and Python (FastAPI style). These handlers perform signature verification, parse the metadata, compile a dynamic retainer document in Markdown, and dispatch it to the client via Signbee.

Node.js — Next.js App Router Webhook Endpoint (route.ts)
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2023-10-16" as any,
});

export async function POST(req: Request) {
  const body = await req.text();
  const sig = req.headers.get("stripe-signature");

  if (!sig) {
    return new Response("Missing stripe-signature header", { status: 400 });
  }

  let event: Stripe.Event;

  try {
    // 1. Verify Stripe webhook signature
    event = stripe.webhooks.constructEvent(
      body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err: any) {
    console.error(`Webhook signature verification failed: ${err.message}`);
    return new Response(`Webhook Error: ${err.message}`, { status: 400 });
  }

  // 2. Process checkout.session.completed event
  if (event.type === "checkout.session.completed") {
    const session = event.data.object as Stripe.Checkout.Session;
    
    const clientEmail = session.customer_details?.email;
    const clientName = session.customer_details?.name;
    const metadata = session.metadata;

    if (!clientEmail || !metadata) {
      return new Response("Missing client information or metadata", { status: 400 });
    }

    // 3. Extract metadata fields
    const agencyName = metadata.agency_name || "Signbee Client Agency";
    const clientCompany = metadata.client_company || "Client Company";
    const monthlyFee = metadata.monthly_fee || "0";
    const hours = metadata.monthly_hours || "0";
    const rate = metadata.hourly_rate || "0";
    const scope = metadata.scope_of_work || "General Consulting Services";

    const currentMonth = new Date().toLocaleString("default", { month: "long" });
    const currentYear = new Date().getFullYear();

    // 4. Generate dynamic Markdown contract
    const contractMarkdown = `# Monthly Retainer Agreement

**Provider:** ${agencyName}
**Client:** ${clientCompany} (${clientName})
**Effective Date:** ${currentMonth} 1, ${currentYear}
**Retainer Fee:** $${monthlyFee}/month
**Allocated Hours:** ${hours} hours/month
**Hourly Rate (Overage):** $${rate}/hour

## Scope of Services
During the retainer period, Provider will deliver:
${scope.split(",").map(item => "- " + item.trim()).join("\n")}

## Terms and Availability
This retainer includes up to ${hours} hours of service per month. Unused hours do not roll over to subsequent months. Additional hours will be billed at $${rate}/hour, subject to mutual agreement.

## Payment Terms
The retainer fee of $${monthlyFee} is payable automatically on a monthly recurring basis. Payments are handled via Stripe Billing. Late payments may result in suspension of services.

## Confidentiality & IP
Both parties agree to maintain the confidentiality of all proprietary materials. Intellectual property generated during the retainer period is transferred to the Client upon receipt of full payment.

By signing below, both parties agree to the terms of this Retainer Agreement.`;

    // 5. Dispatch contract to Signbee API
    try {
      const signbeeResponse = await fetch("https://signb.ee/api/v1/send", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Authorization": `Bearer ${process.env.SIGNBEE_API_KEY}`,
        },
        body: JSON.stringify({
          markdown: contractMarkdown,
          recipient_name: clientName,
          recipient_email: clientEmail,
          title: `${clientCompany} Retainer Agreement - ${currentMonth} ${currentYear}`,
          metadata: {
            stripe_checkout_id: session.id,
            client_company: clientCompany,
          },
        }),
      });

      if (!signbeeResponse.ok) {
        if (signbeeResponse.status === 429) {
          const retryAfter = signbeeResponse.headers.get("Retry-After") || "60";
          console.warn(`Signbee API rate-limited (429). Retry after ${retryAfter}s.`);
        }
        const errorText = await signbeeResponse.text();
        throw new Error(`Signbee API Error (${signbeeResponse.status}): ${errorText}`);
      }

      const result = await signbeeResponse.json();
      console.log(`Successfully dispatched retainer contract. Document ID: ${result.id}`);
    } catch (error) {
      console.error("Failed to send retainer agreement via Signbee:", error);
      return new Response("Signbee transmission failed", { status: 500 });
    }
  }

  return new Response(JSON.stringify({ received: true }), { status: 200 });
}
Python — FastAPI Webhook Endpoint (main.py)
import os
import stripe
import requests
from datetime import datetime
from fastapi import FastAPI, Request, Header, HTTPException

app = FastAPI()

stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
endpoint_secret = os.getenv("STRIPE_WEBHOOK_SECRET")
signbee_api_key = os.getenv("SIGNBEE_API_KEY")

@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request, stripe_signature: str = Header(None)):
    payload = await request.body()

    try:
        # 1. Verify Stripe webhook signature
        event = stripe.Webhook.construct_event(
            payload, stripe_signature, endpoint_secret
        )
    except ValueError as e:
        raise HTTPException(status_code=400, detail="Invalid payload")
    except stripe.error.SignatureVerificationError as e:
        raise HTTPException(status_code=400, detail="Invalid signature")

    # 2. Process checkout.session.completed event
    if event["type"] == "checkout.session.completed":
        session = event["data"]["object"]
        
        client_email = session.get("customer_details", {}).get("email")
        client_name = session.get("customer_details", {}).get("name")
        metadata = session.get("metadata", {})

        if not client_email or not metadata:
            raise HTTPException(status_code=400, detail="Missing client details or metadata")

        # 3. Extract metadata fields
        agency_name = metadata.get("agency_name", "Signbee Client Agency")
        client_company = metadata.get("client_company", "Client Company")
        monthly_fee = metadata.get("monthly_fee", "0")
        hours = metadata.get("monthly_hours", "0")
        rate = metadata.get("hourly_rate", "0")
        scope_raw = metadata.get("scope_of_work", "General Consulting Services")

        current_month = datetime.now().strftime("%B")
        current_year = datetime.now().year

        # 4. Generate dynamic Markdown contract
        scope_list = [item.strip() for item in scope_raw.split(",")]
        scope_markdown = "\n".join([f"- {item}" for item in scope_list])

        contract_markdown = f"""# Monthly Retainer Agreement

**Provider:** {agency_name}
**Client:** {client_company} ({client_name})
**Effective Date:** {current_month} 1, {current_year}
**Retainer Fee:** ${monthly_fee}/month
**Allocated Hours:** {hours} hours/month
**Hourly Rate (Overage):** ${rate}/hour

## Scope of Services
During the retainer period, Provider will deliver:
{scope_markdown}

## Terms and Availability
This retainer includes up to {hours} hours of service per month. Unused hours do not roll over to subsequent months. Additional hours will be billed at ${rate}/hour, subject to mutual agreement.

## Payment Terms
The retainer fee of ${monthly_fee} is payable automatically on a monthly recurring basis. Payments are handled via Stripe Billing. Late payments may result in suspension of services.

## Confidentiality & IP
Both parties agree to maintain the confidentiality of all proprietary materials. Intellectual property generated during the retainer period is transferred to the Client upon receipt of full payment.

By signing below, both parties agree to the terms of this Retainer Agreement."""

        # 5. Dispatch contract to Signbee API
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {signbee_api_key}"
        }
        payload_data = {
            "markdown": contract_markdown,
            "recipient_name": client_name,
            "recipient_email": client_email,
            "title": f"{client_company} Retainer Agreement - {current_month} {current_year}",
            "metadata": {
                "stripe_checkout_id": session.get("id"),
                "client_company": client_company
            }
        }

        try:
            response = requests.post(
                "https://signb.ee/api/v1/send",
                json=payload_data,
                headers=headers,
                timeout=10
            )

            if response.status_code != 200:
                if response.status_code == 429:
                    retry_after = response.headers.get("Retry-After", "60")
                    print(f"Signbee API rate-limited (429). Retry after {retry_after}s.")
                response.raise_for_status()

            result = response.json()
            print(f"Successfully dispatched retainer contract. Document ID: {result.get('id')}")

        except requests.exceptions.RequestException as e:
            print(f"Failed to transmit to Signbee: {e}")
            raise HTTPException(status_code=500, detail="Signbee connection failed")

    return {"received": True}
}

Closing the loop: Signbee webhook notifications

Once Signbee dispatches the e-signature request, the document remains in a sent state. To fully automate your onboarding process, you should implement another simple endpoint that receives webhook events from Signbee. When a document is finalized, Signbee will send a document.completed webhook payload containing the download URL for the signed PDF, along with a tamper-proof SHA-256 digital signature certificate.

Your Signbee webhook handler should execute the following operations:

  • Identify client: Retrieve the custom metadata you attached during the initial send request (such as stripe_checkout_id) to look up the customer in your database.
  • Archive document: Download the signed PDF and upload it to a secure, permanent storage bucket (e.g., AWS S3 or Google Cloud Storage) with object-locking active to guarantee compliance.
  • Activate subscription status: Update the client record state from pending_contract to active in your application database, and trigger user provisioning logic (such as creating their shared Slack workspace or JIRA board).

Resiliency, retries, and API rate limits

Production systems must be built to handle failures gracefully. Stripe webhooks will retry automatically if your server returns a non-200 code, but Signbee requests must be managed carefully. If your system triggers a high volume of contract dispatches at once, you may hit rate limits. Signbee paid tiers allow up to 1,000 requests per minute, which is significantly higher than competitor limits. However, in our rate limit guide, we discuss implementing a client-side queue (such as BullMQ or Celery) that listens to 429 Too Many Requests responses, parses the Retry-After header, and schedules a retry backoff.

Frequently Asked Questions

How do you legally tie a Stripe subscription or charge to a signed retainer agreement?

To legally bind a Stripe transaction to an e-signed contract, you should establish a clear, bi-directional reference trail between the Stripe Checkout Session (or Subscription) and the Signbee signing package. When initiating the Stripe Checkout session, include custom metadata keys such as contract_template_id, client_email, and client_name. Once the payment succeeds, retrieve the checkout session metadata, dynamically generate the agreement incorporating these unique payment and transaction identifiers, and dispatch the e-signature request via Signbee. Once signed, Signbee's webhook will return a cryptographic SHA-256 certificate along with the finalized PDF. You can then write this document URL back into the Stripe customer's metadata or subscription notes, creating an unbroken digital audit trail that links the specific e-signed consent directly to the financial transaction. This audit trail is fully compliant with the ESIGN Act and eIDAS, proving that the client paid and agreed to the specific terms simultaneously.

What is the best way to handle webhook failures or delayed signatures in a Stripe-Signbee integration?

Handling network failures, timeouts, and delayed e-signatures requires building a state-aware database and designing idempotent handlers. When a Stripe webhook fires, your endpoint should quickly validate the signature, write the event to a database table acting as an inbox, and respond with a 200 OK to prevent Stripe from retrying the event. An asynchronous worker or queue process (such as BullMQ, Celery, or standard cron-based pollers) should then pick up the webhook event, mark it as "processing," and make the call to the Signbee API. In case Signbee is temporarily unavailable or returns a 429 Too Many Requests error, your worker must respect the Retry-After header and schedule a retry with exponential backoff. If the client delays signing, your database state should transition from "payment_completed" to "signature_pending". You can configure automatic reminders within Signbee or set up a cron job that checks for unsigned contracts older than 48 hours, sending friendly automated email prompts to secure the signature without manual intervention.

How does Signbee verify the integrity of automated retainer agreements, and how do we store the certificates?

Signbee secures every signed document by generating a cryptographic PDF signing certificate. During the signing process, Signbee captures multiple metadata points, including the signer's IP address, timestamp, email authentication details, and user-agent string. When the document is finalized, Signbee hashes the entire document bundle using the SHA-256 algorithm and embeds a cryptographic seal. This seal guarantees that the document cannot be tampered with after signing; even a single character change in the PDF would invalidate the hash. When the webhook notifies your server that the document is signed, it provides a webhook payload containing the download URL for the signed PDF and the certificate details. Your backend should automatically download the signed PDF, compute or verify the SHA-256 checksum, and archive the file to a secure, immutable storage bucket such as AWS S3 with Object Lock enabled or Supabase Storage. Storing these certificates alongside your transaction records ensures compliance with SOC2, GDPR, and legal standards.

Never drop a document — $0.50/agreement, SHA-256 certified.

Last updated: May 29, 2026 · Michael Beckett is the founder of Signbee and B2bee Ltd.

Related resources