KORAMP

MoonPay Widget

Integrate the MoonPay Widget as the primary payment gateway for KORAMP on-ramp (buy crypto) and off-ramp (sell crypto). MoonPay acts as the merchant of record, handling KYC/AML, fraud, and compliance.

Overview

The MoonPay Widget lets users buy (on-ramp) and sell (off-ramp) cryptocurrency directly inside your application. As the merchant of record, MoonPay takes full responsibility for fraud, chargebacks, KYC/AML, and regulatory compliance in every supported region.

Within KORAMP, MoonPay serves as the primary payment gateway for on-ramp and off-ramp crypto. KORAMP remains wallet-first — the wallet address is the user's identity; no account or password is required.

MoonPay as the primary gateway

MoonPay is KORAMP's primary payment gateway — handling KYC, fraud, and compliance as the merchant of record. Feature availability depends on the region and assets supported by MoonPay.

Prerequisites

  • A MoonPay Business account — sign up at dashboard.moonpay.com
  • Publishable API Key (prefix pk_) — safe to use on the client side
  • Secret Key (prefix sk_) — server-side only, must never be exposed to the client
  • Your application domain whitelisted in the MoonPay dashboard (Developers → Allowed Domains)
  • Use sandbox environment for testing; production for live traffic

Installation

bash
npm install @moonpay/moonpay-js

Then import the loadMoonPay function:

ts
import { loadMoonPay } from '@moonpay/moonpay-js';

Script tag

Alternatively, load the SDK via a <script> tag — the SDK will be available as window.MoonPayWebSdk:

html
<script src="https://static.moonpay.com/web-sdk/v1/moonpay-web-sdk.min.js" defer></script>

Both approaches provide identical functionality. Choose the one that fits your project setup best.

On-Ramp (Buy)

Transaction flow

  1. The user opens the widget (flow buy) — selects a cryptocurrency and the fiat amount they want to spend
  2. MoonPay handles login, KYC (for new users), and payment verification
  3. The user selects a payment method (credit/debit card, bank transfer, Apple Pay, Google Pay, etc.)
  4. Once payment is verified, crypto is sent to the pre-filled wallet address or the one the user enters

Integration example (overlay)

tsx
import { loadMoonPay } from '@moonpay/moonpay-js';
 
// First, fetch a signed URL from your backend (see URL Signing)
const signedUrl = await fetch('/api/moonpay/sign-url', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    flow: 'buy',
    currencyCode: 'eth',
    walletAddress: '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe',
    baseCurrencyCode: 'idr',
    baseCurrencyAmount: '500000',
    externalTransactionId: 'koramp_order_abc123',
  }),
}).then((r) => r.json()).then((d) => d.signedUrl);
 
// Initialize the SDK with the signed URL
const init = await loadMoonPay();
const sdk = new init({
  flow: 'buy',
  environment: 'sandbox',
  variant: 'overlay',
  params: { signedUrl },
  onCloseOverlay: () => {
    console.log('Widget closed by user');
  },
});
 
sdk.show();

Integration example (embedded)

tsx
const sdk = new init({
  flow: 'buy',
  environment: 'sandbox',
  variant: 'embedded',
  containerNodeSelector: '#moonpay-container',
  params: {
    apiKey: process.env.NEXT_PUBLIC_MOONPAY_API_KEY,
    currencyCode: 'eth',
    baseCurrencyCode: 'idr',
    externalTransactionId: 'koramp_order_abc123',
    // If walletAddress is included, a signed URL is REQUIRED
  },
});
// For embedded, do not call sdk.show()

URL Signing required for walletAddress

If you pre-fill walletAddress or walletAddresses, the widget URL must be signed (HMAC-SHA256) server-side before it is shown to the user. Without a valid signature, the widget will fail to load. See the URL Signing section below.

Off-Ramp (Sell)

Transaction flow

  1. The user opens the widget (flow sell) — selects a cryptocurrency and the amount they want to sell
  2. MoonPay handles login and KYC (for new users)
  3. The user selects a payout method (bank transfer)
  4. The user confirms the sale
  5. The partner app (KORAMP) sends crypto to MoonPay's deposit address on behalf of the user
  6. Once the deposit is verified, MoonPay sends a fiat payout to the user's bank account

Deposit timeout — 48 hours

The crypto deposit must reach MoonPay within 48 hours of the sell transaction being created. If it does not arrive in time, the transaction fails with the status Deposit timeout. Always send crypto promptly after the user confirms in the widget.

Integration example (off-ramp)

tsx
import { loadMoonPay } from '@moonpay/moonpay-js';
 
const init = await loadMoonPay();
const sdk = new init({
  flow: 'sell',
  environment: 'sandbox',
  variant: 'overlay',
  params: {
    apiKey: process.env.NEXT_PUBLIC_MOONPAY_API_KEY,
    baseCurrencyCode: 'eth',         // crypto to sell
    quoteCurrencyCode: 'idr',        // fiat the user receives
    baseCurrencyAmount: '0.01',
    email: 'user@example.com',
    externalTransactionId: 'koramp_sell_xyz789',
    // refundWalletAddress: '0x...' → if included, a signed URL is REQUIRED
  },
  onCloseOverlay: () => console.log('Widget closed'),
});
 
