Skip to main content

Hosted Checkout Integration

The Payment Gateway provides a fully-hosted, PCI-compliant checkout page that handles card collection and payment processing. This eliminates your PCI compliance burden and provides a secure, optimized payment experience.
Checkout URL: https://checkout-page-amber.vercel.appBuilt with Next.js 15, React 19, and TypeScript

Why Use Hosted Checkout?

Zero PCI Compliance Burden

Card data never touches your serverNo PCI-DSS compliance requirements for your infrastructure

Enterprise-Grade Security

Client-side validation with Luhn algorithmHTTPS enforcement, input sanitization, automatic expiration

Optimized Conversion

Mobile-first responsive designReal-time validation, clear error messages, smooth animations

Zero Maintenance

Fully managed and updatedWorks across all modern browsers, automatic security patches

Security Features

PCI-DSS Compliance

Card Data Isolation: Card numbers, CVV, and expiry dates never touch your server. All sensitive data is collected directly by the checkout page and sent to the Payment API over encrypted connections.
Card Validation:
  • Luhn algorithm validation (detects typos)
  • Expiry date validation (rejects expired cards)
  • CVV format validation (3-4 digits)
  • Real-time error feedback
Input Sanitization:
  • Special characters blocked
  • Paste prevention on CVV field
  • Numeric-only inputs for card numbers
HTTPS Enforcement:
  • TLS 1.3 encryption for all requests
  • Strict-Transport-Security headers
  • Content-Security-Policy enforcement
Authentication:
  • Client secret validation (not API keys)
  • One-time use secrets per payment intent
  • Automatic expiration after 1 hour
Expiration Handling:
  • Payment intents expire after 1 hour
  • Automatic redirect on expiration
  • Clear expiration messaging
Attempt Limiting:
  • Maximum 5 payment attempts per intent
  • Progressive delay between attempts
  • Automatic intent invalidation after max attempts
No Data Persistence:
  • Card data never logged or stored client-side
  • Tokenization happens server-side immediately
  • Only last 4 digits stored for receipts
Error Handling:
  • Generic error messages (no sensitive data exposed)
  • No card details in error logs
  • Rate limiting on validation failures

How It Works

Payment Flow


Integration Steps

1

Create Payment Intent (Server-Side)

Create a payment intent with your API key:
const response = await fetch('https://paymentgateway.redahaloubi.com/api/v1/payment-intents', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.PAYMENT_GATEWAY_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    amount: 9999,
    currency: 'USD',
    success_url: 'https://yourstore.com/order/success?session_id={CHECKOUT_SESSION_ID}',
    cancel_url: 'https://yourstore.com/order/cancel',
    description: 'Order #12345'
  })
});

const { checkout_url, client_secret, id } = await response.json();
2

Redirect Customer to Checkout

Use the checkout_url from the response:
// Option 1: Immediate redirect
window.location.href = checkout_url;

// Option 2: Button click
<a href={checkout_url} className="btn btn-primary">
  Complete Payment
</a>
Checkout URL format:
https://checkout-page-amber.vercel.app/checkout/{intent_id}?client_secret={secret}
3

Customer Completes Payment

The checkout page:
  • Validates payment intent hasn’t expired
  • Displays order summary (amount, currency)
  • Collects card details with real-time validation
  • Submits payment to API with client_secret
  • Shows success animation or error message
  • Redirects to your success/cancel URL
4

Handle Customer Return

Verify payment on your server:
app.get('/order/success', async (req, res) => {
  const paymentIntentId = req.query.session_id;
  
  // Verify payment status server-side
  const response = await fetch(
    `https://paymentgateway.redahaloubi.com/api/v1/payment-intents/${paymentIntentId}`,
    {
      headers: { 'X-API-Key': process.env.PAYMENT_GATEWAY_API_KEY }
    }
  );
  
  const intent = await response.json();
  
  if (intent.data.status === 'authorized' || intent.data.status === 'captured') {
    // Payment successful - fulfill order
    await fulfillOrder(intent.data.payment_id);
    res.render('success', { order: intent.data });
  } else {
    // Payment incomplete
    res.redirect('/order/cancel');
  }
});

Checkout Page Features

User Experience

Responsive Design

Mobile-first layout optimized for all screen sizesTouch-friendly inputs, large tap targets

