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:
| Source | Purpose |
|---|---|
| Stripe → your server | Payment succeeded, subscription active |
| Atlas → your server | Envelope 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:
- Org admin clicks Buy in dashboard
POST /api/stripe/checkoutreturns Stripe Checkout URLcheckout.session.completedwebhook adds credits via ledger RPC
You do not need to build this for your customers. It is built into Atlas for send credits.
Testing
- Create envelope in Atlas with test signers
- Point
webhook_urlat ngrok or Vercel preview - Sign on mobile to catch real-world latency
- Confirm Stripe test mode subscription appears once
- 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:
- Org admin opens /dashboard/billing
- Frontend calls
POST /api/stripe/checkoutwith pack size 5, 10, or 25 - Stripe Checkout completes
- Atlas
checkout.session.completedhandler 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:
| Key | Purpose |
|---|---|
| stripe_customer_id | Link billing |
| atlas_envelope_id | Link signature |
| org_id | Tenant 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.
Related
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.