Stripe Integration AI Prompt

Copy/paste this prompt into your AI assistant (e.g., GitHub Copilot Chat, Cursor, ChatGPT) to integrate Stripe payments with ExisOne automatic license key generation.

Prompt

You're assisting me in integrating Stripe payments with ExisOne for automatic software license key generation.

GOAL
- Integrate Stripe Checkout on my website
- When customers complete payment, Stripe sends a webhook to ExisOne
- ExisOne automatically generates a license key and emails it to the customer
- Support both one-time payments and subscription-based licensing

CONTEXT
- ExisOne webhook URL: https://www.exisone.com/api/stripe/webhook
- I have already configured my Stripe app in ExisOne with Publishable key, Secret key, and Webhook signing secret
- I have an ExisOne Product ID (found on the Products page, e.g., "42")
- The product has "Auto Email" enabled in ExisOne to send license keys automatically

HOW IT WORKS
1. My website creates a Stripe Checkout session with productId in metadata
2. Customer completes payment on Stripe Checkout
3. Stripe sends webhook to ExisOne (checkout.session.completed or invoice.paid)
4. ExisOne verifies the webhook signature matches my stored signing secret
5. ExisOne reads productId from session/subscription metadata
6. ExisOne generates license key and emails it to customer

REQUIREMENTS FOR ONE-TIME PAYMENTS

1) Server-Side: Create Checkout Session (Node.js)
   const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');

   app.post('/create-checkout-session', async (req, res) => {
       const session = await stripe.checkout.sessions.create({
           mode: 'payment',
           line_items: [{
               price_data: {
                   currency: 'usd',
                   unit_amount: 2999,  // $29.99 in cents
                   product_data: {
                       name: 'Pro License',
                   },
               },
               quantity: 1,
           }],
           metadata: {
               productId: '42',    // ExisOne Product ID - REQUIRED
               quantity: '1'       // Number of licenses to generate
           },
           customer_email: req.body.email,  // Optional: pre-fill email
           success_url: 'https://yoursite.com/success',
           cancel_url: 'https://yoursite.com/cancel',
       });

       res.json({ url: session.url });
   });

2) Client-Side: Redirect to Checkout (JavaScript)
   <button onclick="buyLicense()">Buy Pro License - $29.99</button>

   <script>
   async function buyLicense() {
       const response = await fetch('/create-checkout-session', {
           method: 'POST',
           headers: { 'Content-Type': 'application/json' },
           body: JSON.stringify({ email: customerEmail })
       });

       const { url } = await response.json();
       window.location.href = url;  // Redirect to Stripe Checkout
   }
   </script>

REQUIREMENTS FOR SUBSCRIPTIONS (recurring billing)

For subscription-based licensing, ExisOne issues a license key on initial checkout
and EXTENDS THE SAME KEY on every renewal invoice. The customer keeps one
activation key for the lifetime of the subscription; only the ExpirationDate is
advanced. New expiration = max(now, existingExpiration) + plan/product days.

Each license issued under a subscription is tagged with the Stripe
subscription.id on the initial checkout.session.completed event. Renewal
invoice.paid events look up the license by that ID and extend it.

Legacy subscriptions whose licenses were issued before the StripeSubscriptionId
column existed fall back to the old behavior (mint a new key per renewal).

IMPORTANT: Use subscription_data.metadata (not just metadata) for renewals to work.

1) Server-Side: Create Subscription Checkout (Node.js)
   const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');

   app.post('/create-subscription-checkout', async (req, res) => {
       const session = await stripe.checkout.sessions.create({
           mode: 'subscription',
           line_items: [{
               price: 'price_YOUR_RECURRING_PRICE_ID',  // Created in Stripe Dashboard
               quantity: 1,
           }],
           // IMPORTANT: Use subscription_data.metadata for renewals to work
           subscription_data: {
               metadata: {
                   productId: '42',    // ExisOne Product ID - REQUIRED
                   quantity: '1'       // Number of licenses per renewal
               }
           },
           customer_email: req.body.email,
           success_url: 'https://yoursite.com/success',
           cancel_url: 'https://yoursite.com/cancel',
       });

       res.json({ url: session.url });
   });

METADATA PLACEMENT

One-Time Payments:
| Field     | Location                    | Required | Description                              |
|-----------|-----------------------------|----------|------------------------------------------|
| productId | metadata.productId          | Yes      | ExisOne Product ID                       |
| quantity  | metadata.quantity           | No       | Number of licenses (default: 1)          |
| planId    | metadata.planId             | No       | ExisOne Plan ID to override duration and/or devices per key |

Subscriptions:
| Field     | Location                           | Required | Description                             |
|-----------|-----------------------------------|----------|-----------------------------------------|
| productId | subscription_data.metadata.productId | Yes   | Must be on subscription for renewals    |
| quantity  | subscription_data.metadata.quantity  | No    | Licenses per billing cycle              |
| planId    | subscription_data.metadata.planId    | No    | ExisOne Plan ID to override duration and/or devices per key |

USING PLAN ID FOR DIFFERENT LICENSE DURATIONS AND ACTIVATION TIERS