sdk.show();

refundWalletAddress vs walletAddress

For the sell flow, use refundWalletAddress (not walletAddress) to specify where funds should be returned if a refund is issued. Including refundWalletAddress or refundWalletAddresses also requires URL signing.

Widget Configuration

Common parameters (both flows)

ParameterTypeDescription
flowstring'buy' (on-ramp) or 'sell' (off-ramp)
environmentstring'sandbox' or 'production'
variantstring'overlay', 'embedded', 'newTab', 'newWindow'
containerNodeSelectorstringCSS selector for the embedded variant (required when embedded)
onCloseOverlayfunctionCallback when the overlay is closed (overlay variant only)
useWarnBeforeRefreshbooleanDefault true; disable in webviews if it causes redirect errors
emailstringPre-fill the user's email address
externalTransactionIdstringYour system's transaction ID (for idempotency and tracking)
externalCustomerIdstringYour system's customer ID
themestring'dark' or 'light'
redirectURLstringRedirect URL after the transaction completes (HTTPS or Universal Link)
paymentMethodstringPre-select a payment or payout method

On-Ramp parameters (flow buy)

ParameterTypeDescription
apiKeystringPublishable API key (required)
currencyCodestringCrypto to purchase (e.g. 'eth', 'sol', 'bnb')
walletAddressstringDestination wallet address — signed URL required
walletAddressesstringJSON string of multi-crypto wallet addresses — signed URL required
baseCurrencyCodestringFiat currency code (e.g. 'idr', 'usd')
baseCurrencyAmountnumberAmount of fiat the user wants to spend
lockAmountbooleanLock the amount so the user cannot change it
showAllCurrenciesbooleanShow all cryptos even if not all have a wallet address

Off-Ramp parameters (flow sell)

ParameterTypeDescription
apiKeystringPublishable API key (required)
baseCurrencyCodestringCrypto to sell (e.g. 'eth', 'sol')
baseCurrencyAmountnumberAmount of crypto to sell
quoteCurrencyCodestringFiat currency to receive (e.g. 'idr', 'usd', 'eur')
quoteCurrencyAmountnumberFiat equivalent amount; baseCurrencyAmount takes precedence if both are set
refundWalletAddressstringRefund wallet address — signed URL required
refundWalletAddressesstringJSON string of multi-crypto refund addresses — signed URL required
lockAmountbooleanLock the amount so the user cannot change it

URL Signing

Whenever you pre-fill sensitive information — walletAddress, walletAddresses, refundWalletAddress, or refundWalletAddresses — the widget URL must be signed with your Secret Key before it is sent to the user. If the signature is missing or invalid, MoonPay will refuse to load the widget.

Secret Key is server-side only

The Secret Key (sk_) must never reach the browser or be included in client-side code under any circumstances. All signing must happen on your backend.

How it works

  1. Build the complete widget URL with all query parameters on your server
  2. Use the Secret Key to generate an HMAC-SHA256 of the full query string (including the leading ?)
  3. Base64-encode the result, then URL-encode it
  4. Append &signature=<url-encoded-signature> at the very end of the URL
  5. Return the signed URL to the client; the client uses it directly to initialize the widget

Manual example (Node.js / server-side)

ts
import crypto from 'crypto';
 
export function signMoonPayUrl(url: string, secretKey: string): string {
  // Extract the query string (includes the leading '?')
  const urlObj = new URL(url);
  const queryString = urlObj.search; // e.g. "?apiKey=pk_test_...&walletAddress=0x..."
 
  // Generate HMAC-SHA256, encode to base64
  const signature = crypto
    .createHmac('sha256', secretKey)
    .update(queryString)
    .digest('base64');
 
  // URL-encode the signature and append it last
  return `${url}&signature=${encodeURIComponent(signature)}`;
}
 
// Example usage in a Next.js API route
// app/api/moonpay/sign-url/route.ts
import { NextRequest, NextResponse } from 'next/server';
 
export async function POST(req: NextRequest) {
  const { currencyCode, walletAddress, baseCurrencyCode, baseCurrencyAmount, externalTransactionId } =
    await req.json();
 
  const params = new URLSearchParams({
    apiKey: process.env.MOONPAY_PUBLISHABLE_KEY!,
    currencyCode,
    walletAddress,
    baseCurrencyCode,
    baseCurrencyAmount: String(baseCurrencyAmount),
    externalTransactionId,
  });
 
  const baseUrl = `https://buy-sandbox.moonpay.com/?${params.toString()}`;
  const signedUrl = signMoonPayUrl(baseUrl, process.env.MOONPAY_SECRET_KEY!);
 
  return NextResponse.json({ signedUrl });
}

Using the moonpay-node SDK

MoonPay provides a Node.js SDK (moonpay-node) that simplifies signing:

bash
npm install moonpay-node
ts
import MoonPay from 'moonpay-node';
 
const moonPay = new MoonPay(process.env.MOONPAY_SECRET_KEY!);
 
// Return only the signature (no URL-encoding — the SDK handles it)
const signature = moonPay.url.generateSignature(
  'https://buy-sandbox.moonpay.com/?apiKey=pk_test_...&walletAddress=0x...'
);
 
// Or return the full signed URL
const signedUrl = moonPay.url.generateSignature(
  'https://buy-sandbox.moonpay.com/?apiKey=pk_test_...&walletAddress=0x...',
  { returnFullURL: true }
);

Reference: URL Signing — MoonPay Docs

Webhooks

MoonPay sends real-time transaction status notifications to a webhook endpoint you configure in the dashboard. This is essential for asynchronously updating KORAMP order statuses — especially for the off-ramp flow where crypto sending and deposit verification take time.

Configuration

  1. Open MoonPay Dashboard → Developers → Webhooks
  2. Click Add Endpoint and enter your server URL
  3. Select the event types you want to receive (e.g. transaction_updated)
  4. Copy the Webhook API Key from the dashboard — this is separate from the Secret Key and is used exclusively for webhook signature verification

Signature verification

Every webhook request includes a Moonpay-Signature-V2 header in the format t=<timestamp>,s=<signature>. The signature is an HMAC-SHA256 of <timestamp>.<raw request body> keyed with the Webhook API Key (not the Secret Key).

ts
import crypto from 'crypto';
import { NextRequest, NextResponse } from 'next/server';
 
export async function POST(req: NextRequest) {
  const rawBody = await req.text();
  const signatureHeader = req.headers.get('moonpay-signature-v2') ?? '';
 
  // Parse header: "t=1234567890,s=abc123..."
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((part) => part.split('=') as [string, string])
  );
  const timestamp = parts['t'];
  const receivedSig = parts['s'];
 
  // Compute expected signature
  const expectedSig = crypto
    .createHmac('sha256', process.env.MOONPAY_WEBHOOK_KEY!) // Webhook API Key
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');
 
  if (expectedSig !== receivedSig) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }
 
  const event = JSON.parse(rawBody);
 
  // Idempotency: check whether this event has already been processed
  // use event.type + event.data.id + event.data.updatedAt as the dedup key
 
  console.log('MoonPay webhook event:', event.type, event.data?.id);
 
  return NextResponse.json({ received: true });
}

At-least-once delivery

MoonPay retries failed webhook deliveries up to 9 times. Your handler must be idempotent — deduplicate using event.type + transaction id + updatedAt. Respond with a 2xx status within 5 seconds; do any heavy processing asynchronously.

Status mapping

MoonPay sends various events to update transaction status. Conceptual mapping to KORAMP order states:

MoonPay EventKORAMP Order State (conceptual)
transaction_createdOrder created, awaiting action
transaction_updated (pending)Payment/deposit is being processed
transaction_updated (completed)Transaction complete — mark order COMPLETED
transaction_updated (failed)Transaction failed — mark order FAILED

For the full list of events and statuses, see the Webhooks Overview.

Variants

VariantDescriptionWhen to use
overlayWidget appears as an iframe over the pageDefault — keeps the user within your app
embeddedWidget is embedded inside a DOM element (containerNodeSelector required)Seamless in-page experience
newTabWidget opens in a new browser tabFull-screen experience; inform users they are being taken to MoonPay
newWindowWidget opens in a popup with a fixed sizeControlled experience outside the main tab

useWarnBeforeRefresh (default true) prompts users before they refresh or navigate away from the widget. Inside a mobile webview, this can cause errors when redirecting back to your app — disable it with useWarnBeforeRefresh: false if needed.

Testing

Use environment: 'sandbox' for all testing. Sandbox uses:

  • API keys prefixed pk_test_ (publishable) and sk_test_ (secret)
  • Base URLs https://buy-sandbox.moonpay.com (on-ramp) and https://sell-sandbox.moonpay.com (off-ramp)
  • Sandbox transactions do not move real funds
  • KYC in sandbox can be completed with dummy data

Visit MoonPay Developer Docs for test cards and test data provided by MoonPay.

Security

  • Never expose the Secret Key (sk_) in client-side code or environment variables accessible to the browser
  • Always sign URLs server-side before sending them to the client when the URL contains walletAddress, walletAddresses, refundWalletAddress, or refundWalletAddresses
  • Verify webhook signatures using the Webhook API Key (not the Secret Key)
  • Implement idempotency in your webhook handler using externalTransactionId or the combination of event.type + transaction id + updatedAt
  • Use externalTransactionId to track and reconcile MoonPay transactions with KORAMP orders

Protect your Secret Key

An exposed Secret Key allows third parties to sign widget URLs containing any user's wallet address. Store it in a server-side environment variable and never commit it to a repository.

Resources

Content on this page is summarised and paraphrased from the official MoonPay Developer Docs for licensing compliance.