Skip to main content

Overview

The Payments API provides secure payment processing through Stripe with automatic marketplace commission handling. It supports PaymentIntents for modern payment flows, handles platform fees, and integrates seamlessly with the order creation system. Location: convex/customers/payments.ts

Payment Flow

1

Create Payment Intent

Generate Stripe PaymentIntent with cart totals and commission validation
2

Process Payment

Handle payment on frontend using Stripe Elements or Payment Sheet
3

Webhook Processing

Automatic order creation via Stripe webhooks on successful payment
4

Order Fulfillment

Store receives order and begins fulfillment process

Create Payment Intent

Creates a Stripe PaymentIntent for cart checkout with platform commission validation.
customerId
Id<'customers'>
required
Customer ID making the purchase
storeId
Id<'stores'>
required
Store ID receiving the order
subtotal
number
required
Order subtotal before fees
deliveryFee
number
required
Delivery fee amount
platformFee
number
required
Platform commission fee
totalAmount
number
required
Total order amount (subtotal + delivery + platform fee)
currency
string
Currency code (default: “usd”)
cartItems
Array<object>
required
Cart items for payment metadata
deliveryType
'delivery' | 'pickup'
required
Delivery method
deliveryAddress
object
Delivery address details (required for delivery orders)
const paymentSetup = await convex.action(api.customers.payments.createPaymentIntent, {
  customerId: "c123456789",
  storeId: "j123456789",
  subtotal: 45.97,
  deliveryFee: 5.00,
  platformFee: 3.20,
  totalAmount: 54.17,
  currency: "usd",
  cartItems: [
    { 
      productId: "p123456789", 
      productName: "Pizza Margherita", 
      quantity: 2, 
      price: 22.99 
    }
  ],
  deliveryType: "delivery",
  deliveryAddress: {
    fullAddress: "123 Main St, Apt 4B",
    city: "ct123456789",
    area: "ar123456789"
  }
});
{
  "clientSecret": "pi_1234567890_secret_abcdefghijklmnop",
  "paymentIntentId": "pi_1234567890",
  "totalAmount": 54.17
}

Get Payment Intent Status

Retrieve the current status of a Stripe PaymentIntent.
paymentIntentId
string
required
Stripe PaymentIntent ID to check
const status = await convex.action(api.customers.payments.getPaymentIntentStatus, {
  paymentIntentId: "pi_123456789"
});
{
  "status": "succeeded",
  "amount": 54.17,
  "currency": "usd",
  "metadata": {
    "customerId": "c123456789",
    "storeId": "j123456789",
    "deliveryType": "delivery"
  }
}

Cancel Payment Intent

Cancel a Stripe PaymentIntent if user cancels checkout.
paymentIntentId
string
required
Stripe PaymentIntent ID to cancel
const result = await convex.action(api.customers.payments.cancelPaymentIntent, {
  paymentIntentId: "pi_123456789"
});
{
  "status": "canceled"
}

Complete Payment Integration

Here’s a complete payment integration with Stripe Elements:
import React, { useState, useEffect } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import {
  Elements,
  CardElement,
  useStripe,
  useElements
} from '@stripe/react-stripe-js';
import { useAction } from 'convex/react';
import { api } from './convex/_generated/api';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);

interface PaymentFormProps {
  customerId: string;
  storeId: string;
  subtotal: number;
  deliveryFee: number;
  platformFee: number;
  totalAmount: number;
  cartItems: any[];
  deliveryType: 'delivery' | 'pickup';
  deliveryAddress?: any;
  onSuccess: (orderId: string) => void;
  onError: (error: string) => void;
}