Real-Time Validation

Instant feedback on card input errorsClear, actionable error messages

Smart Card Detection

Automatic brand detection (Visa, Mastercard, etc.)Dynamic card logo display

Accessibility

WCAG 2.1 AA compliantScreen reader friendly, keyboard navigation

Payment Summary Display

The checkout page automatically displays:
┌─────────────────────────────────────┐
│   Order #12345 - Premium Plan       │
│                                     │
│   Amount:           $99.99          │
│   Currency:         USD             │
│                                     │
│   [Card Number Input]               │
│   [Cardholder Name]                 │
│   [MM/YY]        [CVV]              │
│                                     │
│   [ Pay $99.99 ]                    │
│                                     │
│   🔒 Secured by Payment Gateway     │
└─────────────────────────────────────┘

Supported Payment Methods

Visa
  • All Visa cards supported
  • Test card: 4242 4242 4242 4242
Mastercard
  • All Mastercard types supported
  • Test card: 5555 5555 5555 4444
Card Requirements:
  • 13-19 digit card number
  • Valid expiry date (MM/YY format)
  • 3-4 digit CVV/CVC code
  • Cardholder name

Error Handling

Payment Intent Errors

Displayed Message:
This payment session has expired
Please return to merchant and try again
Action:
  • Automatic redirect to cancel_url after 5 seconds
  • “Return to Merchant” button shown immediately
Cause: Payment intent created more than 1 hour ago
Displayed Message:
Invalid or expired payment session
Please contact merchant support
Action:
  • Show support contact information
  • Log error for merchant review
Cause:
  • Tampered client_secret parameter
  • Client secret from different payment intent
Displayed Message:
This payment has already been completed
Redirecting to confirmation page...
Action:
  • Redirect to success_url immediately
Cause:
  • Customer refreshed page after successful payment
  • Attempting to pay twice with same intent
Displayed Message:
Too many payment attempts
Please create a new payment session
Action:
  • Redirect to cancel_url
  • Show “Contact Support” button
Cause:
  • 5 failed payment attempts on this intent

Card Validation Errors

Invalid Card Number

Error: “Invalid card number”Triggers:
  • Luhn check fails
  • Less than 13 or more than 19 digits
  • Non-numeric characters

Expired Card

Error: “Card has expired”Triggers:
  • Expiry date in the past
  • Invalid month (>12)

Invalid CVV

Error: “Invalid security code”Triggers:
  • Less than 3 digits
  • Non-numeric characters

Missing Name

Error: “Cardholder name required”Triggers:
  • Empty field
  • Less than 3 characters

Payment Declined Errors

// Example error response structure
{
  "success": false,
  "error": {
    "code": "PAYMENT_DECLINED",
    "message": "Card declined: insufficient funds",
    "remaining_attempts": 4
  }
}
User-Friendly Messages:
Error CodeDisplayed MessageAction
PAYMENT_DECLINED”Your card was declined. Please try a different payment method.”Show retry button
INSUFFICIENT_FUNDS”Insufficient funds. Please use a different card.”Show retry button
CVV_MISMATCH”Security code is incorrect. Please check and try again.”Highlight CVV field
EXPIRED_CARD”This card has expired. Please use a different card.”Highlight expiry field

Customization Options

URL Configuration

success_url
string
required
Where to redirect customer after successful paymentPlaceholder: Use {CHECKOUT_SESSION_ID} to include payment intent IDExample:
https://yourstore.com/order/success?session_id={CHECKOUT_SESSION_ID}&order=12345
Result:
https://yourstore.com/order/success?session_id=pi_abc123&order=12345
cancel_url
string
Where to redirect customer on cancellation or failureExample:
https://yourstore.com/order/cancel
Triggers:
  • Customer clicks “Cancel” button
  • Payment intent expires
  • Maximum attempts reached

Metadata

Pass custom data that appears on receipts and webhooks:
{
  "amount": 9999,
  "currency": "USD",
  "description": "Order #12345 - Premium Plan",
  "metadata": {
    "order_id": "12345",
    "customer_id": "cus_abc123",
    "sku": "PREMIUM_ANNUAL",
    "source": "web"
  }
}
Metadata is:
  • Returned in webhook events
  • Visible in payment dashboard
  • Searchable for reporting
  • Limited to 50 keys, 500 chars per value

