5 min Quickstart

TL;DR for experienced engineers. Detect Sumo purchases, create users, provision access.

How it works

  1. 1. Sumo creates checkout sessions on your connected Stripe account
  2. 2. You receive standard checkout.session.completed webhooks
  3. 3. Sumo purchases have sumo_campaign_id in metadata — that's your detection flag
  4. 4. Create the user account, link subscription, provision access

Metadata Fields

Field Use
sumo_campaign_id Detection flag — if present, it's a Sumo purchase
sumo_product_tier_id Which tier was purchased — use for access levels
sumo_quantity Number of licenses (default: 1)
sumo_product_id Sumo product ID
sumo_user_id Sumo buyer ID (optional tracking)

Node.js / Express

webhook.js
JavaScript
// Express/Node.js - minimal Sumo webhook handler
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), async (req, res) => {
  const event = stripe.webhooks.constructEvent(
    req.body,
    req.headers['stripe-signature'],
    process.env.STRIPE_WEBHOOK_SECRET
  );

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;

    // Sumo purchase = has sumo_campaign_id in metadata
    if (session.metadata?.sumo_campaign_id) {
      const user = await db.users.upsert({
        where: { email: session.customer_details.email },
        create: {
          email: session.customer_details.email,
          stripeCustomerId: session.customer,
          source: 'sumo'
        }
      });

      await db.subscriptions.create({
        userId: user.id,
        stripeSubscriptionId: session.subscription,
        tierId: session.metadata.sumo_product_tier_id,
        quantity: parseInt(session.metadata.sumo_quantity) || 1
      });

      await provisionAccess(user.id, session.metadata.sumo_product_tier_id);
    }
  }

  res.json({ received: true });
});

Python / FastAPI

webhook.py
Python
# FastAPI - minimal Sumo webhook handler
@app.post("/webhooks/stripe")
async def webhook(request: Request):
    event = stripe.Webhook.construct_event(
        await request.body(),
        request.headers.get("stripe-signature"),
        WEBHOOK_SECRET
    )

    if event["type"] == "checkout.session.completed":
        session = event["data"]["object"]

        # Sumo purchase = has sumo_campaign_id in metadata
        if session.get("metadata", {}).get("sumo_campaign_id"):
            user = await find_or_create_user(
                email=session["customer_details"]["email"],
                stripe_customer_id=session["customer"]
            )
            await create_subscription(
                user_id=user.id,
                stripe_sub_id=session["subscription"],
                tier_id=session["metadata"]["sumo_product_tier_id"],
                quantity=int(session["metadata"].get("sumo_quantity", 1))
            )
            await provision_access(user.id, session["metadata"]["sumo_product_tier_id"])

    return {"status": "ok"}

Test Locally

Terminal
Bash
# Local testing with Stripe CLI
stripe listen --forward-to localhost:8000/webhooks/stripe

# Create test checkout with Sumo metadata
stripe checkout sessions create \
  --success-url="http://localhost:3000/success" \
  --mode=subscription \
  --line-items[0][price]=price_xxx \
  --line-items[0][quantity]=1 \
  --metadata[sumo_campaign_id]=test_camp \
  --metadata[sumo_product_tier_id]=test_tier \
  --metadata[sumo_quantity]=1

Quick Checklist

Key Points

Idempotency is critical

Stripe may retry webhooks. Use upsert/findOrCreate patterns. Add unique constraint on stripe_subscription_id.

User might already exist

Check by email first. If they exist, just link the new subscription. Don't create duplicates.

Return 200 fast

Return success quickly, queue heavy work (emails, complex provisioning) for background processing.