You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Vanilla JS+Node的Stripe美加用户购买限制方案咨询

How to Restrict Stripe Purchases to US/Canada Customers (Vanilla JS + Node.js)

Hey there! I’ve dealt with exactly this scenario after Stripe deprecated some older georestriction APIs, so let me break down the current, supported approaches for your T-shirt store. The key here is to combine frontend guardrails (for better user experience) with strict backend/webhook validation—since frontend checks can easily be bypassed.

Stripe Checkout handles most of the heavy lifting for you, including enforcing country restrictions directly in its hosted checkout page. This is the simplest and most secure approach if you don’t need a fully custom payment form.

Backend (Node.js)

When creating a Checkout Session, specify the shipping_address_collection.allowed_countries parameter to limit shipping to US and Canada. You can also require billing addresses to double-verify the country:

const stripe = require('stripe')('YOUR_STRIPE_SECRET_KEY');
const express = require('express');
const app = express();

app.post('/create-checkout-session', async (req, res) => {
  try {
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      line_items: [
        {
          price: 'YOUR_TSHIRT_PRICE_ID', // Replace with your actual Price ID from Stripe Dashboard
          quantity: 1,
        },
      ],
      mode: 'payment',
      success_url: 'https://your-domain.com/success?session_id={CHECKOUT_SESSION_ID}',
      cancel_url: 'https://your-domain.com/cancel',
      // Restrict shipping to US/CA only
      shipping_address_collection: {
        allowed_countries: ['US', 'CA'],
      },
      // Optional: Require billing address to cross-verify country
      billing_address_collection: 'required',
    });

    res.json({ url: session.url });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));

Frontend (Vanilla JS)

Add a button to trigger the Checkout flow. Stripe will redirect users to its hosted page, which will block any non-US/CA shipping addresses automatically:

<button id="buy-tshirt-btn">Buy T-Shirt</button>

<script src="https://js.stripe.com/v3/"></script>
<script>
  document.getElementById('buy-tshirt-btn').addEventListener('click', async () => {
    const stripe = Stripe('YOUR_STRIPE_PUBLISHABLE_KEY');
    
    // Fetch the Checkout Session from your backend
    const response = await fetch('/create-checkout-session', { method: 'POST' });
    const session = await response.json();

    // Redirect to Stripe Checkout
    const { error } = await stripe.redirectToCheckout({ sessionId: session.id });
    
    // Handle any errors (e.g., network issues)
    if (error) {
      alert(`Oops: ${error.message}`);
    }
  });
</script>

Option 2: Custom Payment Flow (Payment Intents + Stripe Elements)

If you need a fully custom payment form, use Stripe Elements to collect payment details and validate the country both frontend and backend.

Frontend Validation (Vanilla JS)

Add a country dropdown to your form and validate it before submitting to guide users:

<form id="payment-form">
  <div>
    <label>Country</label>
    <select id="country">
      <option value="US">United States</option>
      <option value="CA">Canada</option>
      <!-- Hide other countries or include them but validate -->
    </select>
  </div>
  <div id="card-element"></div>
  <button type="submit">Buy T-Shirt</button>
</form>

<script src="https://js.stripe.com/v3/"></script>
<script>
  const stripe = Stripe('YOUR_STRIPE_PUBLISHABLE_KEY');
  const elements = stripe.elements();
  const cardElement = elements.create('card');
  cardElement.mount('#card-element');

  const form = document.getElementById('payment-form');
  form.addEventListener('submit', async (e) => {
    e.preventDefault();
    
    const country = document.getElementById('country').value;
    // Frontend check to prevent unnecessary API calls
    if (!['US', 'CA'].includes(country)) {
      alert('Sorry, we only accept orders from the United States and Canada.');
      return;
    }

    // Fetch Payment Intent from backend
    const response = await fetch('/create-payment-intent', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ amount: 2000, country: country }), // $20.00 in cents
    });
    const { clientSecret } = await response.json();

    // Confirm payment with Stripe
    const { error } = await stripe.confirmCardPayment(clientSecret, {
      payment_method: {
        card: cardElement,
        billing_details: {
          country: country,
        },
      },
    });

    if (error) {
      alert(`Payment failed: ${error.message}`);
    } else {
      window.location.href = '/success';
    }
  });
</script>

Backend Validation (Node.js)

Validate the country before creating a Payment Intent, and add metadata for later verification:

app.post('/create-payment-intent', async (req, res) => {
  const { amount, country } = req.body;
  
  // Strict backend validation (critical to block malicious requests)
  if (!['US', 'CA'].includes(country.toUpperCase())) {
    return res.status(403).json({ error: 'We only accept orders from US and Canada.' });
  }

  try {
    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount,
      currency: country === 'US' ? 'usd' : 'cad', // Auto-switch currency for better UX
      metadata: {
        customer_country: country,
      },
    });

    res.json({ clientSecret: paymentIntent.client_secret });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

Critical Final Step: Webhook Validation

Even with frontend and backend checks, malicious users can bypass them. Use Stripe webhooks to verify the country after payment is processed, and refund if needed:

app.post('/stripe-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const webhookSecret = 'YOUR_STRIPE_WEBHOOK_SECRET';
  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'payment_intent.succeeded') {
    const paymentIntent = event.data.object;
    const billingCountry = paymentIntent.billing_details.address?.country?.toUpperCase();
    const shippingCountry = paymentIntent.shipping?.address?.country?.toUpperCase();
    const allowedCountries = ['US', 'CA'];

    // Check if either billing or shipping country is invalid
    if (!allowedCountries.includes(billingCountry) && !allowedCountries.includes(shippingCountry)) {
      // Refund the invalid payment
      await stripe.refunds.create({
        payment_intent: paymentIntent.id,
      });
      // Log or notify your team about the suspicious order
      console.log(`Refunded payment for invalid country: ${billingCountry || shippingCountry}`);
    }
  }

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

Key Notes

  • Never rely solely on frontend checks: Users can modify frontend code or send direct API requests to bypass them. Backend and webhook checks are non-negotiable.
  • Test with Stripe’s test cards: Use test cards like 4242 4242 4242 4242 and set test addresses in US/CA to verify restrictions work.
  • Currency optimization: Auto-switch between USD and CAD based on the user’s country to improve checkout experience.

内容的提问来源于stack exchange,提问作者StephD

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 09:02:38