function PaymentForm({
  customerId,
  storeId,
  subtotal,
  deliveryFee,
  platformFee,
  totalAmount,
  cartItems,
  deliveryType,
  deliveryAddress,
  onSuccess,
  onError
}: PaymentFormProps) {
  const stripe = useStripe();
  const elements = useElements();
  const [processing, setProcessing] = useState(false);
  const [clientSecret, setClientSecret] = useState<string | null>(null);

  const createPaymentIntent = useAction(api.customers.payments.createPaymentIntent);
  const createOrder = useMutation(api.customers.cartToOrder.createOrderFromCart);

  // Create PaymentIntent when component mounts
  useEffect(() => {
    const setupPayment = async () => {
      try {
        const paymentSetup = await createPaymentIntent({
          customerId,
          storeId,
          subtotal,
          deliveryFee,
          platformFee,
          totalAmount,
          currency: "usd",
          cartItems,
          deliveryType,
          deliveryAddress
        });

        setClientSecret(paymentSetup.clientSecret);
      } catch (error) {
        console.error('Payment setup failed:', error);
        onError('Failed to initialize payment');
      }
    };

    setupPayment();
  }, []);

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();

    if (!stripe || !elements || !clientSecret) {
      return;
    }

    setProcessing(true);

    const card = elements.getElement(CardElement);
    if (!card) {
      setProcessing(false);
      return;
    }

    try {
      // Confirm payment with Stripe
      const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
        payment_method: {
          card: card,
          billing_details: {
            name: 'Customer Name', // You can get this from user profile
          },
        }
      });

      if (error) {
        console.error('Payment failed:', error);
        onError(error.message || 'Payment failed');
      } else if (paymentIntent.status === 'succeeded') {
        // Payment successful - create order
        const order = await createOrder({
          customerId,
          storeId,
          deliveryAddress: deliveryAddress || {
            fullAddress: "Pickup from store",
            city: "" as any,
            area: "" as any,
            phoneNumber: "N/A"
          },
          deliveryType,
          deliveryFee,
          paymentMethod: "card",
          paymentIntentId: paymentIntent.id,
          paymentStatus: "paid"
        });

        onSuccess(order.orderId);
      }
    } catch (error) {
      console.error('Payment processing error:', error);
      onError('Payment processing failed');
    } finally {
      setProcessing(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      {/* Order Summary */}
      <div className="bg-gray-50 p-4 rounded-lg">
        <h3 className="font-medium text-gray-900 mb-2">Order Summary</h3>
        <div className="space-y-2 text-sm">
          <div className="flex justify-between">
            <span>Subtotal</span>
            <span>${subtotal.toFixed(2)}</span>
          </div>
          <div className="flex justify-between">
            <span>Delivery Fee</span>
            <span>${deliveryFee.toFixed(2)}</span>
          </div>
          <div className="flex justify-between">
            <span>Platform Fee</span>
            <span>${platformFee.toFixed(2)}</span>
          </div>
          <div className="border-t pt-2 flex justify-between font-medium">
            <span>Total</span>
            <span>${totalAmount.toFixed(2)}</span>
          </div>
        </div>
      </div>

      {/* Card Element */}
      <div className="bg-white p-4 border rounded-lg">
        <label className="block text-sm font-medium text-gray-700 mb-2">
          Card Information
        </label>
        <CardElement
          options={{
            style: {
              base: {
                fontSize: '16px',
                color: '#424770',
                '::placeholder': {
                  color: '#aab7c4',
                },
              },
            },
          }}
        />
      </div>

      {/* Submit Button */}
      <button
        type="submit"
        disabled={!stripe || processing}
        className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
      >
        {processing ? (
          <>
            <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
              <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
              <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
            </svg>
            Processing Payment...
          </>
        ) : (
          `Pay $${totalAmount.toFixed(2)}`
        )}
      </button>
    </form>
  );
}

// Main component with Stripe provider
function CheckoutPage({ orderData }: { orderData: any }) {
  return (
    <Elements stripe={stripePromise}>
      <PaymentForm
        {...orderData}
        onSuccess={(orderId) => {
          console.log('Order created:', orderId);
          // Navigate to success page
        }}
        onError={(error) => {
          console.error('Payment error:', error);
          // Show error message
        }}
      />
    </Elements>
  );
}

export { PaymentForm, CheckoutPage };

Payment Status Monitoring

Monitor payment status for order processing:
import { useState, useEffect } from 'react';
import { useAction } from 'convex/react';
import { api } from './convex/_generated/api';

