Stage 04 of 05
Payment
Bot
The moment a meeting is booked, the Payment Bot generates a Stripe checkout link and emails it to the lead. When they pay, the tenant activates automatically — zero manual steps.
The Pipeline
9 steps from booked meeting to active account
The Payment Bot (B4) is a BullMQ worker on bot-payment. It handles two job types: payment.request (creates checkout) and stripe.checkout.session.completed (activates tenant).
Trigger: payment.request
The Scheduling Bot enqueues this job the moment a lead confirms a meeting slot (status = BOOKED). The Payment Bot picks it from the bot-payment BullMQ queue — only payment.request jobs are processed here.
Fetch lead from DB
Read the full Lead record from Postgres — email, name, company, BANT score. All context was already captured by Intake and scored by Qualification. No re-extraction needed.
Validate Stripe configuration
Check STRIPE_SECRET_KEY and STRIPE_PRICE_ID are set. If either is missing, the bot enters stub mode — it logs the checkout URL to console and continues. No crash, no blocked pipeline.
Create Stripe Checkout Session
Call stripe.checkout.sessions.create() with the lead's email, the configured price ID, and metadata carrying leadId + tenantId. The session expires in 24 hours. Stripe returns a hosted checkout URL.
Record pending PaymentTransaction
Write a PaymentTransaction row to Postgres with status=PENDING and the Stripe session ID. The upsert on stripeSessionId prevents duplicate records if the bot job retries.
Update lead status → PENDING
Atomically update Lead.status = PENDING (awaiting payment). Audit log entry written. The lead is now in payment limbo — the pipeline pauses here until the Stripe webhook fires.
Send payment email
SMTP email to the lead with a branded "Complete Payment" button linking to the Stripe-hosted checkout page. No login required — clicking the link opens Stripe directly.
Stripe fires checkout.session.completed
After the lead pays, Stripe sends a POST webhook to /api/triggers/stripe-webhook. The NestJS endpoint validates the signature (STRIPE_WEBHOOK_SECRET) and enqueues a stripe.checkout.session.completed job.
Activate tenant + mark lead paid
The Payment Bot handles the webhook job: updates Tenant.subscriptionStatus = ACTIVE, upserts the PaymentTransaction to status = SUCCESS, sets Lead.stripePaid = true. The account is now live.
Stripe Integration
3 integration points — create, verify, activate
The Payment Bot uses the official Stripe SDK v15. All calls use the STRIPE_SECRET_KEY from env. The webhook endpoint uses constructEvent() for cryptographic signature verification.
POST /v1/checkout/sessions
Creates a hosted payment page. Sends customer_email, line_items with the configured price_id, success_url, cancel_url, and expires_at = now + 86400s. Metadata carries leadId + tenantId for webhook routing.
POST /api/triggers/stripe-webhook
Stripe calls this endpoint when payment completes. NestJS validates the stripe-signature header using STRIPE_WEBHOOK_SECRET and constructEvent(). Passes the raw body buffer — not the parsed JSON.
BullMQ → bot-payment
The webhook enqueues stripe.checkout.session.completed to bot-payment. The worker updates the Tenant and PaymentTransaction records and marks the lead as paid. Idempotent via upsert on stripeSessionId.
// Stripe session creation — bots/payment/src/index.ts
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'payment',
customer_email: lead.email,
line_items: [{ price: process.env.STRIPE_PRICE_ID, quantity: 1 }],
success_url: `${STRIPE_SUCCESS_URL}?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: STRIPE_CANCEL_URL,
expires_at: Math.floor(Date.now() / 1000) + 86400, // 24h
metadata: {
leadId: lead.id,
tenantId: lead.tenantId,
},
})
// session.url → emailed to leadThe Payment Email
One click to Stripe checkout
The lead receives a branded HTML email with a single "Complete Payment" button. The link goes directly to the Stripe-hosted checkout page — no login, no redirect chain. The link expires in 24 hours.
SMTP credentials are already configured (SMTP_HOST, SMTP_USER, SMTP_PASS). In stub mode (no credentials) the checkout URL is printed to the bot console.
<div style="font-family: sans-serif; max-width: 600px; margin: auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px;">
<h2 style="color: #2563eb;">Hi {leadName},</h2>
<p>Your discovery call is confirmed.</p>
<p>To activate your Motus AI account, complete your payment using the secure link below:</p>
<!-- Stripe Checkout Button -->
<div style="margin: 28px 0; text-align: center;">
<a href="{checkoutUrl}"
style="display: inline-block; padding: 14px 32px; background-color: #2563eb;
color: white; text-decoration: none; border-radius: 6px;
font-weight: bold; font-size: 16px;">
Complete Payment
</a>
</div>
<p style="color: #666; font-size: 0.85em;">
This link expires in 24 hours. Hosted securely by Stripe.
</p>
<p style="color: #666; font-size: 0.85em;">— The Motus Labs Team</p>
</div>The Webhook Flow
POST /api/triggers/stripe-webhook
NestJS handles the Stripe webhook. It validates the signature using the raw body buffer (preserved via rawBody: true in NestFactory.create()), then enqueues the session object to BullMQ for async processing.
// apps/api/src/modules/triggers/triggers.controller.ts
// POST /api/triggers/stripe-webhook (NestJS)
1. Read stripe-signature header
2. If STRIPE_WEBHOOK_SECRET is set:
rawBody = req.rawBody // preserved via NestFactory({ rawBody: true })
event = stripe.webhooks.constructEvent(rawBody, sig, secret)
3. If event.type === 'checkout.session.completed':
queues.enqueue('bot-payment', 'stripe.checkout.session.completed', {
type: 'stripe.checkout.session.completed',
data: session // full Stripe.Checkout.Session object
})
4. Payment Bot picks up job:
tenant.update({ subscriptionStatus: 'ACTIVE' })
paymentTransaction.upsert({ status: 'SUCCESS' })
lead.update({ stripePaid: true })Happy path
Stripe fires the webhook → NestJS validates → BullMQ job queued → Payment Bot activates the tenant and marks the lead paid. All within ~500ms of payment.
Security
Without STRIPE_WEBHOOK_SECRET, the endpoint returns 400 in production. Only NODE_ENV=development skips verification — never in prod.
DB Schema Changes
Fields added in Stage 04
| field | type | values / purpose | added in |
|---|---|---|---|
| stripePaid | Boolean | true after webhook confirms payment | Stage 04 |
| status | LeadStatus | PENDING = awaiting payment | Stage 01 |
| stripeSessionId | String @unique | PaymentTransaction — Stripe session ID | Stage 04 |
| subscriptionStatus | String | UNPAID → ACTIVE on Tenant model | Stage 04 |
| stripeSubscriptionId | String? | Stripe subscription ID if recurring | Stage 04 |
Lead status state machine
Environment Variables
8 vars required for Stage 04
| variable | required | description |
|---|---|---|
| STRIPE_SECRET_KEY | yes | sk_live_... or sk_test_... — from Stripe Dashboard → API Keys |
| STRIPE_PRICE_ID | yes | price_... — the product price ID from Stripe Dashboard → Products |
| STRIPE_WEBHOOK_SECRET | yes | whsec_... — from Stripe Dashboard → Developers → Webhooks |
| STRIPE_SUCCESS_URL | yes | Redirect URL after successful payment (e.g. https://app.domain.com/payments/success) |
| STRIPE_CANCEL_URL | yes | Redirect URL if lead cancels checkout (e.g. https://app.domain.com/payments/cancel) |
| SMTP_HOST | yes | SMTP server for sending the payment link email |
| SMTP_USER | yes | SMTP authentication username |
| SMTP_PASS | yes | SMTP authentication password |
STRIPE_WEBHOOK_SECRET is pending from the client. Until it arrives, the bot runs in stub mode and still sends payment emails. All other Stripe functionality (checkout session creation) is fully operational.
Escalation Matrix
3 failure modes, all handled
Trigger: STRIPE_SECRET_KEY missing or Stripe API returns 5xx
Action: Bot enters stub mode — generates a placeholder checkout URL using STRIPE_SUCCESS_URL. Lead status still updated to PENDING and email still sent. BullMQ retries up to 4 times with exponential backoff.
Trigger: STRIPE_PRICE_ID not set in environment
Action: Stub checkout URL generated and emailed. Warning logged. Pipeline continues — no crash. Admin must set STRIPE_PRICE_ID and re-trigger the job to issue a real Stripe checkout link.
Trigger: Stripe webhook received without STRIPE_WEBHOOK_SECRET in production
Action: Request rejected with 400. In development (NODE_ENV=development) the check is bypassed with a loud warning. Production never accepts unverified webhooks.
What Happens Next
Payment complete → Support Bot activates
Once a tenant activates, the Support Bot (B5) takes over — creating a Zendesk ticket for the new account, handling onboarding queries, and escalating issues to the sales team.
Test the pipeline
Submit a lead — see the full journey
Submit a HOT lead. The pipeline runs: Intake → Qualification → Scheduling → Payment. Within seconds of confirming a meeting slot you will receive a payment link email.