MotusAI
← Stage 03

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.

Stripe CheckoutWebhook signature verification24h link expiryAuto tenant activationESC-PAY-001/002/003SMTP email

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).

01

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.

02

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.

03

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.

04

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.

05

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.

06

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.

07

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.

08

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.

09

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.

Session

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.

Webhook

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.

Activation

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 lead

The 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.

email templateSMTP → lead email
<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

fieldtypevalues / purposeadded in
stripePaidBooleantrue after webhook confirms paymentStage 04
statusLeadStatusPENDING = awaiting paymentStage 01
stripeSessionIdString @uniquePaymentTransaction — Stripe session IDStage 04
subscriptionStatusStringUNPAID → ACTIVE on Tenant modelStage 04
stripeSubscriptionIdString?Stripe subscription ID if recurringStage 04

Lead status state machine

BOOKEDPENDINGpayment.request job picked up — checkout link sent
PENDINGPENDINGLead has not paid yet — webhook not fired
PENDINGACTIVE (tenant)checkout.session.completed — Tenant.subscriptionStatus = ACTIVE

Environment Variables

8 vars required for Stage 04

variablerequireddescription
STRIPE_SECRET_KEYyessk_live_... or sk_test_... — from Stripe Dashboard → API Keys
STRIPE_PRICE_IDyesprice_... — the product price ID from Stripe Dashboard → Products
STRIPE_WEBHOOK_SECRETyeswhsec_... — from Stripe Dashboard → Developers → Webhooks
STRIPE_SUCCESS_URLyesRedirect URL after successful payment (e.g. https://app.domain.com/payments/success)
STRIPE_CANCEL_URLyesRedirect URL if lead cancels checkout (e.g. https://app.domain.com/payments/cancel)
SMTP_HOSTyesSMTP server for sending the payment link email
SMTP_USERyesSMTP authentication username
SMTP_PASSyesSMTP 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

ESC-PAY-001

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.

ESC-PAY-002

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.

ESC-PAY-003

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.

Stage 01Intake Botdone
Stage 02Qualificationdone
Stage 03Schedulingdone
Stage 04Paymentactive
Stage 05Supportnext

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.