Skip to main content

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

Martian Pay is a payment gateway that supports two primary payment methods:

  1. Crypto Payment - Multi-chain, multi-token cryptocurrency payments
  2. 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:

ParameterTypeDescriptionRequired
paymentIntentIdstringUnique identifier for this payment intentYes
publicKeystringMerchant's public API key for client-side authenticationYes
clientSecretstringClient-side secret for secure payment operationsYes

Important Security Notes:

  • These parameters should be generated by your backend server
  • publicKey is your public API key (safe to use in frontend/browser code) - different from your secret key used for backend API calls
  • The clientSecret should 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: true for fiat currencies (USD-STRIPE, USD-PAYERMAX), false for crypto
  • payable: Only assets with payable: true can be used for payments
  • provider: 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: true can be used for payments
  • Filter out fiat assets (is_fiat: true) for crypto payments
  • The asset_id is 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

StatusDescriptionNext Action
submittedTransaction detected on blockchainContinue polling
confirmingWaiting for block confirmationsContinue polling
confirmedTransaction fully confirmedCheck AML status

Step 6: AML (Anti-Money Laundering) Check

AML StatusAction
approvedPayment accepted, continue processing
rejectedPayment 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:

  1. Path A - Using Saved Payment Method:

    • Direct API call with payment_method_id
    • No frontend Stripe integration needed
    • Payment completes immediately via backend
  2. 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_methods array 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 CodeMeaningHandling
successRequest successfulContinue flow
server_errorServer error occurredPrompt user to retry
ErrStatusUnexpectedPayment intent expiredNotify 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

TypeDataPurpose
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_secret on 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 postMessage communication
  • 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

BrowserMinimum Version
Chrome90+
Firefox88+
Safari14+
Edge90+

Required APIs:

  • fetch
  • postMessage
  • ResizeObserver
  • URLSearchParams

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.