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:
#[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
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
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
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.
| Setting | Type | Default (Stripe) | Default (MENA) | Description |
|---|---|---|---|---|
PAYMENT_REQUIRE_EMAIL | boolean | true | false | Require customer email |
PAYMENT_REQUIRE_NAME | boolean | false | false | Require customer name |
PAYMENT_REQUIRE_PHONE | boolean | false | true | Require 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:
# 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=truePayment Methods Configuration
Each provider supports enabling/disabling specific payment methods:
| Setting | Type | Default | Description |
|---|---|---|---|
{PROVIDER}_APPLE_PAY_ENABLED | boolean | false | Enable Apple Pay button |
{PROVIDER}_GOOGLE_PAY_ENABLED | boolean | false | Enable Google Pay button |
{PROVIDER}_CARD_PAYMENTS_ENABLED | boolean | true | Enable card payment form |
HYPERPAY_MADA_ENABLED | boolean | false | Enable Mada debit cards (HyperPay) |
CHECKOUT_MADA_ENABLED | boolean | false | Enable Mada debit cards (Checkout.com) |
HYPERPAY_SAMSUNG_PAY_ENABLED | boolean | false | Enable Samsung Pay (HyperPay only) |
Replace {PROVIDER} with STRIPE, CHECKOUT, or HYPERPAY.
Provider Support Matrix
| Payment Method | Stripe | Checkout.com | HyperPay |
|---|---|---|---|
| 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:
forge provider:add payment stripeConfiguration:
| Setting | Type | Description |
|---|---|---|
STRIPE_PUBLIC_KEY | string | Stripe publishable key (pk_test_...) |
STRIPE_SECRET_KEY | encrypted | Stripe secret key (sk_test_...) |
STRIPE_WEBHOOK_SECRET | encrypted | Webhook signing secret (whsec_...) |
STRIPE_MODE | string | "test" or "live" |
STRIPE_APPLE_PAY_ENABLED | boolean | Enable Apple Pay (true/false) |
STRIPE_APPLE_PAY_MERCHANT_ID | string | Apple Pay Merchant ID |
STRIPE_GOOGLE_PAY_ENABLED | boolean | Enable Google Pay (true/false) |
STRIPE_CARD_PAYMENTS_ENABLED | boolean | Enable 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:
- Go to Stripe Dashboard → Settings → Payment Methods → Apple Pay
- Add your domain and download the verification file
- Host the file at
https://yourdomain.com/.well-known/apple-developer-merchantid-domain-association - Click "Verify" in the Stripe dashboard
Alternatively, use the API:
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 confirmationHyperPay
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:
forge provider:add payment hyperpayConfiguration:
| Setting | Type | Description |
|---|---|---|
HYPERPAY_ENTITY_ID | string | HyperPay Entity ID for your checkout |
HYPERPAY_ACCESS_TOKEN | encrypted | HyperPay Access Token |
HYPERPAY_MODE | string | "test" or "live" |
HYPERPAY_APPLE_PAY_ENABLED | boolean | Enable Apple Pay (true/false) |
HYPERPAY_GOOGLE_PAY_ENABLED | boolean | Enable Google Pay via Fast Checkout (true/false) |
HYPERPAY_MADA_ENABLED | boolean | Enable Mada debit cards (true/false) |
HYPERPAY_MADA_ENTITY_ID | string | Separate Entity ID for Mada transactions |
HYPERPAY_CARD_PAYMENTS_ENABLED | boolean | Enable card payments (true/false) |
HYPERPAY_SAMSUNG_PAY_ENABLED | boolean | Enable 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):
// 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:
forge provider:add payment checkoutConfiguration:
| Setting | Type | Description |
|---|---|---|
CHECKOUT_PUBLIC_KEY | string | Checkout.com Public Key (for frontend) |
CHECKOUT_SECRET_KEY | encrypted | Checkout.com Secret Key |
CHECKOUT_MODE | string | "sandbox" or "production" |
CHECKOUT_APPLE_PAY_ENABLED | boolean | Enable Apple Pay (true/false) |
CHECKOUT_APPLE_PAY_MERCHANT_ID | string | Apple Pay Merchant ID |
CHECKOUT_GOOGLE_PAY_ENABLED | boolean | Enable Google Pay (true/false) |
CHECKOUT_MADA_ENABLED | boolean | Enable Mada debit cards (true/false) |
CHECKOUT_CARD_PAYMENTS_ENABLED | boolean | Enable 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:
- Register your domain with Apple Pay through Checkout.com dashboard
- Set
CHECKOUT_APPLE_PAY_MERCHANT_IDenvironment variable - The payment form will automatically show Apple Pay on supported devices
Mada Setup
To enable Mada payments with Checkout.com:
- Contact Checkout.com to enable Mada BIN configuration in your dashboard
- Set
CHECKOUT_MADA_ENABLED=truein your environment - 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 updatesUsage
Getting Payment Configuration
Before rendering the payment form, fetch the configuration to know which customer fields are required:
// 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:
<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:
use crate::services::payments::PaymentFactory;
let payments = PaymentFactory::create(&settings).await?;Creating a Checkout Session
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:
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
// 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
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:
// 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:
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.