Skip to main content

POST /api/stripe

Create a Stripe payment intent for processing subscription payments. This endpoint validates the plan and pricing, then returns a client secret for completing the payment on the client side.
This endpoint creates a payment intent but does not charge the customer. Use the returned clientSecret with Stripe.js or mobile SDKs to collect payment details and confirm the payment.

Rate Limiting

  • Limit: 10 requests per IP
  • Window: 15 minutes
  • Purpose: Prevent payment intent spam

Security Features

  • Server-side price validation (prevents client-side tampering)
  • Plan and duration validation
  • Input sanitization
  • Stripe API integration with automatic payment methods

Request Body

planId
string
required
Plan identifier (max 50 characters)Must be one of: plan-1 (Basic), plan-2 (Standard), plan-3 (Premium)Example: "plan-2"
months
number
required
Subscription duration in months (1-12 range)Must be one of: 1, 2, 3, 6, or 12Example: 6
amount
number
required
Payment amount in USD (1-10000 range)Must exactly match the plan’s price for the selected duration. The server validates this to prevent price manipulation (tolerance: 0.01).Example: 70

Response

clientSecret
string
Stripe payment intent client secretUse this with Stripe.js or mobile SDKs to complete the payment. Format: pi_xxxxx_secret_xxxxx

Example Request

curl -X POST https://your-domain.com/api/stripe \
  -H "Content-Type: application/json" \
  -d '{
    "planId": "plan-2",
    "months": 6,
    "amount": 70
  }'

Example Response

200 OK
{
  "clientSecret": "pi_3AbCdEfGhIjKlMnO_secret_1234567890abcdef"
}

Error Responses

Stripe Payment Intent Details

The payment intent created by this endpoint has the following configuration:
amount
number
Amount in cents (USD)Calculated as: Math.round(amount * 100)Example: $70.00 becomes 7000 cents
currency
string
Currency codeAlways set to: "usd"
automatic_payment_methods
object
Payment method configurationSet to: { enabled: true } to support cards, wallets, etc.
metadata
object
Custom metadata attached to the payment intentContains:
  • planId (string): The selected plan ID
  • months (string): Subscription duration
Example: { planId: "plan-2", months: "6" }

Validation Rules

FieldValidation
planIdRequired, max 50 chars, must exist in PLANS
monthsRequired, must be 1, 2, 3, 6, or 12
amountRequired, 1-10000 range, must match plan price

Price Validation

The endpoint validates that the amount matches the expected price:
// Price validation logic (from route.ts:42-44)
const expectedPrice = plan.prices.find((p) => p.months === months)?.price;
if (expectedPrice === undefined || Math.abs(amount - expectedPrice) > 0.01) {
  return NextResponse.json({ error: "Monto inválido." }, { status: 400 });
}
This prevents malicious clients from manipulating prices before payment.

Implementation Details

The endpoint is implemented in /src/app/api/stripe/route.ts:11 and uses:
  • Stripe SDK: stripe package
  • Environment: STRIPE_SECRET_KEY required
  • Security: Rate limiting, input sanitization, price validation
  • Payment Methods: Automatic payment methods enabled (cards, wallets)

Complete Payment Flow

  1. Client: Call /api/stripe with plan details
  2. Server: Validate plan, months, and amount
  3. Server: Create Stripe payment intent
  4. Server: Return clientSecret
  5. Client: Use clientSecret with Stripe.js to collect payment
  6. Client: Confirm payment with Stripe
  7. Stripe: Process payment and return payment intent ID
  8. Client: Call /api/orders with payment intent ID as paymentReceiptId
  9. Server: Create order and customer records
  10. Client: Show success confirmation

Example: Full Integration

import { loadStripe } from '@stripe/stripe-js';
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';

const stripePromise = loadStripe('pk_test_...');

function CheckoutForm({ planId, months, amount }) {
  const stripe = useStripe();
  const elements = useElements();
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (event) => {
    event.preventDefault();
    setLoading(true);

    // Step 1: Create payment intent
    const response = await fetch('/api/stripe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ planId, months, amount }),
    });
    const { clientSecret } = await response.json();

    // Step 2: Confirm payment
    const { error, paymentIntent } = await stripe.confirmCardPayment(
      clientSecret,
      {
        payment_method: {
          card: elements.getElement(CardElement),
          billing_details: { name: 'Customer Name' },
        },
      }
    );

    if (error) {
      console.error(error.message);
      setLoading(false);
      return;
    }

    // Step 3: Create order
    await fetch('/api/orders', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        name: 'Customer Name',
        email: '[email protected]',
        phone: '+1234567890',
        planId,
        devices: 2,
        months,
        amount,
        paymentMethod: 'stripe',
        paymentReceiptId: paymentIntent.id,
      }),
    });

    setLoading(false);
    alert('Subscription created successfully!');
  };

  return (
    <form onSubmit={handleSubmit}>
      <CardElement />
      <button type="submit" disabled={!stripe || loading}>
        Pay ${amount}
      </button>
    </form>
  );
}

Testing

When testing, use Stripe test mode:
  • Test card: 4242 4242 4242 4242
  • Any future expiration date
  • Any 3-digit CVC
  • Any ZIP code

Test Scenarios

ScenarioExpected Result
Valid plan and priceReturns clientSecret
Invalid plan ID400 error: “Plan inválido.”
Wrong amount for plan400 error: “Monto inválido.”
Invalid duration400 error: “Duración inválida.”
11th request in 15 min429 error: Rate limited

Environment Variables

Required environment variable:
STRIPE_SECRET_KEY=sk_test_...

See Also

Build docs developers (and LLMs) love