Best Practices

Never trust client-side redirects alone.
// ❌ BAD: Trust the redirect
app.get('/success', (req, res) => {
  res.render('order-success');
});

// ✅ GOOD: Verify payment status
app.get('/success', async (req, res) => {
  const intent = await verifyPaymentIntent(req.query.session_id);
  if (intent.status === 'authorized') {
    await fulfillOrder(intent.payment_id);
    res.render('order-success');
  } else {
    res.redirect('/order/cancel');
  }
});
Implement both success and cancel handlers.
// Success handler
app.get('/order/success', handleSuccess);

// Cancel handler (payment failed, expired, or user canceled)
app.get('/order/cancel', handleCancellation);
Cancel reasons:
  • Customer clicked “Cancel” button
  • Payment declined after max attempts
  • Payment intent expired
  • Customer abandoned checkout
Don’t rely solely on redirect for order fulfillment.Redirects can be missed (browser crash, network issue). Use webhooks as the primary fulfillment trigger:
app.post('/webhooks/payment', (req, res) => {
  const event = req.body;
  
  if (event.event === 'payment.authorized') {
    // Fulfill order (primary path)
    await fulfillOrder(event.data.payment_id);
  }
  
  res.status(200).send('OK');
});
Use redirects for user feedback only.
One payment intent = one order.
// ❌ BAD: Reuse intent for multiple orders
const intent = await createPaymentIntent({ amount: 9999 });
// Store intent.id for later...

// ✅ GOOD: New intent per order
app.post('/checkout', async (req, res) => {
  const intent = await createPaymentIntent({
    amount: order.total,
    metadata: { order_id: order.id }
  });
  res.redirect(intent.checkout_url);
});
Help customers recognize charges.
// ❌ BAD: Generic description
description: "Payment"

// ✅ GOOD: Descriptive
description: "Order #12345 - Premium Plan (Annual)"
Appears on:
  • Checkout page header
  • Customer bank statement
  • Payment receipts

Browser Support

Desktop Browsers

Chrome/Edge: 88+Firefox: 85+Safari: 14+

Mobile Browsers

iOS Safari: 14+Chrome Mobile: 88+Samsung Internet: 13+
Features:
  • ES6+ JavaScript
  • CSS Grid & Flexbox
  • Fetch API
  • Web Crypto API (for validation)

FAQ

Currently: No, the checkout page uses a standard design for all merchants.Future: We’re working on customization options:
  • Custom logo upload
  • Brand color selection
  • Custom success messages
Contact sales for enterprise customization.
Short answer: Nothing is charged.Details:
  • Payment intent remains in created status
  • Customer can return to same checkout URL (if not expired)
  • Intent auto-expires after 1 hour
  • No money is held or charged
Currently: No, checkout collects card details only.Workaround: Collect billing address on your order form before redirecting to checkout. Pass it in metadata:
{
  "metadata": {
    "billing_address": "123 Main St, City, State 12345",
    "billing_country": "US"
  }
}
Use test cards (see Test Cards section).All test cards work in production environment without charging real money. Results are simulated based on card number.
Currently: No, each payment requires new card entry.Future: Card-on-file and subscription features are planned for Q2 2026.

Troubleshooting

Causes:
  • Payment intent ID in URL doesn’t exist
  • Client secret doesn’t match intent
  • Intent was created for different merchant
Solution:
  • Verify checkout_url from payment intent response is used exactly
  • Don’t modify URL parameters
  • Check API key belongs to correct merchant
Cause: Payment intent created more than 1 hour ago.Solution:
  • Create new payment intent
  • Reduce time between intent creation and checkout redirect
  • Consider creating intent when customer clicks “Checkout” button, not when cart is created
Causes:
  • Customer clicked browser back button before completing payment
  • Payment was declined but customer was redirected to success_url (shouldn’t happen)
Solution:
  • Always verify payment status server-side (see Best Practices)
  • Check for status: 'authorized' or status: 'captured' before fulfilling
Causes:
  • Using real card number in test mode
  • Incorrect expiry format
  • Test card not in supported list
Solution:
  • Use exact test card numbers (4242 4242 4242 4242 for success)
  • Use any future expiry date (e.g., 12/2027)
  • Use any 3-digit CVV

Next Steps


Questions? Contact support at [email protected]