API Reference
Overview
The Virtual Accounts API allows partners to retrieve virtual account transaction data through a secure, signed request mechanism. All requests must be authenticated using RSA-SHA256 digital signatures.
Base URL
https://api.moonpay.com/v1/virtual-accounts
Authentication
RSA-SHA256 Digital Signatures
All API requests must be signed using RSA-SHA256 digital signatures. The signature is generated by signing the complete request payload (including path and query parameters) with your private key.
Required Headers
Header | Description |
---|---|
x-signature | Base64-encoded RSA-SHA256 signature of the request payload |
Content-Type | application/json |
Signature Generation Process
-
Create the payload: Concatenate the request path with query parameters
Format: {path}?{queryString} Example: /v1/virtual-accounts/transactions/onramp?apiKey=pk_test_xxx×tamp=1234567890
-
Generate signature: Use RSA-SHA256 to sign the payload with your private key
const sign = crypto.createSign('SHA256'); sign.update(payload); const signature = sign.sign(privateKey, 'base64');
-
Include signature: Add the signature to the
x-signature
header
Get Virtual Account Details
Retrieves virtual account details. Can be filtered by customer ID and/or wallet address.
Request
GET /v1/virtual-accounts?apiKey={publishableApiKey}×tamp={timestamp}&customerId={customerId}&virtualAccountId={virtualAccountId}
Query Parameters
Parameter | Type | Required | Description |
---|---|---|---|
apiKey | string | Yes | Your publishable API key (starts with pk_test_ for sandbox or pk_live_ for production) |
timestamp | number | Yes | Unix timestamp in milliseconds for request validation |
walletAddress | string | No | Optional filter by wallet address |
customerId | string | No | Optional filter by customer ID |
virtualAccountId | string | No | Optional filter by virtual account id |
Headers
Header | Type | Required | Description |
---|---|---|---|
x-signature | string | Yes | RSA-SHA256 signature of the request payload |
Content-Type | string | Yes | Must be application/json |
Example Request
async function getVirtualAccountDetails(customerId = null, virtualAccountId = null) {
// Generate timestamp
const timestamp = Date.now();
// Build query string with optional parameters
let queryParams = [`apiKey=${PUBLISHABLE_API_KEY}`, `timestamp=${timestamp}`];
if (customerId) queryParams.push(`customerId=${customerId}`);
if (walletAddress) queryParams.push(`virtualAccountId=${virtualAccountId}`);
// Build payload for signature
const path = '/v1/virtual-accounts';
const queryString = queryParams.join('&');
const payload = `${path}?${queryString}`;
// Create RSA-SHA256 signature
const sign = crypto.createSign('SHA256');
sign.update(payload);
const signature = sign.sign(PRIVATE_KEY, 'base64');
// Make the request
const url = `${SERVER_URL}${payload}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'x-signature': signature,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return await response.json();
}
// Usage examples:
// Get all virtual accounts
await getVirtualAccountDetails();
// Filter by customer ID only
await getVirtualAccountDetails('customerId123');
// Filter by virtual account ID
await getVirtualAccountDetails(null, 'virtualAccount123');
// Filter by both customer ID and virtual account ID
await getVirtualAccountDetails('customerId123', 'virtualAccount123');
Success Response (200 OK)
[
{
"id": "1234567890",
"type": "auto_on_ramp",
"destination": {
"wallet": {
"walletAddress": "0x1234576867890123456789012345678901234567",
"walletAddressTag": null
},
"network": {
"code": "solana",
"name": "Solana",
"icon": "https://static.moonpay.com/widget/currencies/usdc.svg",
"isAddressCaseSensitive": false,
"isEvmCompatible": true,
"addressRegex": "0x[a-fA-F0-9]{40}",
"supportsAddressTag": true,
"testnetWalletExplorerLink": "https://solana.com/testnet",
"mainnetWalletExplorerLink": "https://solana.com/mainnet",
"testnetTxnExplorerLink": "https://solana.com/testnet",
"mainnetTxnExplorerLink": "https://solana.com/mainnet",
"addressTagRegex": "0x[a-fA-F0-9]{40}",
"createdAt": "2025-06-10T11:45:25.202Z",
"updatedAt": "2025-06-10T11:45:25.202Z"
}
},
"sourceCurrency": {
"id": "123",
"code": "USD",
"name": "US Dollar",
"precision": 2,
"icon": "",
"maxAmount": 1000000,
"minAmount": 1,
"maxBuyAmount": 1000000,
"minBuyAmount": 1,
"accountBuyMinAmount": 1,
"isSellSupported": true,
"deletedAt": null,
"type": "fiat",
"decimals": 2,
"spreadPercentage": 0,
"isUtxoCompatible": false,
"supportedAcquirers": [],
"createdAt": "2025-06-10T11:45:25.202Z",
"updatedAt": "2025-06-10T11:45:25.202Z"
},
"destinationCurrency": {
"id": "123",
"code": "usdc_sol",
"name": "USD Coin (Solana)",
"type": "crypto",
"precision": 2,
"icon": "",
"maxAmount": 1000000,
"minAmount": 1,
"maxBuyAmount": 1000000,
"minBuyAmount": 1
},
"customerId": "123",
"organizationId": "123",
"createdAt": "2025-06-10T11:45:25.202Z",
"updatedAt": "2025-06-10T11:45:25.202Z",
"source": {
"iban": "DE89370400440532013000",
"bic": "DEUTDEDB123",
"accountNumber": "1234567890",
"sortCode": "1234567890",
"bankName": "Deutsche Bank",
"bankAddress": "123 Main St, Anytown, USA",
"accountName": "John Doe",
}
}
]
Response Field Descriptions
Field | Type | Description |
---|---|---|
id | string | Unique virtual account identifier |
type | string | Current virtual account type (auto_on_ramp, auto_off_ramp) |
state | string | Current virtual account state (pending, completed, failed) |
sourceCurrency | object | Source currency details, including code, name, precision, and limits (Fiat for auto_on_ramp and Crypto for auto_off_ramp) |
destinationCurrency | object | Destination currency details (Crypto for auto_on_ramp and Fiat for auto_off_ramp) |
source | object | Source account information: Bank account details for auto_on_ramp: name, IBAN, and address. |
destination | object | Destination wallet and network information (wallet and network) |
createdAt | string | ISO 8601 timestamp of virtual account creation |
updatedAt | string | ISO 8601 timestamp of virtual account last update |
Virtual Account Types
Type | Description |
---|---|
auto_on_ramp | Virtual account is receiving FIAT and sending Crypto |
auto_off_ramp | Virtual account is receiving Crypto and sending FIAT |
Virtual Account States
State | Description |
---|---|
pending | Virtual account is being created. |
completed | Virtual account is ready to use by customer |
failed | Virtual account creation failed due to some reason. |
Get Virtual Account OnRamp Transactions
Retrieves virtual account on-ramp transaction data for the authenticated partner (For VirtualAccount with type = auto_on_ramp
)
Request
GET /v1/virtual-accounts/transactions/onramp?apiKey={publishableApiKey}×tamp={timestamp}&virtualAccountId={virtualAccountId}&cursor={cursor}&pageSize={pageSize}
Query Parameters
Parameter | Type | Required | Description |
---|---|---|---|
apiKey | string | Yes | Your publishable API key (starts with pk_test_ for sandbox or pk_live_ for production) |
timestamp | number | Yes | Unix timestamp in milliseconds for request validation |
virtualAccountId | string | Yes | Virtual account ID to filter transactions by |
pageSize | number | Yes | Number of transactions per page |
cursor | string | No | Cursor value from the previous page response |
Headers
Header | Type | Required | Description |
---|---|---|---|
x-signature | string | Yes | RSA-SHA256 signature of the request payload |
Content-Type | string | Yes | Must be application/json |
Example Request
const crypto = require('crypto');
const fetch = require('node-fetch');
const SERVER_URL = 'https://api.moonpay.com';
const PUBLISHABLE_API_KEY = '[Your Publishable Api Key]';
const PRIVATE_KEY = `-----BEGIN PRIVATE KEY-----
[Your Private Key Here]
-----END PRIVATE KEY-----`;
async function getVirtualAccountTransactions(virtualAccountId) {
// Generate timestamp
const timestamp = Date.now();
// Build payload for signature
const path = '/v1/virtual-accounts/transactions/onramp';
const queryString = `apiKey=${PUBLISHABLE_API_KEY}×tamp=${timestamp}&virtualAccountId=${virtualAccountId}&pageSize=50`;
const payload = `${path}?${queryString}`;
// Create RSA-SHA256 signature
const sign = crypto.createSign('SHA256');
sign.update(payload);
const signature = sign.sign(PRIVATE_KEY, 'base64');
// Make the request
const url = `${SERVER_URL}${payload}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'x-signature': signature,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return await response.json();
}
Success Response (200 OK)
{
"nextCursor": "12345",
"transactions": [
{
"id": "1234567890",
"status": "Completed",
"sourceAmount": "100",
"sourceCurrency": {
"id": "123",
"code": "USD",
"name": "US Dollar",
"precision": 2,
"icon": "",
"maxBuyAmount": 1000000,
"minBuyAmount": 1,
"accountBuyMinAmount": 1,
"isSellSupported": true,
"maxAmount": 1000000,
"minAmount": 1,
"createdAt": "2025-06-10T11:47:27.582Z",
"updatedAt": "2025-06-10T11:47:27.582Z"
},
"destinationAmount": "100",
"destinationCurrency": {
"id": "123",
"code": "usdc_sol",
"name": "USD Coin (Solana)",
"precision": 2,
"icon": "",
"maxBuyAmount": 1000000,
"minBuyAmount": 1,
"accountBuyMinAmount": 1,
"isSellSupported": true,
"maxAmount": 1000000,
"minAmount": 1,
"createdAt": "2025-06-10T11:47:27.582Z",
"updatedAt": "2025-06-10T11:47:27.582Z"
},
"source": {
"accountName": "John Doe",
"iban": "DE89370400440532013000",
"accountAddress": "123 Main St, Anytown, USA"
},
"destination": {
"wallet": {
"walletAddress": "0x1234576867890123456789012345678901234567",
"walletAddressTag": null
},
"network": {
"code": "solana",
"name": "Solana",
"icon": "https://static.moonpay.com/widget/currencies/usdc.svg",
"isAddressCaseSensitive": false,
"isEvmCompatible": true,
"addressRegex": "0x[a-fA-F0-9]{40}",
"supportsAddressTag": true,
"testnetWalletExplorerLink": "https://solana.com/testnet",
"mainnetWalletExplorerLink": "https://solana.com/mainnet",
"testnetTxnExplorerLink": "https://solana.com/testnet",
"mainnetTxnExplorerLink": "https://solana.com/mainnet",
"addressTagRegex": "0x[a-fA-F0-9]{40}",
"createdAt": "2025-06-10T11:47:27.582Z",
"updatedAt": "2025-06-10T11:47:27.582Z"
}
},
"createdAt": "2025-06-10T11:47:27.582Z"
}
]
}
Response Field Descriptions
Field | Type | Description |
---|---|---|
nextCursor | string | Unique string to fetch next page |
transactions | Array | Transactions array |
Transaction Field Descriptions
Field | Type | Description |
---|---|---|
id | string | Unique transaction identifier |
status | string | Current transaction state (Pending, PayoutPending, Payout, PayoutCompleted, Completed, Failed, InAmlReview, AmlRejected, AmountRejected) |
sourceAmount | string | Amount in source currency |
sourceCurrency | object | Source currency details including code, name, precision, and limits |
destinationAmount | string | Amount in destination currency |
destinationCurrency | object | Destination currency details |
source | object | Source account information (name, IBAN, address) |
destination | object | Destination wallet and network information |
createdAt | string | ISO 8601 timestamp of transaction creation |
Errors
Error responses
400 Bad Request
{
"success": false,
"error": {
"code": "INVALID_REQUEST",
"message": "Missing required parameter: apiKey"
}
}
401 Unauthorized
{
"success": false,
"error": {
"code": "INVALID_SIGNATURE",
"message": "Request signature verification failed"
}
}
403 Forbidden
{
"success": false,
"error": {
"code": "INVALID_API_KEY",
"message": "The provided API key is invalid or inactive"
}
}
429 Too Many Requests
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Please try again later."
}
}
500 Internal Server Error
{
"success": false,
"error": {
"code": "INTERNAL_ERROR",
"message": "An internal server error occurred"
}
}
Error Handling Best Practices
- Always check response status codes before processing response data
- Implement retry logic for transient errors (5xx status codes)
- Log signature generation details for debugging authentication issues
- Handle rate limiting by implementing exponential backoff
Security Considerations
Private Key Management
- Never expose your private key in client-side code or public repositories
- Store private keys securely using environment variables or secure key management systems
- Rotate private keys regularly for enhanced security
Request Validation
- Timestamps are validated to prevent replay attacks
- Signatures are verified against your registered public key
- All requests must be made over HTTPS in production
Rate Limiting
- API requests are rate-limited per API key
- Current limit: 100 requests per minute per API key
- Exceeding rate limits will result in HTTP 429 responses
Testing
Sandbox Environment
Use the following for testing:
- API Key: Test keys start with
pk_test_
Sample Test Data
The sandbox environment includes sample virtual account transactions for testing integration.
Updated 6 days ago