function usePaymentStatus(paymentIntentId: string | null) {
  const [status, setStatus] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  
  const getPaymentStatus = useAction(api.customers.payments.getPaymentIntentStatus);

  useEffect(() => {
    if (!paymentIntentId) return;

    const checkStatus = async () => {
      setLoading(true);
      try {
        const result = await getPaymentStatus({ paymentIntentId });
        setStatus(result.status);
      } catch (error) {
        console.error('Failed to check payment status:', error);
      } finally {
        setLoading(false);
      }
    };

    // Check immediately
    checkStatus();

    // Poll every 2 seconds for status updates
    const interval = setInterval(checkStatus, 2000);

    // Cleanup
    return () => clearInterval(interval);
  }, [paymentIntentId]);

  return { status, loading };
}

// Usage in component
function OrderStatus({ paymentIntentId }: { paymentIntentId: string }) {
  const { status, loading } = usePaymentStatus(paymentIntentId);

  const getStatusDisplay = () => {
    switch (status) {
      case 'requires_payment_method':
        return { text: 'Waiting for payment', color: 'yellow' };
      case 'requires_confirmation':
        return { text: 'Confirming payment', color: 'blue' };
      case 'processing':
        return { text: 'Processing payment', color: 'blue' };
      case 'succeeded':
        return { text: 'Payment successful', color: 'green' };
      case 'canceled':
        return { text: 'Payment canceled', color: 'gray' };
      case 'requires_action':
        return { text: 'Action required', color: 'orange' };
      default:
        return { text: 'Unknown status', color: 'gray' };
    }
  };

  const statusDisplay = getStatusDisplay();

  return (
    <div className="flex items-center space-x-2">
      <div className={`w-3 h-3 rounded-full ${
        statusDisplay.color === 'green' ? 'bg-green-500' :
        statusDisplay.color === 'blue' ? 'bg-blue-500' :
        statusDisplay.color === 'yellow' ? 'bg-yellow-500' :
        statusDisplay.color === 'orange' ? 'bg-orange-500' :
        'bg-gray-500'
      } ${loading ? 'animate-pulse' : ''}`}></div>
      <span className="text-sm font-medium">{statusDisplay.text}</span>
    </div>
  );
}

Stripe Webhook Integration

The payment system automatically processes orders via webhooks:
// Webhook endpoint: POST /stripe/webhook
// Handles these events:

// 1. payment_intent.succeeded
//    → Creates order from cart
//    → Clears customer cart
//    → Credits store wallet
//    → Sends confirmation notifications

// 2. payment_intent.payment_failed
//    → Logs failure
//    → Sends failure notification
//    → Preserves cart for retry

// 3. payment_intent.canceled
//    → Cleans up resources
//    → Preserves cart
//    → Logs cancellation

Payment Methods

Credit Cards

Supported: Visa, Mastercard, American Express
Processing: Instant via Stripe
Fees: Standard Stripe rates

Debit Cards

Supported: Local and international debit cards
Processing: Instant verification
Fees: Lower processing rates

Digital Wallets

Supported: Apple Pay, Google Pay
Processing: One-touch payments
Fees: Same as card rates

Commission Handling

The payment system automatically handles marketplace commissions:

Error Handling

Status Code: 400
{
  "error": "Failed to create payment intent",
  "details": "Invalid amount or currency"
}
Status Code: 402
{
  "error": "Payment declined",
  "code": "card_declined",
  "decline_code": "insufficient_funds"
}
Status Code: 400
{
  "error": "Invalid payment method",
  "supported_methods": ["card", "cash", "wallet"]
}
Status Code: 400
{
  "error": "Commission validation failed",
  "expected": 3.20,
  "provided": 2.80
}

Security Features

PCI Compliance

All card data is handled by Stripe, ensuring PCI DSS compliance

Fraud Detection

Stripe’s machine learning fraud detection protects against fraudulent transactions

3D Secure

Automatic 3D Secure authentication for enhanced security

Webhook Verification

All webhooks are verified using Stripe signatures
The payment system automatically validates platform commission amounts to ensure consistency between frontend calculations and backend processing.
Always handle payment failures gracefully and provide clear error messages to users. Use Stripe’s test cards for development and testing.