Stripe webhook when a contract is signed

SaaS teams often want billing or provisioning to start only after a contract is signed. Stripe handles payments. Atlas handles signatures. Connect them with Atlas lifecycle webhooks, not by mixing Stripe and Atlas into one checkout session for the contract PDF itself.

> Share: "Atlas envelope.signed webhook is the gate for Stripe subscriptions or invoice creation."

Two different Stripe webhooks

Do not confuse these:

SourcePurpose
Stripe → your serverPayment succeeded, subscription active
Atlas → your serverEnvelope signed, PDF ready

This guide covers Atlas → your middleware → Stripe API calls after sign. For buying Atlas send credits via Stripe Checkout, see the credits section below.

Reference flow

POST /api/envelope (MSA PDF)
  → Customer signs in Atlas
  → envelope.signed webhook to your app
  → Verify X-Atlas-Signature
  → stripe.subscriptions.create or invoices.create
  → Update CRM with subscription id

Keep contract signing and payment capture as separate steps unless counsel approves a combined ceremony.

Atlas webhook handler (Node)

import Stripe from 'stripe';
import crypto from 'crypto';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function handleAtlasWebhook(req) {
  const raw = await req.text();
  const sig = req.headers.get('x-atlas-signature') ?? '';
  const expected = 'sha256=' + crypto.createHmac('sha256', process.env.ATLAS_API_KEY).update(raw).digest('hex');
  if (sig !== expected) return new Response('Unauthorized', { status: 401 });

  const event = JSON.parse(raw);
  if (event.event !== 'envelope.signed') return new Response('ok');

  const envelopeId = event.envelope_id;
  const customerEmail = event.parties?.[0]?.email;
  const stripeCustomerId = event.metadata?.stripe_customer_id;

  if (!stripeCustomerId) {
    console.error('Missing stripe_customer_id on envelope metadata');
    return new Response('ok');
  }

  await stripe.subscriptions.create({
    customer: stripeCustomerId,
    items: [{ price: process.env.STRIPE_PRICE_ID }],
    metadata: { atlas_envelope_id: envelopeId },
  });

  return new Response('ok');
}

Pass stripe_customer_id in envelope metadata at create time from your signup flow.

Idempotency

Stripe subscriptions should be idempotent on envelope_id. Store processed envelope IDs in your database before calling Stripe:

INSERT INTO atlas_stripe_bridge (envelope_id, processed_at)
VALUES ($1, now())
ON CONFLICT DO NOTHING RETURNING envelope_id;

Only call Stripe when the insert succeeds.

Reverse flow: payment before sign

Some teams collect payment first, then send the contract:

Stripe checkout.session.completed
  → Create Atlas envelope with order PDF
  → Send signing link to buyer email

Use Stripe metadata org_id to find the right template and prefill. Atlas send still consumes a credit when email goes out.

Buying Atlas envelope credits with Stripe

Atlas dashboard billing uses Stripe Checkout for credit packs (5, 10, 25 envelopes). That flow is separate from your product's Stripe integration:

  1. Org admin clicks Buy in dashboard
  2. POST /api/stripe/checkout returns Stripe Checkout URL
  3. checkout.session.completed webhook adds credits via ledger RPC

You do not need to build this for your customers. It is built into Atlas for send credits.

Testing

  1. Create envelope in Atlas with test signers
  2. Point webhook_url at ngrok or Vercel preview
  3. Sign on mobile to catch real-world latency
  4. Confirm Stripe test mode subscription appears once
  5. Void test envelope if needed (pending void may refund credit if no signer completed)

Compliance notes

  • Store signed PDF from Atlas API in your record system when Stripe billing starts
  • Align MSA effective date field with webhook timestamp
  • Do not log full API keys in Stripe metadata

Atlas credit purchase flow (separate from your Stripe)

Keyword overlap: teams also search "stripe buy envelope credits" when funding Atlas sends. That purchase path is entirely inside Atlas dashboard:

  1. Org admin opens /dashboard/billing
  2. Frontend calls POST /api/stripe/checkout with pack size 5, 10, or 25
  3. Stripe Checkout completes
  4. Atlas checkout.session.completed handler adds credits via ledger RPC

Your product Stripe integration does not need to implement this unless you resell Atlas credits to customers.

Load testing webhooks

Stripe and Atlas both retry failed webhook delivery. Load-test your handler with duplicate events before Black Friday contract spikes. Idempotency tables for both event sources prevent double subscriptions and double CRM updates.

Metadata contract

Standardize metadata keys across Stripe and Atlas handlers:

KeyPurpose
stripe_customer_idLink billing
atlas_envelope_idLink signature
org_idTenant scope

Document schema in OpenAPI or internal wiki so new engineers do not invent parallel keys.

FAQ

Can Stripe Checkout embed signing? Not natively. Sequence sign then pay or pay then sign explicitly.

Does Atlas emit Stripe events? No. Atlas emits envelope lifecycle events only.

402 on send? Atlas credits exhausted. Complete Stripe purchase in dashboard before send.

Platform mode agencies? Each connected account has its own credit balance. Stripe bridge metadata should include Atlas-Account id.

Webhook timeout? Respond 2xx within ten seconds. Queue Stripe calls asynchronously.

API reference

Full route list and request schemas live at /dev. Start with E-signature API for the mental model, then use this guide for copy-paste examples.