Skip to content

Payment Providers

FORGE provides a unified payment abstraction for processing online payments, managing checkout sessions, handling refunds, and tracking transaction history. The payment system supports both regional and global payment providers through a shared trait interface.

PaymentProvider Trait

All payment drivers implement the PaymentProvider trait:

rust
#[async_trait]
pub trait PaymentProvider: Send + Sync {
    /// Create a new checkout session and return a payment URL or form data
    async fn create_checkout(
        &self,
        request: CreateCheckoutRequest,
    ) -> Result<CheckoutResponse, PaymentError>;

    /// Get the current status of a payment by its provider reference
    async fn get_status(
        &self,
        payment_id: &str,
    ) -> Result<PaymentStatus, PaymentError>;

    /// Issue a full or partial refund for a completed payment
    async fn refund(
        &self,
        payment_id: &str,
        amount: Option<f64>,
    ) -> Result<RefundResult, PaymentError>;

    /// List transactions with optional filters
    async fn list_transactions(
        &self,
        filters: TransactionFilters,
    ) -> Result<Vec<Transaction>, PaymentError>;
}

Request and Response Types

rust
pub struct CreateCheckoutRequest {
    /// Amount in the smallest currency unit (e.g., cents, halalas)
    pub amount: u64,
    /// ISO 4217 currency code (e.g., "SAR", "USD", "AED")
    pub currency: String,
    /// Human-readable description shown on the payment page
    pub description: String,
    /// Your internal reference for this order
    pub reference_id: String,
    /// URL to redirect the customer after successful payment
    pub success_url: String,
    /// URL to redirect the customer after cancellation
    pub cancel_url: String,
    /// Optional customer metadata
    pub customer: Option<CustomerInfo>,
}

pub struct CustomerInfo {
    pub email: Option<String>,
    pub name: Option<String>,
    pub phone: Option<String>,
}

pub struct CheckoutResponse {
    /// Provider's unique identifier for this checkout session
    pub checkout_id: String,
    /// URL to redirect the customer to for payment
    pub payment_url: String,
    /// Current status of the checkout
    pub status: PaymentStatus,
}

PaymentStatus Enum

rust
pub enum PaymentStatus {
    /// Checkout created, awaiting customer action
    Pending,
    /// Payment is being processed by the provider
    Processing,
    /// Payment completed successfully
    Succeeded,
    /// Payment failed (card declined, insufficient funds, etc.)
    Failed(String),
    /// Payment was cancelled by the customer
    Cancelled,
    /// Payment was refunded (fully or partially)
    Refunded,
    /// Payment expired before completion
    Expired,
}

Refund and Transaction Types

rust
pub struct RefundResult {
    /// Provider's refund identifier
    pub refund_id: String,
    /// Amount refunded (in smallest currency unit)
    pub amount: u64,
    /// Refund status
    pub status: RefundStatus,
}

pub enum RefundStatus {
    Pending,
    Succeeded,
    Failed(String),
}

pub struct TransactionFilters {
    /// Filter by status
    pub status: Option<PaymentStatus>,
    /// Filter by date range (start)
    pub from_date: Option<chrono::NaiveDate>,
    /// Filter by date range (end)
    pub to_date: Option<chrono::NaiveDate>,
    /// Pagination: page number
    pub page: Option<u32>,
    /// Pagination: items per page
    pub per_page: Option<u32>,
}

