Custom Payment Interface Development Guide
This document provides a comprehensive guide to implementing custom payment interfaces for Martian Pay, designed to help developers build payment pages in other languages/frameworks.
Table of Contents
- Overview
- Initialization Flow
- Crypto Payment Flow
- Stripe Card Payment Flow
- API Reference
- Data Type Definitions
- State Management
- Error Handling
Overview
Martian Pay is a payment gateway that supports two primary payment methods:
- Crypto Payment - Multi-chain, multi-token cryptocurrency payments
- Credit Card Payment - Stripe-powered card payments
Key Features
- Real-time payment status tracking
- Multi-currency support
- QR code generation for crypto addresses
- Saved payment method management
- Parent-child iframe communication
- Automatic page height adjustment
Initialization Flow
Step 1: Required Parameters from Backend
Before rendering the payment interface, you need to obtain the following parameters from your backend server:
| Parameter | Type | Description | Required |
|---|---|---|---|
paymentIntentId | string | Unique identifier for this payment intent | Yes |
publicKey | string | Merchant's public API key for client-side authentication | Yes |
clientSecret | string | Client-side secret for secure payment operations | Yes |
Important Security Notes:
- These parameters should be generated by your backend server
publicKeyis your public API key (safe to use in frontend/browser code) - different from your secret key used for backend API calls- The
clientSecretshould be unique per payment session - All three parameters are required for every API call to MartianPay
- Never use your secret API key in frontend applications - always use the public key for client-side integrations
Example Backend Implementation:
# Python/Flask example
@app.route('/create-payment')
def create_payment():
# Create payment intent via Martian Pay backend API
payment_intent = martian_pay.create_payment_intent(
amount=100.00,
currency='USD',
product_id='prod_xxx'
)
return {
'payment_intent_id': payment_intent.id,
'public_key': payment_intent.public_key,
'client_secret': payment_intent.client_secret
}
// Node.js/Express example
app.post("/create-payment", async (req, res) => {
const paymentIntent = await martianPay.createPaymentIntent({
amount: 100.0,
currency: "USD",
productId: "prod_xxx",
});
res.json({
paymentIntentId: paymentIntent.id,
publicKey: paymentIntent.publicKey,
clientSecret: paymentIntent.clientSecret,
});
});
Frontend Usage:
Once your backend provides these parameters, pass them to your payment interface:
// Fetch from your backend
const response = await fetch("/api/create-payment", { method: "POST" });
const { paymentIntentId, publicKey, clientSecret } = await response.json();
// Initialize payment interface with these parameters
initializePaymentInterface({
paymentIntentId,
publicKey,
clientSecret,
});
Step 2: Fetch Payment Intent Details
Endpoint: POST /payment_intents/link/{paymentIntentId}
For detailed API reference, see: Get Payment Intent Link API
Request Body:
{
"key": "public_key",
"client_secret": "client_secret"
}
Response:
{
"error_code": "success",
"msg": "Success",
"data": {
"id": "pi_xxx",
"amount": {
"asset_id": "USD",
"amount": "100.00"
},
"status": "RequiresPaymentMethod",
"client_secret": "cs_xxx",
"expired_at": 1234567890,
"payment_link_details": {
"merchant_id": "merchant_xxx",
"merchant_name": "Example Store",
"payment_link": {
"id": "pl_xxx",
"product": {
/* Product details */
},
"product_items": [
/* Product list */
],
"total_price": {
"asset_id": "USD",
"amount": "100.00"
}
}
}
}
}
Step 3: Fetch Supported Assets
Endpoint: GET /assets/all
Retrieves all supported cryptocurrency and fiat payment assets.
Note: This endpoint does not require authentication.
Response Structure:
{
"error_code": "success",
"msg": "success",
"data": [
{
"id": "USDC-Ethereum-TEST", // Asset ID (use this for API calls)
"display_name": "USDC", // Display name for UI
"coin": "USDC", // Token/coin symbol
"is_fiat": false, // true for fiat currencies
"decimals": 6, // Decimal precision
"payable": true, // Can be used for payments
"network": "Ethereum Sepolia", // Blockchain network
"is_mainnet": false, // Mainnet or testnet
"contract_address": "0x1c7D4B...", // Smart contract address
"token": "USDC", // Token symbol
"chain_id": 11155111 // EVM chain ID
},
// ... more assets
]
}
Key Fields:
id: The asset_id (unique identifier combining coin + network)coin: Token/coin symbol (e.g., "USDC", "BTC", "ETH")network: Blockchain network (e.g., "Ethereum Sepolia", "BSC Test")is_fiat:truefor fiat currencies (USD-STRIPE, USD-PAYERMAX),falsefor cryptopayable: Only assets withpayable: truecan be used for paymentsprovider: Payment provider for fiat currencies ("stripe", "payermax")
Usage Note: Store this asset list to build your payment UI. When users select a token and network combination, you'll need to map it to the corresponding asset_id for subsequent payment API calls (explained in the following steps).
For detailed API reference, see: https://api-docs.martianpay.com/reference#tag/assets/get/assets/all
Step 4: Initialize Page Communication
The payment page communicates with the parent window via postMessage:
// Notify parent that page is ready
window.parent.postMessage({ type: "ready" }, "*");
// Monitor and report height changes
const resizeObserver = new ResizeObserver(() => {
window.parent.postMessage(
{
type: "resize",
height: element.getBoundingClientRect().height,
},
"*"
);
});
Crypto Payment Flow
Flow Diagram
[Select Token] → [Select Network] → [Get Deposit Address] → [Show QR Code] → [Poll Transactions] → [Complete]
Detailed Steps
Step 1: User Selects Token and Network
Users choose from available options in your UI:
- Token: Cryptocurrency type (e.g., USDT, USDC, BTC)
- Network: Blockchain network (e.g., Ethereum Sepolia, BSC Test, Polygon)
Mapping to Asset ID:
After the user selects a token and network, you need to find the corresponding asset_id from the assets list fetched in Step 3.
// Example: User selected USDC on Ethereum Sepolia
const selectedToken = "USDC";
const selectedNetwork = "Ethereum Sepolia";
// Find the matching asset from the assets list
const selectedAsset = assets.find(asset =>
asset.coin === selectedToken &&
asset.network === selectedNetwork &&
asset.payable === true
);
// Get the asset_id to use in the API call
const assetId = selectedAsset.id; // "USDC-Ethereum-TEST"
Important Notes:
- Only assets with
payable: truecan be used for payments - Filter out fiat assets (
is_fiat: true) for crypto payments - The
asset_idis the unique identifier that combines token and network (e.g., "USDC-Ethereum-TEST")
Step 2: Request Deposit Address and Exchange Rate
Endpoint: POST /payment_intents/link/{paymentIntentId}/update
For detailed API reference, see: Update Payment Intent Link API
Request Body:
{
"payment_method_type": "crypto",
"payment_method_options": {
"crypto": {
"asset_id": "selected_asset_id"
}
},
"key": "public_key",
"client_secret": "client_secret"
}
Response:
{
"error_code": "success",
"data": {
"id": "pi_xxx",
"charges": [
{
"id": "charge_xxx",
"payment_method_options": {
"crypto": {
"amount": "100.50",
"token": "USDT",
"asset_id": "USDT_BSC",
"network": "BSC",
"exchange_rate": "1.0",
"deposit_address": "0x1234...5678",
"expired_at": 1234567890
}
},
"exchange_rate": "1.0",
"status": "Pending"
}
]
}
}
Step 3: Display Payment Information
The UI should display:
- QR Code: Scannable QR code with the deposit address
- Deposit Address: Copyable wallet address
- Amount to Pay: Converted cryptocurrency amount
- Exchange Rate: Current fiat to crypto exchange rate
- Remaining Amount: Real-time update of unpaid amount
- Expiry Timer: Countdown until payment expires
Step 4: Poll Transaction Status
Poll every 10 seconds to check for incoming transactions:
Endpoint: POST /payment_intents/link/{paymentIntentId}
Request Body:
{
"payment_method_options": {
"crypto": {
"asset_id": "selected_asset_id"
}
},
"key": "public_key",
"client_secret": "client_secret"
}
Response:
{
"error_code": "success",
"data": {
"id": "pi_xxx",
"status": "Processing",
"payment_details": {
"amount_captured": {
"asset_id": "USD",
"amount": "50.00"
}
},
"charges": [
{
"transactions": [
{
"tx_hash": "0xabcd...ef01",
"amount": "50.25",
"token": "USDT",
"network": "BSC",
"asset_id": "USDT_BSC",
"status": "confirmed",
"aml_status": "approved",
"created_at": 1234567890
}
]
}
]
}
}
Step 5: Transaction Status States
| Status | Description | Next Action |
|---|---|---|
submitted | Transaction detected on blockchain | Continue polling |
confirming | Waiting for block confirmations | Continue polling |
confirmed | Transaction fully confirmed | Check AML status |
Step 6: AML (Anti-Money Laundering) Check
| AML Status | Action |
|---|---|
approved | Payment accepted, continue processing |
rejected | Payment rejected, display reason to user |
'' (empty) | Still checking, continue polling |
Step 7: Payment Completion
When PaymentIntent.status === 'Success':
// Stop polling
clearInterval(transactionTimer);
// Notify parent window
window.parent.postMessage(
{
type: "success",
intent: paymentIntent,
},
"*"
);
// Update UI to success state
setStatus("success");
State Transitions
pending → initializing → waiting → processing → success
↓
expired / failed
Stripe Card Payment Flow
Flow Diagram
[Check Saved Cards] → [Choose Payment Method] → [Path A: Use Saved Card] → [API Call Only] → [Poll Status] → [Complete]
↓
[Path B: Add New Card] → [Initialize Stripe] → [Stripe Elements] → [Confirm Payment] → [Poll Status] → [Complete]
Two Payment Paths:
-
Path A - Using Saved Payment Method:
- Direct API call with
payment_method_id - No frontend Stripe integration needed
- Payment completes immediately via backend
- Direct API call with
-
Path B - Adding New Card:
- Requires Stripe.js frontend integration
- User enters card details in Stripe Elements
- Card can be saved for future use
Detailed Steps
Step 1: Fetch Saved Payment Methods
Endpoint: GET /customers/payment_methods/public/list
For detailed API reference, see: List Payment Methods API
Query Parameters:
?customer_id={customer_id}
&key={public_key}
&client_secret={client_secret}
Response:
{
"error_code": "success",
"data": {
"payment_methods": [
{
"id": "pm_xxx",
"provider": "stripe",
"type": "card",
"last4": "4242",
"brand": "visa",
"exp_month": 12,
"exp_year": 2025,
"funding": "credit",
"country": "US"
}
]
}
}
Handling:
- If
payment_methodsarray is empty or customer doesn't exist → Show new card form - If saved cards exist → Display saved cards list
Step 2: Initialize Stripe Payment Session
Endpoint: POST /payment_intents/link/{paymentIntentId}/update
For detailed API reference, see: Update Payment Intent Link API
Request Body:
{
"payment_method_type": "cards",
"payment_method_options": {
"fiat": {
"currency": "USD",
"save_payment_method": true // Optional: save for future use
}
},
"key": "public_key",
"client_secret": "client_secret"
}
Response:
{
"error_code": "success",
"data": {
"id": "pi_xxx",
"charges": [
{
"stripe_payload": {
"client_secret": "pi_xxx_secret_yyy",
"public_key": "pk_test_xxx",
"status": "pending"
}
}
]
}
}
Step 3: Load Stripe.js and Create Elements
import { loadStripe } from "@stripe/stripe-js";
// Initialize Stripe
const stripe = await loadStripe(stripe_payload.public_key);
// Create Elements instance
const elements = stripe.elements({
clientSecret: stripe_payload.client_secret,
appearance: {
theme: "stripe",
rules: {
".Input": {
border: "2px solid rgba(255, 255, 255, 0.2)",
},
},
},
loader: "auto",
});
// Create and mount Payment Element
const paymentElement = elements.create("payment", {
layout: { type: "tabs" },
});
paymentElement.mount("#stripe-payment-element");
Step 4: Payment Confirmation
Path A: Pay with Saved Card (Backend Only)
When the user selects a saved payment method, make a simple API call - no Stripe.js or frontend integration is required.
Endpoint: POST /payment_intents/link/{paymentIntentId}/update
Request Body:
{
"payment_method_type": "cards",
"payment_method_options": {
"fiat": {
"currency": "USD",
"payment_method_id": "pm_xxx" // ID from saved payment methods list
}
},
"key": "public_key",
"client_secret": "client_secret"
}
Response: Payment will be processed immediately via backend. Proceed directly to Step 5 (polling) to check the payment status.
Path B: Pay with New Card (Stripe Frontend Integration)
When the user chooses to add a new card, you need to use Stripe.js and Stripe Elements for secure card input.
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: "",
},
redirect: "if_required",
});
if (error) {
// Handle error
console.error(error.message);
} else {
// Payment successful, proceed to polling
setPaymentStatus("success");
}
Note: Path B requires Steps 2-3 (Initialize Stripe Payment Session and Load Stripe.js) to be completed first. Path A skips these steps entirely.
Step 5: Poll Payment Status
Poll every 3 seconds to check payment confirmation:
Endpoint: POST /payment_intents/link/{paymentIntentId}
Request Body:
{
"key": "public_key",
"client_secret": "client_secret"
}
Response:
{
"error_code": "success",
"data": {
"id": "pi_xxx",
"status": "Success",
"charges": [
{
"status": "Success",
"paid": true
}
]
}
}
Status Checks:
- If
intent.status === 'Success'→ Payment complete - If any
charge.status === 'Failed'→ Payment failed
Step 6: Completion
// Stop polling
clearInterval(transactionTimer);
// Notify parent
window.parent.postMessage(
{
type: "success",
intent: paymentIntent,
},
"*"
);
State Transitions
pending → initializing → payment_waiting → payment_processing → success
↓
failed
API Reference
Base Configuration
// Base URL (from environment)
const baseUrl = process.env.VITE_MARTIAN_PAY_API_URL;
// Default headers
headers: {
'Content-Type': 'application/json'
}
// CORS mode
mode: 'cors'
Unified Response Format
All API responses follow this structure:
interface JsonResponse {
code: number; // HTTP status code
error_code: string; // Error code ("success" means OK)
msg: string; // Message
data?: unknown; // Response data
}
Error Codes
| Error Code | Meaning | Handling |
|---|---|---|
success | Request successful | Continue flow |
server_error | Server error occurred | Prompt user to retry |
ErrStatusUnexpected | Payment intent expired | Notify user to refresh |
Common Endpoints
1. Get Payment Intent
- Method:
POST - Path:
/payment_intents/link/{paymentIntentId} - Purpose: Fetch payment intent details
2. Update Payment Intent
- Method:
POST - Path:
/payment_intents/link/{paymentIntentId}/update - Purpose: Initialize payment method (crypto/card)
3. Get Merchant Assets
- Method:
GET - Path:
/merchants/{merchantId}/assets - Purpose: Fetch supported crypto assets
4. Get Saved Payment Methods
- Method:
GET - Path:
/customers/payment_methods/public/list - Purpose: Retrieve customer's saved cards
Data Type Definitions
PaymentIntent
interface PaymentIntent {
id: string; // Payment intent ID
object: "payment_intent";
amount: PaymentAmount; // Total amount
status: PaymentIntentStatus; // Current status
payment_method_type: "crypto" | "card"; // Payment method
client_secret: string; // Client secret for auth
expired_at: number; // Unix timestamp
charges: Charge[] | null; // Charge records
payment_link_details: PaymentLinkDetail; // Product/merchant info
payment_details: PaymentDetail; // Amount breakdown
customer: Customer | null; // Customer info
}
PaymentIntentStatus
type PaymentIntentStatus =
| "RequiresPaymentMethod" // Needs payment method selection
| "Processing" // Payment being processed
| "Success" // Payment completed
| "Canceled" // Canceled by user/system
| "Refunded" // Refunded
| "PartialPaid"; // Partial payment received
Charge
interface Charge {
id: string;
amount: PaymentAmount;
payment_method_type: "crypto" | "card";
payment_method_options: PaymentMethodOptions;
exchange_rate: string; // Exchange rate
status: "Pending" | "Success" | "Failed";
transactions: Transaction[] | null; // Transaction list
stripe_payload?: {
// Stripe-specific data
client_secret: string;
public_key: string;
status: string;
};
}
Transaction (Crypto only)
interface Transaction {
tx_hash: string; // Blockchain transaction hash
amount: string; // Amount
token: string; // Token symbol (e.g., USDT)
network: string; // Network (e.g., BSC)
asset_id: string; // Asset identifier
status: "submitted" | "confirming" | "confirmed";
aml_status: "" | "approved" | "rejected"; // AML check result
aml_info: string; // AML details (if rejected)
created_at: number; // Unix timestamp
}
PaymentAmount
interface PaymentAmount {
asset_id: string; // Asset ID (e.g., "USD", "USDT_BSC")
amount: string; // Amount as string (for precision)
}
PaymentDetail
interface PaymentDetail {
amount_captured: PaymentAmount; // Amount received
amount_refunded: PaymentAmount; // Amount refunded
tx_fee: PaymentAmount; // Transaction fee
tax_fee: PaymentAmount; // Tax fee
frozen_amount: PaymentAmount; // Frozen amount (under review)
net_amount: PaymentAmount; // Net amount to merchant
gas_fee: Record<string, PaymentAmount>; // Gas fees by network
}
Token & Chain (Crypto only)
interface Token {
id: string; // Token ID
name: string; // Token name
symbol: string; // Token symbol (e.g., "USDT")
chains: Chain[]; // Supported chains
}
interface Chain {
id: string; // Chain ID (asset_id for API)
name: string; // Chain name (e.g., "Ethereum")
network: string; // Network identifier
}
SavedPaymentMethod (Stripe only)
interface SavedPaymentMethod {
id: string; // Payment method ID
provider: string; // "stripe"
type: string; // "card"
last4: string; // Last 4 digits
brand: string; // Card brand (visa, mastercard, etc.)
exp_month: number; // Expiration month
exp_year: number; // Expiration year
funding: string; // "credit" or "debit"
country: string; // Country code
}
State Management
Payment Status
type PaymentStatus =
| "pending" // Awaiting user action
| "initializing" // Setting up payment method
| "waiting" // Waiting for payment (crypto)
| "payment_waiting" // Waiting for card input (Stripe)
| "payment_processing" // Processing payment
| "processing" // Verifying transaction
| "success" // Payment completed
| "failed" // Payment failed
| "expired"; // Payment session expired
Crypto Payment State Flow
stateDiagram-v2
[*] --> pending: Page Load
pending --> initializing: Token/Chain Selected
initializing --> waiting: Address Retrieved
waiting --> processing: Transaction Detected
processing --> success: Transaction Confirmed
waiting --> expired: Timeout
processing --> failed: AML Rejected
Stripe Payment State Flow
stateDiagram-v2
[*] --> pending: Page Load
pending --> initializing: Payment Method Selected
initializing --> payment_waiting: Stripe Loaded
payment_waiting --> payment_processing: User Clicks Pay
payment_processing --> success: Payment Confirmed
payment_processing --> failed: Payment Error
Locking Mechanism
Prevents users from switching payment methods mid-transaction:
const [lockedPaymentMethod, setLockedPaymentMethod] = useState(undefined);
// Lock when payment starts
onLock(true); // Sets lockedPaymentMethod = 'crypto' or 'card'
// This disables other payment method accordions
collapsible: !lockedPaymentMethod || lockedPaymentMethod === "crypto"
? undefined
: "disabled";
Error Handling
1. Network Errors
try {
const res = await $fetch.POST(url, { data });
if (res.error_code !== "success") {
throw new Error(res.msg);
}
} catch (error) {
// Notify parent window
window.parent.postMessage(
{
type: "error",
message: error.message,
},
"*"
);
// Update UI
setStatus("failed");
onFailure(error);
}
2. Payment Intent Expired
if (res.error_code === ErrorMessages.ErrStatusUnexpected.error_code) {
setStatus("expired");
onExpired();
onLock(true);
clearInterval(transactionTimer);
window.parent.postMessage(
{
type: "error",
message:
"Your payment session has expired. Please refresh the page and try again.",
},
"*"
);
}
3. Crypto Transaction Errors
- Insufficient amount: User sent less than required → Display remaining amount
- AML rejection: Transaction flagged → Display
transaction.aml_info - Timeout: No transaction within expiry period → Show expired state
4. Stripe Payment Errors
const { error } = await stripe.confirmPayment({ elements });
if (error) {
// Display error to user
setStatus("failed");
onFailure(new Error(error.message));
// Common errors:
// - card_declined
// - insufficient_funds
// - expired_card
// - incorrect_cvc
}
Parent-Child Communication
Child to Parent Messages
// 1. Page ready
window.parent.postMessage({ type: "ready" }, "*");
// 2. Height change (for iframe resizing)
window.parent.postMessage(
{
type: "resize",
height: 600,
},
"*"
);
// 3. Payment success
window.parent.postMessage(
{
type: "success",
intent: paymentIntent,
},
"*"
);
// 4. Error occurred
window.parent.postMessage(
{
type: "error",
message: "Error message",
},
"*"
);
Message Types
| Type | Data | Purpose |
|---|---|---|
ready | - | Iframe loaded and ready |
resize | { height: number } | Content height changed |
success | { intent: PaymentIntent } | Payment completed |
error | { message: string } | Error occurred |
Best Practices
1. Amount Precision
Use BigNumber.js or similar for currency calculations:
import BigNumber from "bignumber.js";
// Parse from API
const amount = new BigNumber(paymentIntent.amount.amount);
// Calculate remaining
const remaining = totalAmount.minus(capturedAmount);
// Format for display
const display = amount.toFixed(2);
2. Polling Strategy
// Crypto: Poll every 10 seconds
const cryptoTimer = setInterval(() => {
fetchTransactions();
}, 10000);
// Stripe: Poll every 3 seconds
const stripeTimer = setInterval(() => {
fetchPaymentStatus();
}, 3000);
// Always cleanup
useEffect(() => {
return () => clearInterval(timer);
}, []);
3. Security Considerations
- ✅ Always use HTTPS
- ✅ Never store private keys or secrets client-side
- ✅ Validate
client_secreton every API call - ✅ Sanitize user inputs
- ✅ Use CORS properly
4. User Experience
- ✅ Show countdown timer for expiry
- ✅ Real-time remaining amount updates
- ✅ Clear error messages
- ✅ Copy-to-clipboard for addresses/amounts
- ✅ Loading states for all async operations
- ✅ Disable form during processing
5. Testing
Crypto Payments:
- Test with testnet addresses
- Verify QR code generation
- Test expiry timer
- Test partial payments
Stripe Payments:
- Use Stripe test cards:
4242 4242 4242 4242 - Test saved payment methods
- Test payment failures
- Test 3D Secure flows
Implementation Checklist
Initial Setup
- Configure API base URL
- Set up CORS
- Implement
postMessagecommunication - Add height monitoring
Crypto Payment
- Fetch and display available tokens/chains
- Implement deposit address request
- Generate and display QR code
- Show payment amount and exchange rate
- Implement transaction polling (10s interval)
- Handle AML status
- Display transaction list
- Calculate and show remaining amount
- Implement expiry timer
- Handle payment completion
Stripe Payment
- Load Stripe.js
- Fetch saved payment methods
- Display saved cards UI
- Initialize Stripe Elements
- Mount payment form
- Implement new card payment
- Implement saved card payment
- Add "save payment method" option
- Implement status polling (3s interval)
- Handle payment errors
- Handle payment success
Error Handling
- Network error handling
- Payment intent expiry
- Invalid payment method
- Transaction failures
- User-friendly error messages
Environment Variables
# API Configuration
VITE_MARTIAN_PAY_API_URL=https://api.martianpay.com
# Analytics (optional)
VITE_GA_TRACKING_ID=G-XXXXXXXXXX
# Feature Flags (optional)
VITE_PROJECT_ID=your-reown-project-id
Dependencies
Core Libraries
- BigNumber.js (for precise decimal calculations)
- @stripe/stripe-js (for Stripe integration)
Optional
- QRCode library (for QR code generation)
- React/Vue/Angular (UI framework of choice)
Browser Compatibility
| Browser | Minimum Version |
|---|---|
| Chrome | 90+ |
| Firefox | 88+ |
| Safari | 14+ |
| Edge | 90+ |
Required APIs:
fetchpostMessageResizeObserverURLSearchParams
Changelog
- 2025-12-01: Initial version extracted from martian-pay-checkout
Support
For questions or issues, please contact technical support or refer to the project repository.