If you sell the same product with different license durations (e.g., yearly subscription vs perpetual)
or different activation counts (e.g., a 5-device standard tier and a 25-device enterprise tier),
you can use planId to specify which plan's settings to use:

1) Create plans in ExisOne (e.g., "Yearly" with 365 days, "Perpetual" with 0 days,
   "Enterprise Yearly" with 365 days and Max Devices = 25)
2) Include planId in metadata alongside productId:

   // Example: Yearly subscription
   metadata: {
       productId: '42',
       planId: '5'      // Uses plan's DurationDays (e.g., 365)
   }

   // Example: Perpetual license (different checkout session)
   metadata: {
       productId: '42',
       planId: '6'      // Uses plan's DurationDays (e.g., 0 for perpetual)
   }

   // Example: Enterprise tier - ONE key with 25 activations on the SAME product
   metadata: {
       productId: '42',
       planId: '7'      // Plan with DurationDays=365 and Max Devices=25
   }                    // Do NOT set quantity - one key carries all 25 activations

If planId is not provided, the product's DefaultLicenseDays and Default Max Devices are used.

NOTE ON ACTIVATION TIERS (plan Max Devices):
- The plan's "Max devices per key" overrides the product's Default Max Devices for
  licenses generated from that purchase. Blank = product default. This keeps the
  product (and product name) identical across tiers, so shipped client software
  that activates by product name keeps working for every tier.
- quantity multiplies KEYS (separate licenses); plan Max Devices raises the number
  of activations on EACH key. For a single 25-activation key use planId with
  Max Devices = 25 and leave quantity at 1.
- On subscription renewals the existing key keeps its original device count; the
  plan's Max Devices only applies when a key is first generated.

STRIPE WEBHOOK EVENTS
- checkout.session.completed - Primary event for completed payments
- payment_intent.succeeded - Backup for one-time payments
- invoice.paid - For subscription renewals

PYTHON EXAMPLE (Flask)
   import stripe
   from flask import Flask, request, jsonify

   stripe.api_key = 'sk_test_YOUR_SECRET_KEY'

   @app.route('/create-checkout-session', methods=['POST'])
   def create_checkout_session():
       session = stripe.checkout.Session.create(
           mode='payment',
           line_items=[{
               'price_data': {
                   'currency': 'usd',
                   'unit_amount': 2999,
                   'product_data': {'name': 'Pro License'},
               },
               'quantity': 1,
           }],
           metadata={
               'productId': '42',
               'quantity': '1'
           },
           success_url='https://yoursite.com/success',
           cancel_url='https://yoursite.com/cancel',
       )
       return jsonify({'url': session.url})

EMAIL TEMPLATE PLACEHOLDERS
- {{ActivationKey}} - The generated license key
- {{ApiKey}} - The generated API key (for API license products)
- {{ProductName}} - Your product name
- {{PlanName}} - The plan name (e.g., "Yearly Subscription", "Perpetual License") - requires planId in metadata
- {{CustomerEmail}} - Customer's email address
- {{PaymentProcessorCustomerName}} - Customer name from Stripe
- {{CustomerPortalUrl}} - Link to customer self-service portal

TESTING
1. Use test mode keys (sk_test_..., pk_test_...)
2. Use Stripe test card: 4242 4242 4242 4242 (any future expiry, any CVC)
3. Check ExisOne > Stripe Apps and Events to verify webhook received with "verified" status
4. Verify license appears in ExisOne > License Keys
5. Confirm customer received email with license key

COMMON ISSUES

Subscription renewals not extending the license:
- The license row must have StripeSubscriptionId populated (set on the initial
  checkout.session.completed). Licenses issued before this column existed
  fall back to minting a new key per renewal. Backfill if needed:
  UPDATE EO_Licenses SET StripeSubscriptionId = 'sub_xxx' WHERE ActivationKey = 'KEY-...';
- Ensure productId is in subscription_data.metadata, not just metadata
- The subscription object must have the metadata, not just the checkout session
- Verify webhook is listening for invoice.paid events

Signature verification fails:
- Ensure webhook signing secret (whsec_...) matches the endpoint targeting ExisOne
- Test vs Live secrets must match your mode
- Re-enter the secret in ExisOne, avoid leading/trailing spaces

GOING LIVE
1. Create new webhook endpoint in Stripe Dashboard (live mode)
2. Point to: https://www.exisone.com/api/stripe/webhook
3. Copy the live webhook signing secret
4. Add new Stripe app in ExisOne with live keys (check "Is Live")
5. Update website to use live secret key
6. Make a real test purchase (refund afterward)

DELIVERABLES
- Server-side code to create Stripe Checkout sessions with productId in metadata
- For subscriptions, use subscription_data.metadata
- Client-side redirect to Stripe Checkout
- Success/cancel page handling
- Ensure productId is always included in the appropriate metadata location

Please produce idiomatic code for my stack (Node.js/Express/Python/Flask/Django/PHP/etc. as applicable), with proper error handling and no extraneous commentary.

Notes