pub struct Transaction {
    pub id: String,
    pub reference_id: String,
    pub amount: u64,
    pub currency: String,
    pub status: PaymentStatus,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

Customer Fields Configuration

FORGE allows you to configure which customer fields are required during checkout. This is especially useful for MENA region apps where phone numbers are the primary customer identifier instead of email.

SettingTypeDefault (Stripe)Default (MENA)Description
PAYMENT_REQUIRE_EMAILbooleantruefalseRequire customer email
PAYMENT_REQUIRE_NAMEbooleanfalsefalseRequire customer name
PAYMENT_REQUIRE_PHONEbooleanfalsetrueRequire customer phone number

MENA Region Default

When you run forge provider:add payment hyperpay or forge provider:add payment checkout, the defaults are automatically set for MENA apps (phone required, email optional). Stripe defaults to Western patterns (email required).

Example configurations:

bash
# Phone-only (MENA apps) - Default for HyperPay & Checkout.com
PAYMENT_REQUIRE_EMAIL=false
PAYMENT_REQUIRE_NAME=false
PAYMENT_REQUIRE_PHONE=true

# Email-only (Western apps) - Default for Stripe
PAYMENT_REQUIRE_EMAIL=true
PAYMENT_REQUIRE_NAME=false
PAYMENT_REQUIRE_PHONE=false

# Full customer info
PAYMENT_REQUIRE_EMAIL=true
PAYMENT_REQUIRE_NAME=true
PAYMENT_REQUIRE_PHONE=true

Payment Methods Configuration

Each provider supports enabling/disabling specific payment methods:

SettingTypeDefaultDescription
{PROVIDER}_APPLE_PAY_ENABLEDbooleanfalseEnable Apple Pay button
{PROVIDER}_GOOGLE_PAY_ENABLEDbooleanfalseEnable Google Pay button
{PROVIDER}_CARD_PAYMENTS_ENABLEDbooleantrueEnable card payment form
HYPERPAY_MADA_ENABLEDbooleanfalseEnable Mada debit cards (HyperPay)
CHECKOUT_MADA_ENABLEDbooleanfalseEnable Mada debit cards (Checkout.com)
HYPERPAY_SAMSUNG_PAY_ENABLEDbooleanfalseEnable Samsung Pay (HyperPay only)

Replace {PROVIDER} with STRIPE, CHECKOUT, or HYPERPAY.

Provider Support Matrix

Payment MethodStripeCheckout.comHyperPay
Card Payments
Apple Pay
Google Pay✅ (Fast Checkout)
Samsung Pay⚠️ KRW only✅ (Mobile SDK)
Mada

Available Drivers

Stripe

Global payment platform with Apple Pay, Google Pay, and comprehensive card support. The most widely used payment processor for SaaS and e-commerce applications.

Installation:

bash
forge provider:add payment stripe

Configuration:

SettingTypeDescription
STRIPE_PUBLIC_KEYstringStripe publishable key (pk_test_...)
STRIPE_SECRET_KEYencryptedStripe secret key (sk_test_...)
STRIPE_WEBHOOK_SECRETencryptedWebhook signing secret (whsec_...)
STRIPE_MODEstring"test" or "live"
STRIPE_APPLE_PAY_ENABLEDbooleanEnable Apple Pay (true/false)
STRIPE_APPLE_PAY_MERCHANT_IDstringApple Pay Merchant ID
STRIPE_GOOGLE_PAY_ENABLEDbooleanEnable Google Pay (true/false)
STRIPE_CARD_PAYMENTS_ENABLEDbooleanEnable card payments (true/false)

Features:

  • Apple Pay & Google Pay -- Native wallet payment support for mobile and web
  • Global card payments -- Visa, MasterCard, Amex, and 135+ currencies
  • 3D Secure -- Built-in SCA/PSD2 compliance with automatic authentication
  • Payment Intents -- Modern payment flow with automatic confirmation handling
  • Refunds -- Full and partial refunds with automatic balance adjustments
  • Webhooks -- Real-time event notifications with signature verification

TIP

Stripe is recommended for Western applications. Defaults to email-based customer identification.

Apple Pay Domain Verification

To enable Apple Pay with Stripe, you must register your domain:

  1. Go to Stripe Dashboard → Settings → Payment Methods → Apple Pay
  2. Add your domain and download the verification file
  3. Host the file at https://yourdomain.com/.well-known/apple-developer-merchantid-domain-association
  4. Click "Verify" in the Stripe dashboard

Alternatively, use the API:

bash
curl https://api.stripe.com/v1/apple_pay/domains \
  -u sk_live_xxx: \
  -d domain_name="yourdomain.com"
Stripe payment flow
1. Your server calls create_checkout() with payment details
2. Stripe creates a PaymentIntent and returns client_secret
3. Frontend uses Stripe.js to render payment form or Apple Pay button
4. Customer completes payment (card entry or Apple Pay authentication)
5. Stripe redirects to success_url with payment_intent parameter
6. Your server verifies payment status via get_status()
7. Stripe sends webhook for asynchronous confirmation

HyperPay

Payment gateway widely used in the MENA (Middle East and North Africa) region. Supports local payment methods including Mada debit cards, STC Pay, and Apple Pay alongside international card networks.

Installation:

bash
forge provider:add payment hyperpay

Configuration:

SettingTypeDescription
HYPERPAY_ENTITY_IDstringHyperPay Entity ID for your checkout
HYPERPAY_ACCESS_TOKENencryptedHyperPay Access Token
HYPERPAY_MODEstring"test" or "live"
HYPERPAY_APPLE_PAY_ENABLEDbooleanEnable Apple Pay (true/false)
HYPERPAY_GOOGLE_PAY_ENABLEDbooleanEnable Google Pay via Fast Checkout (true/false)
HYPERPAY_MADA_ENABLEDbooleanEnable Mada debit cards (true/false)
HYPERPAY_MADA_ENTITY_IDstringSeparate Entity ID for Mada transactions
HYPERPAY_CARD_PAYMENTS_ENABLEDbooleanEnable card payments (true/false)
HYPERPAY_SAMSUNG_PAY_ENABLEDbooleanEnable Samsung Pay (true/false)

Features:

  • Card payments -- Visa, MasterCard, and Mada (Saudi debit network)
  • Apple Pay -- Native Apple Pay integration for iOS and Safari users
  • Google Pay -- Google Pay via HyperPay's Fast Checkout widget
  • Samsung Pay -- Samsung Pay via OPPWA mobile SDK (Android only)
  • Mada Support -- Saudi Arabia's local debit card network
  • Tokenization -- Store card tokens for returning customers without handling raw card data
  • Refunds -- Full and partial refunds through the API
  • Webhooks -- Receive asynchronous payment status notifications

MENA Default

HyperPay defaults to phone-based customer identification (PAYMENT_REQUIRE_PHONE=true). This matches the common pattern in Saudi Arabia and Gulf countries where mobile numbers are the primary identifier.

iOS 18+ Apple Pay Enhancement

Starting with iOS 18, Apple Pay works from all browsers (Chrome, Firefox, Edge) on desktop — not just Safari. When a customer clicks Apple Pay on a non-Safari browser, they scan a QR code with their iPhone/iPad to complete the payment. HyperPay's COPYandPAY widget automatically supports this feature.

Samsung Pay Setup

Samsung Pay is only available through HyperPay and requires the OPPWA mobile SDK for Android apps.

Why only HyperPay?

  • Stripe: Samsung Pay only supports KRW (Korean Won) currency
  • Checkout.com: Does not offer Samsung Pay integration

Mobile SDK Integration (Android):

kotlin
// 1. Add Samsung Pay brand to checkout
val paymentBrands = hashSetOf("SAMSUNGPAY")
val checkoutSettings = CheckoutSettings(checkoutId, paymentBrands, Connect.ProviderMode.TEST)

// 2. Submit payment with Samsung Pay credential
val paymentParams = SamsungPayPaymentParams(checkoutId, paymentCredential)
paymentParams.shopperResultUrl = "yourapp://payment/result"

val paymentProvider = OppPaymentProvider(context, providerMode)
paymentProvider.submitTransaction(Transaction(paymentParams), transactionListener)

The samsung_pay_enabled field in the /payments/config response tells your mobile app whether to show the Samsung Pay button.

HyperPay payment flow
1. Your server calls create_checkout() with payment details
2. HyperPay returns a checkout_id
3. You render the HyperPay payment form on your frontend
4. Customer enters card details on the secure HyperPay form
5. HyperPay processes the payment and redirects to success_url
6. Your server calls get_status(checkout_id) to confirm payment
7. HyperPay sends a webhook notification (backup confirmation)

Checkout.com

Global payment platform supporting 150+ currencies and a wide range of payment methods. Suitable for international businesses and high-volume processing, with strong presence in MENA region.

Installation:

bash
forge provider:add payment checkout

Configuration:

SettingTypeDescription
CHECKOUT_PUBLIC_KEYstringCheckout.com Public Key (for frontend)
CHECKOUT_SECRET_KEYencryptedCheckout.com Secret Key
CHECKOUT_MODEstring"sandbox" or "production"
CHECKOUT_APPLE_PAY_ENABLEDbooleanEnable Apple Pay (true/false)
CHECKOUT_APPLE_PAY_MERCHANT_IDstringApple Pay Merchant ID
CHECKOUT_GOOGLE_PAY_ENABLEDbooleanEnable Google Pay (true/false)
CHECKOUT_MADA_ENABLEDbooleanEnable Mada debit cards (true/false)
CHECKOUT_CARD_PAYMENTS_ENABLEDbooleanEnable card payments (true/false)

Features:

  • Apple Pay & Google Pay -- Native wallet payment support via Payment Request API
  • Global card payments -- Visa, MasterCard, Amex, Discover, and regional card networks
  • Mada Support -- Saudi Arabia's local debit card network (requires BIN configuration in Checkout.com dashboard)
  • 3D Secure -- Built-in 3DS2 authentication for regulatory compliance (PSD2, SCA)
  • Tokenization -- Secure card storage for one-click payments and subscriptions
  • Recurring payments -- Set up subscription billing with automatic card charges
  • Refunds -- Full and partial refunds with webhook status updates
  • Webhooks -- Comprehensive event notifications for all payment lifecycle events

MENA Default

Checkout.com defaults to phone-based customer identification (PAYMENT_REQUIRE_PHONE=true) for MENA region apps. This matches the common pattern where mobile numbers are the primary customer identifier.

Apple Pay Setup

To enable Apple Pay with Checkout.com:

  1. Register your domain with Apple Pay through Checkout.com dashboard
  2. Set CHECKOUT_APPLE_PAY_MERCHANT_ID environment variable
  3. The payment form will automatically show Apple Pay on supported devices

Mada Setup

To enable Mada payments with Checkout.com:

  1. Contact Checkout.com to enable Mada BIN configuration in your dashboard
  2. Set CHECKOUT_MADA_ENABLED=true in your environment
  3. Mada cards are automatically detected by BIN and routed appropriately

Important: Mada payments require full capture only (no partial captures or authorization-only requests). Refunds are handled by the acquirer.

WARNING

Always verify payment status server-side using get_status() after the customer returns to your success_url. Never trust client-side redirects alone as confirmation of payment.

Checkout.com payment flow
1. Your server calls create_checkout() with payment details
2. Checkout.com returns a payment URL (hosted payment page)
3. Customer is redirected to the Checkout.com payment page
4. Customer completes payment with 3D Secure if required
5. Checkout.com redirects to your success_url or cancel_url
6. Your server calls get_status(payment_id) to confirm
7. Checkout.com sends webhook events for status updates

Usage

Getting Payment Configuration

Before rendering the payment form, fetch the configuration to know which customer fields are required:

typescript
// Frontend: Fetch payment config
const config = await api.get('/payments/config');

// Returns:
{
  "provider": "checkout",
  "payment_methods": {
    "apple_pay_enabled": true,
    "google_pay_enabled": true,
    "card_payments_enabled": true
  },
  "customer_fields": {
    "require_email": false,
    "require_name": false,
    "require_phone": true
  }
}

Use this to configure your payment form:

tsx
<PaymentForm
  amount={99.99}
  customerFields={{
    requireEmail: config.customer_fields.require_email,
    requireName: config.customer_fields.require_name,
    requirePhone: config.customer_fields.require_phone,
  }}
/>

Creating the Provider

The factory creates the correct driver based on your configuration:

rust
use crate::services::payments::PaymentFactory;

let payments = PaymentFactory::create(&settings).await?;

Creating a Checkout Session

rust
let checkout = payments.create_checkout(CreateCheckoutRequest {
    amount: 9999,         // 99.99 in smallest unit (cents/halalas)
    currency: "SAR".to_string(),
    description: "Premium Plan - Annual Subscription".to_string(),
    reference_id: "order-abc-123".to_string(),
    success_url: "https://myapp.com/payment/success".to_string(),
    cancel_url: "https://myapp.com/payment/cancel".to_string(),
    customer: Some(CustomerInfo {
        email: Some("customer@example.com".to_string()),
        name: Some("Ahmed Ali".to_string()),
        phone: Some("+966501234567".to_string()),
    }),
}).await?;

// Redirect the customer to the payment page
println!("Redirect to: {}", checkout.payment_url);
println!("Checkout ID: {}", checkout.checkout_id);

Checking Payment Status

After the customer completes payment and returns to your success_url:

rust
let status = payments.get_status(&checkout_id).await?;

match status {
    PaymentStatus::Succeeded => {
        // Activate the subscription, fulfill the order, etc.
        println!("Payment confirmed!");
    }
    PaymentStatus::Pending | PaymentStatus::Processing => {
        // Payment still in progress -- wait for webhook
        println!("Payment is being processed");
    }
    PaymentStatus::Failed(reason) => {
        println!("Payment failed: {}", reason);
    }
    _ => {
        println!("Unexpected status: {:?}", status);
    }
}

Processing a Refund

rust
// Full refund
let refund = payments.refund("payment-id-123", None).await?;
println!("Refund {}: {:?}", refund.refund_id, refund.status);

// Partial refund (refund 25.00 of 99.99)
let partial = payments.refund("payment-id-123", Some(2500.0)).await?;
println!("Partial refund: {} units", partial.amount);

Listing Transactions

rust
let transactions = payments.list_transactions(TransactionFilters {
    status: Some(PaymentStatus::Succeeded),
    from_date: Some(chrono::NaiveDate::from_ymd_opt(2025, 1, 1).unwrap()),
    to_date: None,
    page: Some(1),
    per_page: Some(50),
}).await?;

for tx in &transactions {
    println!(
        "{} | {} {} | {:?} | {}",
        tx.id, tx.amount, tx.currency, tx.status, tx.created_at
    );
}

Webhook Handling

Both providers send webhook notifications for asynchronous payment events. FORGE generates a webhook endpoint automatically when you install a payment provider:

rust
// Auto-generated at POST /api/webhooks/payments
pub async fn payment_webhook(
    req: HttpRequest,
    body: web::Json<serde_json::Value>,
    payments: web::Data<Box<dyn PaymentProvider>>,
) -> Result<HttpResponse, AppError> {
    // 1. Verify webhook signature (provider-specific)
    // 2. Extract payment ID and event type
    // 3. Update order status in your database
    // 4. Return 200 OK to acknowledge receipt

    Ok(HttpResponse::Ok().finish())
}

WARNING

Always verify webhook signatures to prevent spoofed payment confirmations. Each provider uses a different signing mechanism -- refer to the provider's documentation for details.

Configuration via Admin

Payment settings are managed through the Settings admin page under the Payments group:

Settings > Payments
┌──────────────────────────────────────────────────┐
│  Provider:         [HyperPay          v]         │
│  Entity ID:        [abc123def456        ]        │
│  Access Token:     [................    ]        │
│  Mode:             [Test             v]          │
│  Currency:         [SAR                ]         │
└──────────────────────────────────────────────────┘

Error Handling

All payment operations return structured error types:

rust
match payments.create_checkout(request).await {
    Ok(checkout) => redirect_to(checkout.payment_url),
    Err(PaymentError::InvalidAmount) => println!("Amount must be positive"),
    Err(PaymentError::UnsupportedCurrency(c)) => println!("Currency {} not supported", c),
    Err(PaymentError::ProviderError(msg)) => println!("Provider error: {}", msg),
    Err(PaymentError::NetworkError(msg)) => println!("Network error: {}", msg),
    Err(e) => println!("Unexpected error: {}", e),
}

TIP

For production deployments, implement idempotent order processing. Use the reference_id field to prevent duplicate charges if a webhook is delivered more than once or if the customer refreshes the success page.

Released under the MIT License.