Mintarex OTC API Documentation
The Mintarex API provides programmatic access to OTC crypto trading, wallet management, and real-time market data for institutional clients.
Mintarex is an OTC (over-the-counter) trading platform — not an exchange. We use a Request-for-Quote (RFQ) model: you request a price, we lock it, you execute with zero slippage. This is what treasury desks, payment processors, and remittance companies need.
What you can do with the API
- Trade — Request RFQ quotes and execute trades across 2,000+ crypto/fiat pairs and 6 fiat currencies (USD, EUR, GBP, CAD, AED, INR)
- Wallets — Check balances, get deposit addresses, submit withdrawals across 90+ blockchain networks
- Market Data — Real-time price feeds via Server-Sent Events (SSE)
- Webhooks — Receive push notifications for trade fills, deposits, and withdrawals
- Account — View balances, fee rates, and transaction limits
Supported currencies
| Type | Currencies |
|---|---|
| Fiat | USD, EUR, GBP, CAD, AED, INR |
| Crypto | BTC, ETH, USDT, USDC, SOL, XRP, ADA, DOGE, LTC, MATIC, TON, and 500+ more |
| Networks | 90+ blockchain networks (EVM chains + Bitcoin, Solana, Cardano, XRP, TON, Sui, Aptos, etc.) |
eth, btc, sol), NOT full names (ethereum, bitcoin, solana). Always use GET /v1/networks to discover the exact identifiers.Common coin & network identifiers
Use GET /v1/networks?coin={SYMBOL} for the complete list. Here are the most common ones:
| Coin | Network Key | Chain Name |
|---|---|---|
| BTC | btc | Bitcoin |
| ETH | eth | Ethereum (ERC-20) |
| ETH | arb | Arbitrum One |
| ETH | base | Base |
| ETH | op | Optimism |
| ETH | matic | Polygon |
| USDT | eth | Ethereum (ERC-20) |
| USDT | arb | Arbitrum One |
| USDT | sol | Solana (SPL) |
| USDT | arb | Arbitrum One |
| USDC | eth | Ethereum (ERC-20) |
| USDC | sol | Solana (SPL) |
| USDC | base | Base |
| SOL | sol | Solana |
| XRP | xrp | XRP Ledger |
| LTC | ltc | Litecoin |
| DOGE | doge | Dogecoin |
| ADA | ada | Cardano |
| TON | ton | The Open Network |
| MATIC | matic | Polygon |
For the full list of 500+ coins and 90+ networks, call GET /v1/networks (no authentication required).
Base URL
All API requests are made to:
https://institutional.mintarex.com/v1
All requests must use HTTPS. HTTP requests are automatically redirected.
Health check
GET https://institutional.mintarex.com/health
Response:
{
"ok": true,
"service": "mintarex-otc-gateway",
"version": "0.0.1",
"timestamp": "2026-04-09T09:33:04.593Z",
"uptime_seconds": 3600
}
Authentication
Every API request must include four authentication headers:
| Header | Description |
|---|---|
MX-API-KEY | Your public API key (e.g. mxn_live_a8f3b2c1d4e5f6a7b8c9d0e1) |
MX-SIGNATURE | HMAC-SHA256 signature of the request (64 hex characters) |
MX-TIMESTAMP | Current Unix timestamp in seconds |
MX-NONCE | Unique UUID v4 per request (replay protection) |
Optional headers
| Header | Description |
|---|---|
MX-IDEMPOTENCY-KEY | UUID v4 for safe retries on POST requests. Same key returns cached response within 24h. |
Generating the API Key
- Log in to the Mintarex Dashboard with a corporate account
- Complete corporate KYC verification (must be approved)
- Navigate to Settings → API Keys
- Click Create API Key
- Select your desired permissions (scopes)
- Add at least one IP address to the whitelist (required for production keys)
- Your API key and secret will be displayed once — save the secret immediately
Key format
API Key: mxn_live_a8f3b2c1d4e5f6a7b8c9d0e1 (33 characters, public) API Secret: YWJjZGVmZ2hpamtsbW5vcH... (64 characters, secret — save immediately)
Permission scopes
| Scope | Description |
|---|---|
read:account | View account info, fees, limits |
read:wallet | View balances, deposit addresses |
read:trade | View trade history |
read:market | View market data and instruments |
trade:quote | Request RFQ quotes |
trade:execute | Execute trades (accept quotes) |
withdraw:crypto | Submit crypto withdrawals (requires pre-whitelisted addresses) |
webhook:manage | Create/manage webhook endpoints |
stream:market | Subscribe to price streams (SSE) |
stream:account | Subscribe to account event streams (SSE) |
IP whitelisting
Production keys require at least one IP address. Requests from non-whitelisted IPs are rejected with 403 IP_NOT_WHITELISTED. Supports IPv4, IPv6, and CIDR notation (e.g. 203.0.113.0/24).
Signing Requests
Every authenticated request must be signed with HMAC-SHA256 using your API secret.
Step 1: Build the canonical string
Concatenate with newline (\n) separator:
canonical = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + BODY_HASH
| Component | Description | Example |
|---|---|---|
METHOD | HTTP method (uppercase) | GET |
PATH | Full path including query string | /v1/account/balances?currency_type=crypto |
TIMESTAMP | Unix timestamp in seconds | 1712582345 |
NONCE | UUID v4 (unique per request) | 550e8400-e29b-41d4-a716-446655440000 |
BODY_HASH | SHA-256 hex of request body | e3b0c44298fc1c14... (empty body hash for GET) |
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Step 2: Compute the HMAC
signature = HMAC-SHA256(api_secret, canonical_string).hex() // 64 hex characters
Step 3: Send the headers
MX-API-KEY: mxn_live_a8f3b2c1d4e5f6a7b8c9d0e1 MX-TIMESTAMP: 1712582345 MX-NONCE: 550e8400-e29b-41d4-a716-446655440000 MX-SIGNATURE: 7d9b2c5f8a1e4b6d9c2e5f8a1b4c7d0e3f6a9b2c5f8a1e4b6d9c2e5f8a1b4c7
Official SDKs
Use an official SDK to skip the manual signing, retry, and webhook-verification work shown in the code samples below. Each SDK auto-detects sandbox vs live from the key prefix, handles HMAC signing, parses error responses into typed exceptions, and ships a webhook signature verifier.
| Language | Package | Source | Status |
|---|---|---|---|
| Node.js / TypeScript | @mintarex-official/node |
github.com/mintarex/mintarex-node | v0.0.6 |
| Python | mintarex |
github.com/mintarex/mintarex-python | v0.0.6 |
| Go | github.com/mintarex/mintarex-go |
github.com/mintarex/mintarex-go | v0.0.5 |
npm install @mintarex-official/node
import { Mintarex } from '@mintarex-official/node';
const mx = new Mintarex({
apiKey: process.env.MINTAREX_API_KEY, // mxn_live_... or mxn_test_...
apiSecret: process.env.MINTAREX_API_SECRET,
});
// Request a quote and execute it
const quote = await mx.rfq.quote({
base: 'BTC', quote: 'USD', side: 'buy',
amount: '0.5', amount_type: 'base',
});
const trade = await mx.rfq.accept(quote.quote_id);
console.log(trade.trade_id, trade.status);
// Verify a webhook (use raw request body, not parsed JSON)
import { verifyWebhook } from '@mintarex-official/node';
const event = verifyWebhook({
body: rawBody, headers: req.headers,
secret: process.env.MINTAREX_WEBHOOK_SECRET,
});
fetch and node:crypto.pip install mintarex
import os
from mintarex import Mintarex
mx = Mintarex(
api_key=os.environ["MINTAREX_API_KEY"], # mxn_live_... or mxn_test_...
api_secret=os.environ["MINTAREX_API_SECRET"],
)
# Request a quote and execute it
quote = mx.rfq.quote({
"base": "BTC", "quote": "USD", "side": "buy",
"amount": "0.5", "amount_type": "base",
})
trade = mx.rfq.accept(quote["quote_id"])
print(trade["trade_id"], trade["status"])
# Verify a webhook (use raw request body, not parsed JSON)
from mintarex import verify_webhook
event = verify_webhook(
body=request.get_data(), # exact bytes, NOT parsed JSON
headers=dict(request.headers),
secret=os.environ["MINTAREX_WEBHOOK_SECRET"],
)
py.typed, mypy-strict clean). One runtime dependency: httpx.go get github.com/mintarex/[email protected]
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/mintarex/mintarex-go"
)
func main() {
mx, err := mintarex.New(mintarex.Options{
APIKey: os.Getenv("MINTAREX_API_KEY"), // mxn_live_... or mxn_test_...
APISecret: os.Getenv("MINTAREX_API_SECRET"),
})
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// Request a quote and execute it
quote, err := mx.RFQ.Quote(ctx, mintarex.QuoteRequest{
Base: "BTC", Quote: "USD", Side: "buy",
Amount: "0.5", AmountType: "base",
})
if err != nil {
log.Fatal(err)
}
trade, err := mx.RFQ.Accept(ctx, quote.QuoteID, mintarex.AcceptOptions{})
if err != nil {
log.Fatal(err)
}
fmt.Println(trade.TradeID, trade.Status)
}
// Verify a webhook (use the raw request body, not parsed JSON)
event, err := mintarex.VerifyWebhook(mintarex.VerifyParams{
Body: rawBody, // []byte from io.ReadAll(r.Body)
Headers: r.Header,
Secret: os.Getenv("MINTAREX_WEBHOOK_SECRET"),
})
github.com/google/uuid). Context-aware, race-free, fully tested.Code Samples
Complete signing and request examples in 5 languages — for callers who don't use an SDK.
const crypto = require('node:crypto');
const https = require('node:https');
const API_KEY = 'mxn_live_your_key_here';
const API_SECRET = 'your_secret_here';
const BASE = 'https://institutional.mintarex.com';
function signedRequest(method, path, body = '') {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID();
const bodyStr = (body !== null && typeof body === 'object') ? JSON.stringify(body) : (body || '');
const bodyHash = crypto.createHash('sha256').update(bodyStr).digest('hex');
const canonical = [method, path, timestamp, nonce, bodyHash].join('\n');
const signature = crypto.createHmac('sha256', API_SECRET).update(canonical).digest('hex');
return {
headers: {
'MX-API-KEY': API_KEY, 'MX-TIMESTAMP': timestamp,
'MX-NONCE': nonce, 'MX-SIGNATURE': signature,
'Content-Type': 'application/json',
},
body: bodyStr,
};
}
// GET example
const { headers } = signedRequest('GET', '/v1/account/balances');
https.get(new URL('/v1/account/balances', BASE), { headers }, (res) => {
let d = ''; res.on('data', c => d += c);
res.on('end', () => console.log(JSON.parse(d)));
});
// POST example (request a quote)
const quoteBody = { base: 'BTC', quote: 'USD', side: 'buy', amount: '1000', amount_type: 'quote' };
const req = signedRequest('POST', '/v1/rfq', quoteBody);
const r = https.request(new URL('/v1/rfq', BASE), {
method: 'POST', headers: req.headers,
}, (res) => {
let d = ''; res.on('data', c => d += c);
res.on('end', () => console.log(JSON.parse(d)));
});
r.write(req.body); r.end();
import hashlib, hmac, json, time, uuid, requests
API_KEY = 'mxn_live_your_key_here'
API_SECRET = 'your_secret_here'
BASE = 'https://institutional.mintarex.com'
def signed_request(method, path, body=None):
timestamp = str(int(time.time()))
nonce = str(uuid.uuid4())
body_str = json.dumps(body) if body else ''
body_hash = hashlib.sha256(body_str.encode()).hexdigest()
canonical = f"{method}\n{path}\n{timestamp}\n{nonce}\n{body_hash}"
signature = hmac.new(API_SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest()
headers = {
'MX-API-KEY': API_KEY, 'MX-TIMESTAMP': timestamp,
'MX-NONCE': nonce, 'MX-SIGNATURE': signature,
'Content-Type': 'application/json',
}
return headers, body_str
# GET example
headers, _ = signed_request('GET', '/v1/account/balances')
resp = requests.get(f'{BASE}/v1/account/balances', headers=headers)
print(resp.json())
# POST example (request a quote)
body = {'base': 'BTC', 'quote': 'USD', 'side': 'buy', 'amount': '1000', 'amount_type': 'quote'}
headers, data = signed_request('POST', '/v1/rfq', body)
resp = requests.post(f'{BASE}/v1/rfq', headers=headers, data=data)
print(resp.json())
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/google/uuid"
)
const (
apiKey = "mxn_live_your_key_here"
apiSecret = "your_secret_here"
baseURL = "https://institutional.mintarex.com"
)
func signRequest(method, path, body string) http.Header {
timestamp := fmt.Sprintf("%d", time.Now().Unix())
nonce := uuid.New().String()
h := sha256.Sum256([]byte(body))
bodyHash := hex.EncodeToString(h[:])
canonical := strings.Join([]string{method, path, timestamp, nonce, bodyHash}, "\n")
mac := hmac.New(sha256.New, []byte(apiSecret))
mac.Write([]byte(canonical))
signature := hex.EncodeToString(mac.Sum(nil))
headers := http.Header{}
headers.Set("MX-API-KEY", apiKey)
headers.Set("MX-TIMESTAMP", timestamp)
headers.Set("MX-NONCE", nonce)
headers.Set("MX-SIGNATURE", signature)
headers.Set("Content-Type", "application/json")
return headers
}
func main() {
path := "/v1/account/balances"
req, _ := http.NewRequest("GET", baseURL+path, nil)
req.Header = signRequest("GET", path, "")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.*;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.UUID;
public class MintarexApi {
static final String API_KEY = "mxn_live_your_key_here";
static final String API_SECRET = "your_secret_here";
static final String BASE_URL = "https://institutional.mintarex.com";
static String sha256(String data) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : hash) sb.append(String.format("%02x", b));
return sb.toString();
}
static String hmacSha256(String key, String data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"));
byte[] hash = mac.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : hash) sb.append(String.format("%02x", b));
return sb.toString();
}
public static void main(String[] args) throws Exception {
String method = "GET";
String path = "/v1/account/balances";
String timestamp = String.valueOf(Instant.now().getEpochSecond());
String nonce = UUID.randomUUID().toString();
String bodyHash = sha256("");
String canonical = method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + bodyHash;
String signature = hmacSha256(API_SECRET, canonical);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + path))
.header("MX-API-KEY", API_KEY)
.header("MX-TIMESTAMP", timestamp)
.header("MX-NONCE", nonce)
.header("MX-SIGNATURE", signature)
.GET().build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}
using System.Security.Cryptography;
using System.Text;
var apiKey = "mxn_live_your_key_here";
var apiSecret = "your_secret_here";
var baseUrl = "https://institutional.mintarex.com";
string Sha256(string data) {
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(data));
return Convert.ToHexString(bytes).ToLower();
}
string HmacSha256(string key, string data) {
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
return Convert.ToHexString(hash).ToLower();
}
var method = "GET";
var path = "/v1/account/balances";
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var nonce = Guid.NewGuid().ToString();
var bodyHash = Sha256("");
var canonical = $"{method}\n{path}\n{timestamp}\n{nonce}\n{bodyHash}";
var signature = HmacSha256(apiSecret, canonical);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, baseUrl + path);
request.Headers.Add("MX-API-KEY", apiKey);
request.Headers.Add("MX-TIMESTAMP", timestamp);
request.Headers.Add("MX-NONCE", nonce);
request.Headers.Add("MX-SIGNATURE", signature);
var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
<?php
$apiKey = 'mxn_live_your_key_here';
$apiSecret = 'your_secret_here';
$baseUrl = 'https://institutional.mintarex.com';
function signedRequest($method, $path, $body = '') {
global $apiKey, $apiSecret;
$timestamp = (string)time();
$nonce = sprintf('%s-%s-%s-%s-%s',
bin2hex(random_bytes(4)), bin2hex(random_bytes(2)),
bin2hex(random_bytes(2)), bin2hex(random_bytes(2)),
bin2hex(random_bytes(6)));
$bodyHash = hash('sha256', $body);
$canonical = implode("\n", [$method, $path, $timestamp, $nonce, $bodyHash]);
$signature = hash_hmac('sha256', $canonical, $apiSecret);
return [
'MX-API-KEY: ' . $apiKey,
'MX-TIMESTAMP: ' . $timestamp,
'MX-NONCE: ' . $nonce,
'MX-SIGNATURE: ' . $signature,
'Content-Type: application/json',
];
}
// GET example
$path = '/v1/account/balances';
$headers = signedRequest('GET', $path);
$ch = curl_init($baseUrl . $path);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
// POST example
$path = '/v1/rfq';
$body = json_encode(['base'=>'BTC','quote'=>'USD','side'=>'buy','amount'=>'1000','amount_type'=>'quote']);
$headers = signedRequest('POST', $path, $body);
$ch = curl_init($baseUrl . $path);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
?>
# Set credentials
API_KEY="mxn_live_your_key_here"
API_SECRET="your_secret_here"
# Build signature components
METHOD="GET"
PATH="/v1/account/balances"
TIMESTAMP=$(date +%s)
NONCE=$(python3 -c "import uuid; print(uuid.uuid4())")
BODY_HASH=$(echo -n "" | sha256sum | cut -d' ' -f1)
# Sign
CANONICAL=$(printf "%s\n%s\n%s\n%s\n%s" "$METHOD" "$PATH" "$TIMESTAMP" "$NONCE" "$BODY_HASH")
SIGNATURE=$(echo -ne "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" | cut -d' ' -f2)
# Request
curl -s "https://institutional.mintarex.com${PATH}" \
-H "MX-API-KEY: ${API_KEY}" \
-H "MX-TIMESTAMP: ${TIMESTAMP}" \
-H "MX-NONCE: ${NONCE}" \
-H "MX-SIGNATURE: ${SIGNATURE}" | python3 -m json.tool
Rate Limits
Rate limits are enforced per API key using a sliding window. The same limits apply to every account.
| Bucket | Requests /min |
|---|---|
| Read | 300 |
| Write | 100 |
| Quote | 60 |
| Execute | 30 |
| Withdraw | 15 |
Endpoint classification
| Bucket | Endpoints |
|---|---|
| Read | All GET requests (balances, history, fees, limits) |
| Write | POST/DELETE requests not in other buckets (webhooks, address management) |
| Quote | POST /v1/rfq — requesting a quote |
| Execute | POST /v1/rfq/{id}/accept — executing a trade |
| Withdraw | POST /v1/crypto/withdraw |
Concurrent quote limit
Each API key may hold a maximum of 5 active (unexpired) quotes simultaneously. Requesting a 6th quote while 5 are still within their 30-second TTL returns 429 too_many_active_quotes. Wait for existing quotes to expire before requesting more.
Request format
All POST, PUT, and PATCH requests must include a Content-Type: application/json header. Requests with other content types are rejected with 415 Unsupported Media Type.
Request bodies must contain only documented fields. Unknown or extra fields are rejected with 400 invalid_parameter.
Rate limit headers
Every response includes IETF-standard rate limit headers:
| Header | Description |
|---|---|
RateLimit-Limit | Maximum requests allowed in the current window |
RateLimit-Remaining | Requests remaining in the current window |
RateLimit-Reset | Seconds until the window resets |
Retry-After | Seconds to wait before retrying (only on 429 responses) |
Error Codes
All errors return JSON with flat error and message fields:
{ "error": "invalid_signature", "message": "Request signature verification failed" }
HTTP status codes
| Status | Meaning |
|---|---|
| 200 | Success |
| 202 | Accepted — operation requires email confirmation (webhook create/delete, address revoke) |
| 400 | Bad request (invalid parameters) |
| 401 | Authentication failed |
| 403 | Forbidden (IP blocked, insufficient scope, feature not enabled) |
| 404 | Resource not found |
| 413 | Request body too large (max 100KB) |
| 422 | Unprocessable (e.g. idempotency key reused with different body) |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
| 502 | Upstream service unavailable |
| 503 | Service temporarily unavailable |
Authentication error codes
| Code | Description |
|---|---|
missing_auth_headers | Required headers missing (MX-API-KEY, MX-SIGNATURE, MX-TIMESTAMP, MX-NONCE) |
invalid_key_format | Key doesn't match mxn_live_ or mxn_test_ + 24 hex chars |
invalid_key | API key not found |
key_disabled | Key disabled by owner |
key_suspended | Key suspended by administrator |
key_revoked | Key permanently revoked |
key_expired | Key past expiry date |
key_auto_disabled | Key temporarily locked due to repeated auth failures (auto-unlocks after 5 minutes) |
timestamp_skew | Timestamp outside 30-second window |
nonce_replay | Nonce already used (possible replay attack) |
invalid_signature | HMAC signature verification failed |
ip_not_whitelisted | Client IP not in key's whitelist |
insufficient_scope | Key lacks required permission scope |
trading_not_enabled | Trading not enabled for this key (contact support) |
withdrawals_not_enabled | Withdrawals not enabled for this key (contact support) |
rate_limit_exceeded | Too many requests — check RateLimit-* headers |
Trading error codes
| Code | HTTP | Description |
|---|---|---|
invalid_parameter | 400 | Invalid or missing request parameter |
same_currency | 400 | Base and quote currencies must be different |
unsupported_pair | 400 | One or more currencies not supported |
fiat_disabled | 400 | Fiat currency temporarily disabled for new trades |
no_network | 400 | Could not determine network — specify explicitly |
invalid_network | 400 | Network not supported for this currency |
price_unavailable | 400 | Unable to get market price — try again |
amount_too_small | 400 | Trade amount too small (after spread) |
amount_too_large | 400 | Amount exceeds maximum allowed |
min_amount | 400 | Below minimum trade amount for this currency |
max_amount | 400 | Above maximum trade amount for this currency |
daily_limit | 400 | Daily trading limit reached |
monthly_limit | 400 | Monthly trading limit reached |
insufficient_balance | 400 | Not enough funds to execute trade |
quote_expired_or_not_found | 410 | Quote has expired or does not exist |
quote_already_consumed | 409 | Quote has already been executed |
idempotency_key_conflict | 409 | Idempotency key used with a different quote |
too_many_active_quotes | 429 | Maximum 5 concurrent active quotes per key — wait for existing quotes to expire |
per_key_concurrency_limit | 429 | Too many concurrent trade executions per API key |
invalid_address | 400 | Address format invalid for the specified network |
unsupported_media_type | 415 | Content-Type must be application/json |
service_unavailable | 503 | Temporary service issue — retry with backoff |
Idempotency
For safe retries on POST requests (orders, withdrawals), include the MX-IDEMPOTENCY-KEY header with a UUID v4.
MX-IDEMPOTENCY-KEY: 550e8400-e29b-41d4-a716-446655440001
- Same key + same body within 24h: returns the cached response from the first request
- Same key + different body: returns
422 Unprocessable Entity - Keys expire after 24 hours
Pagination
List endpoints (trades, deposits, withdrawals) support cursor-based pagination.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Number of results per page (max 200) |
offset | integer | 0 | Number of results to skip |
from | string | — | Start date (ISO 8601, e.g. 2026-01-01T00:00:00Z) |
to | string | — | End date (ISO 8601) |
sort | string | desc | Sort order: asc or desc |
Example
GET /v1/trades?limit=20&offset=0&from=2026-04-01T00:00:00Z&sort=desc
Response envelope
{
"data": [ ... ],
"pagination": {
"total": 145,
"limit": 20,
"offset": 0,
"has_more": true
}
}
Get Balances
GET /v1/account/balances
Returns all wallet balances. Requires read:wallet scope.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
currency_type | string | No | fiat or crypto |
include_empty | boolean | No | Include zero-balance wallets (default: true) |
Response
{
"balances": [
{
"currency": "BTC",
"currency_type": "crypto",
"available": "1.25000000",
"locked": "0.10000000",
"pending_in": "0.05000000",
"pending_out": "0.00000000",
"total": "1.35000000",
"usd_value": "88290.15",
"usd_price": "65400.11"
},
{
"currency": "USD",
"currency_type": "fiat",
"available": "50000.00",
"locked": "0.00",
"pending_in": "0.00",
"pending_out": "0.00",
"total": "50000.00"
}
],
"timestamp": "2026-04-09T10:30:00.000Z"
}
Get Single Balance
GET /v1/account/balance/{currency}
Returns balance for a specific currency across all wallet types. Requires read:wallet scope.
Response
{
"currency": "USDT",
"currency_type": "crypto",
"total_available": "125000.00000000",
"total_locked": "5000.00000000",
"total": "130000.00000000",
"by_wallet_type": [
{ "wallet_type": "funding", "available": "125000.00000000", "locked": "5000.00000000" }
],
"timestamp": "2026-04-09T10:30:00.000Z"
}
Get Transaction Limits
GET /v1/account/limits
Returns daily and monthly transaction limits by type. Requires read:account scope.
{
"limits": {
"crypto_deposit": {
"daily_limit": "1000000.00",
"daily_used": null,
"monthly_limit": "5000000.00",
"monthly_used": null,
"remaining_daily": null,
"remaining_monthly": null
},
"crypto_withdrawal": {
"daily_limit": "500000.00",
"daily_used": null,
"monthly_limit": "2000000.00",
"monthly_used": null,
"remaining_daily": null,
"remaining_monthly": null
}
},
"account_type": "corporate",
"timestamp": "2026-04-09T10:30:00.000Z"
}
Only crypto deposit and withdrawal limits are exposed via the API. Fiat deposit/withdrawal limits (bank, card, e-wallet) apply to dashboard-only operations and are not returned here. All amounts are decimal strings; daily_used, monthly_used, remaining_daily, and remaining_monthly are returned as null when not yet computed.
Request Quote (RFQ)
POST /v1/rfq
Request a locked-price quote. Valid for 30 seconds. Requires trade:quote scope.
from_network and to_network.Request
// Crypto/fiat trade
{ "base": "BTC", "quote": "USD", "side": "buy", "amount": "1000.00", "amount_type": "quote" }
// Crypto-to-crypto swap
{ "base": "ETH", "quote": "BTC", "side": "buy", "amount": "1.0", "amount_type": "base", "from_network": "eth", "to_network": "btc" }
| Field | Type | Required | Description |
|---|---|---|---|
base | string | Yes | Base currency — must be crypto (BTC, ETH, SOL, etc.) |
quote | string | Yes | Quote currency — fiat (USD, EUR) or crypto for swaps (BTC, USDT) |
side | string | Yes | buy (receive base, pay quote) or sell (pay base, receive quote) |
amount | string | Yes | Trade amount as a string (e.g. "100.50"). Must be positive, no leading zeros, no scientific notation, max 18 decimal places. Integer or array values are rejected. |
amount_type | string | Yes | base or quote — which currency the amount is denominated in |
network | string | No | Network for the crypto side (e.g. eth, btc, sol). Auto-detected if omitted. |
from_network | string | No | Source network for crypto-to-crypto swaps (base currency network). Auto-detected if omitted. |
to_network | string | No | Destination network for crypto-to-crypto swaps (quote currency network). Auto-detected if omitted. |
Response
{
"quote_id": "550e8400-e29b-41d4-a716-446655440000",
"base": "BTC",
"quote": "USD",
"side": "buy",
"network": "btc",
"price": "72014.25000000",
"base_amount": "0.01388614",
"quote_amount": "1000.00",
"expires_at": "2026-04-09T14:32:18Z",
"expires_in_ms": 30000
}
For crypto-to-crypto swaps, additional fields are included:
{
"is_swap": true,
"from_network": "eth",
"to_network": "btc",
...
}
Response fields
| Field | Type | Description |
|---|---|---|
quote_id | string | Unique quote identifier (UUID) |
network | string | Network identifier for the primary crypto currency |
price | string | Executable price with spread included (all-in, no separate fee) |
base_amount | string | Amount of base currency in the trade |
quote_amount | string | Amount of quote currency in the trade |
expires_at | string | ISO 8601 timestamp when the quote expires |
expires_in_ms | integer | Milliseconds until expiry (30000) |
is_swap | boolean | Present only for crypto-to-crypto swaps |
from_network | string | Base currency network (swaps only) |
to_network | string | Quote currency network (swaps only) |
price field is NOT the raw market price. It includes the OTC spread. Market/indicative prices are available via GET /v1/instruments and the price stream.Accept Quote
POST /v1/rfq/{quote_id}/accept
Execute a previously received quote before it expires. Requires trade:execute scope.
Request
The quote_id is in the URL path. The only body field is the idempotency key — all trade parameters are stored server-side in the quote.
{ "idempotency_key": "550e8400-e29b-41d4-a716-446655440000" }
| Field | Type | Required | Description |
|---|---|---|---|
idempotency_key | string | Yes | UUID v4 for safe retries (max 64 chars). Same key returns same result. |
Response (200 — filled)
{
"trade_id": "550e8400-e29b-41d4-a716-446655440001",
"status": "filled",
"base": "BTC",
"quote": "USD",
"side": "buy",
"network": "btc",
"price": "72014.25000000",
"base_amount": "0.01388614",
"quote_amount": "1000.00",
"filled_at": "2026-04-09T14:32:01Z"
}
For crypto-to-crypto swaps, additional fields: is_swap, from_network, to_network.
Get Trade History
GET /v1/trades
Paginated list of executed trades. Requires read:trade scope.
Query parameters
| Parameter | Type | Description |
|---|---|---|
base | string | Filter by base currency (e.g. BTC) |
quote | string | Filter by quote currency (e.g. USD) |
side | string | Filter by side (buy or sell) |
limit | integer | Results per page (default 50, max 200) |
offset | integer | Results to skip |
from | string | Start date (ISO 8601) |
to | string | End date (ISO 8601) |
Response
{
"data": [
{
"trade_id": "tr_550e8400...",
"base": "BTC", "quote": "USD", "side": "buy",
"price": "72014.25", "base_amount": "0.01388614",
"quote_amount": "1000.00",
"status": "filled", "created_at": "2026-04-09T14:32:01Z"
}
],
"pagination": { "total": 23, "limit": 50, "offset": 0, "has_more": false }
}
Get Trade Detail
GET /v1/trades/{trade_id}
Returns details of a single trade. Requires read:trade scope.
Get Deposit Address
GET /v1/crypto/deposit-address?coin={symbol}&network={network}
Returns (or creates) a deposit address for the specified coin and network. If no network is specified, the default deposit-enabled network is used. Requires read:wallet scope.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
coin | string | Yes | Coin symbol (e.g. BTC, ETH, USDT) |
network | string | No | Network identifier (e.g. btc, eth, sol). If omitted, the default deposit-enabled network is used. |
Response
{
"address": "bc1q59c6n76ta7rgh093mlxr86kvhled77yux6km8r",
"coin": "BTC",
"network": "btc",
"memo_required": false,
"min_deposit": "0.00010000",
"required_confirmations": 3,
"timestamp": "2026-04-12T12:35:46.123Z"
}
memo_required field. Use GET /v1/networks?coin=XRP for network details.Get Crypto Deposits
GET /v1/crypto/deposits
Paginated list of crypto deposits. Requires read:wallet scope. Supports pagination and filters.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
coin | string | No | Filter by coin (e.g. BTC) |
status | string | No | detected, pending_confirmations, confirming, crediting, completed, failed |
from | ISO 8601 | No | Start date |
to | ISO 8601 | No | End date |
limit | integer | No | 1-200, default 50 |
offset | integer | No | Default 0 |
Response
{
"data": [
{
"deposit_id": "550e8400-e29b-41d4-a716-446655440000",
"coin": "BTC",
"network": "btc",
"amount": "0.50000000",
"tx_hash": "abc123def456...",
"from_address": "1A1zP1eP5QGefi2...",
"confirmations": 6,
"required_confirmations": 3,
"status": "completed",
"detected_at": "2026-04-09T10:00:00.000Z",
"updated_at": "2026-04-09T10:45:00.000Z"
}
],
"pagination": { "total": 5, "limit": 50, "offset": 0, "has_more": false }
}
Submit Crypto Withdrawal
POST /v1/crypto/withdraw
Submit a crypto withdrawal request. The withdrawal enters pending_review status and requires admin approval before on-chain execution. The destination address must be pre-whitelisted and past its 24-hour cooling period. Requires withdraw:crypto scope + allow_withdrawals enabled.
Request
{
"coin": "BTC",
"network": "btc",
"amount": "0.1",
"address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"address_tag": "",
"idempotency_key": "my-withdrawal-20260412-001"
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
coin | string | Yes | Coin symbol |
network | string | Yes | Network identifier |
amount | string | Yes | Plain decimal string (e.g. "0.1"). No scientific notation, no leading zeros, max 18 decimal places. Must be a string type — integers rejected. |
address | string | Yes | Must be in the active withdrawal allowlist. Address format is validated against the network's native checksum. |
address_tag | string | No | Memo/tag (max 100 chars, for chains that require it) |
idempotency_key | string | Yes | Unique key for safe retries (1-64 chars) |
Response
{
"withdrawal_id": "550e8400-e29b-41d4-a716-446655440000",
"reference": "MNX-CW26041200001",
"status": "pending_review",
"coin": "BTC",
"network": "btc",
"amount": "0.10000000",
"fee": "0.00050000",
"total_deducted": "0.10050000",
"amount_usd": "4215.05",
"to_address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"message": "Withdrawal request submitted for admin review."
}
Get Crypto Withdrawals
GET /v1/crypto/withdrawals
Paginated list of crypto withdrawals. Requires read:wallet scope. Same filter parameters as Get Deposits.
Response
{
"data": [
{
"withdrawal_id": "550e8400-e29b-41d4-a716-446655440000",
"reference": "MNX-CW26041200001",
"coin": "BTC",
"network": "btc",
"amount": "0.10000000",
"fee": "0.00050000",
"amount_usd": "4215.05",
"to_address": "bc1qxy2kgdygjrsqtzq2n0yrf...",
"tx_hash": null,
"status": "pending_review",
"reject_reason": null,
"created_at": "2026-04-12T11:00:00.000Z",
"completed_at": null
}
],
"pagination": { "total": 1, "limit": 50, "offset": 0, "has_more": false }
}
Get Withdrawal Detail
GET /v1/crypto/withdrawals/{uuid}
Returns a single withdrawal with full detail including tx_hash and explorer_url when available. Requires read:wallet scope.
Add Allowlist Address
POST /v1/crypto/withdrawal-addresses
Add a crypto address to the withdrawal allowlist. Triggers an email confirmation. After confirmation, a 24-hour cooling period begins before the address can be used for withdrawals. Requires withdraw:crypto scope.
400 invalid_address. Use GET /v1/networks?coin=BTC to find valid network identifiers.address_tag must be numeric (destination tag). Adding an address without a memo on these networks returns a warning field in the response — funds sent to an exchange without a memo may be unrecoverable.Request
{
"currency": "BTC",
"network": "btc",
"address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"address_tag": "",
"label": "Cold Storage Vault"
}
Response (201)
{
"success": true,
"address_uuid": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"message": "Confirmation email sent. Click the link to confirm, then wait 24-hour cooling period."
}
List Allowlisted Addresses
GET /v1/crypto/withdrawal-addresses
Returns all addresses in your withdrawal allowlist with status and usage stats. Requires read:wallet scope. Filter by currency, network, or status.
Response
{
"data": [
{
"address_uuid": "550e8400-e29b-41d4-a716-446655440000",
"currency": "BTC",
"network": "btc",
"address": "bc1qxy2kgdygjrsqtzq2n0yrf...",
"address_tag": null,
"label": "Cold Storage Vault",
"status": "active",
"cooling_until": null,
"is_usable": true,
"withdrawal_count": 5,
"total_withdrawn_amount": "1.50000000",
"last_withdrawal_at": "2026-04-10T14:30:00.000Z",
"created_at": "2026-04-01T09:00:00.000Z"
}
],
"pagination": { "total": 1, "limit": 50, "offset": 0, "has_more": false }
}
Revoke Address
DELETE /v1/crypto/withdrawal-addresses/{uuid}
Initiates revocation of an address from the withdrawal allowlist. Requires email confirmation — the address is not revoked until you click the link in the confirmation email. Requires withdraw:crypto scope.
Response (202 — pending confirmation)
{
"confirmation_uuid": "550e8400-e29b-41d4-a716-446655440000",
"operation_type": "address_revoke",
"status": "pending_confirmation",
"message": "Confirmation email sent. Click the link to confirm this operation. Expires in 24 hours."
}
Webhooks
Register HTTPS endpoints to receive push notifications for trades, deposits, withdrawals, and account events.
Webhook URL requirements
- Must use HTTPS (HTTP rejected)
- Must use ASCII-only domain names (no internationalized/punycode domains)
- Must not contain credentials (
user:pass@hostrejected) - Must not point to private/reserved IPs, localhost, or cloud metadata endpoints
- Must not use URL shorteners or redirect services (bit.ly, tinyurl, ngrok, httpbin, etc.)
- Maximum 2,048 characters
- Maximum 10 webhook endpoints per account
How it works
- Call
POST /v1/webhookswith your URL and event types — returns202withpending_confirmation - Check your email and click the confirmation link
- The webhook is created and the signing secret is sent to your email — save it immediately
- Mintarex signs each delivery with HMAC-SHA256 using the signing secret
- Your server verifies the
X-Mintarex-Signatureheader and responds with200 OKwithin 10 seconds - Failed deliveries retry with exponential backoff (30s, 1m, 2m, 4m, 8m, 16m, 32m, 64m — max 8 attempts)
Webhook delivery headers
| Header | Description |
|---|---|
X-Mintarex-Signature | Signature in the form v1=<64-hex> — HMAC-SHA256 of {timestamp}.{rawBody} using your webhook signing secret |
X-Mintarex-Timestamp | Unix timestamp (seconds) of the delivery — used in the signed payload |
X-Mintarex-Event-Type | Event type (e.g. trade.executed) |
X-Mintarex-Event-Id | Unique event identifier (the same ID is sent on every retry of the same event — use for idempotent processing) |
X-Mintarex-Delivery-Id | Unique identifier for this delivery attempt (differs across retries) |
Delivery payload
The body is a flat JSON object containing the event-specific fields plus an ISO timestamp. Event metadata (event_type, event_id, delivery_uuid) is delivered via the X-Mintarex-* headers listed above, not inside the body.
{
"timestamp": "2026-04-09T14:32:01Z",
"trade_id": "tr_550e8400...",
"base": "BTC",
"quote": "USD",
"side": "buy",
"price": "65432.10",
"base_amount": "0.01521",
"quote_amount": "995.18",
"filled_at": "2026-04-09T14:32:01Z",
"sandbox": false
}
"sandbox": true in the payload. Production events omit the field or set it to false. Sandbox-only webhook endpoints receive only sandbox events, and vice versa.Webhook Event Types
| Event | Description |
|---|---|
trade.executed | RFQ trade was filled successfully |
deposit.detected | Crypto deposit seen on-chain (unconfirmed) |
deposit.confirmed | Deposit reached required confirmations and credited to balance |
withdrawal.requested | Crypto withdrawal submitted and pending admin approval |
withdrawal.approved | Withdrawal approved by admin, queued for broadcast |
withdrawal.completed | Withdrawal confirmed on-chain |
withdrawal.cancelled | Withdrawal was cancelled or rejected |
Verifying Webhook Signatures
Always verify the X-Mintarex-Signature header before processing.
The signed string is {X-Mintarex-Timestamp}.{rawBody}. Verify in constant time and reject deliveries older than 5 minutes.
const crypto = require('node:crypto');
function verifyWebhook(rawBody, headers, secret) {
const sigHeader = headers['x-mintarex-signature'] || '';
const ts = headers['x-mintarex-timestamp'] || '';
if (!sigHeader.startsWith('v1=')) return false;
const given = Buffer.from(sigHeader.slice(3), 'hex');
const expected = Buffer.from(
crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex'),
'hex',
);
if (given.length !== expected.length) return false;
if (!crypto.timingSafeEqual(given, expected)) return false;
// Reject stale deliveries (±5 min tolerance)
return Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) <= 300;
}
app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyWebhook(req.body, req.headers, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const eventType = req.headers['x-mintarex-event-type'];
const eventId = req.headers['x-mintarex-event-id'];
const data = JSON.parse(req.body); // { timestamp, ...event fields }
console.log(eventType, eventId, data);
res.status(200).send('OK');
});
import hmac, hashlib, time
def verify_webhook(body: bytes, headers: dict, secret: str) -> bool:
sig_header = headers.get('X-Mintarex-Signature', '')
ts = headers.get('X-Mintarex-Timestamp', '')
if not sig_header.startswith('v1='):
return False
given = sig_header[3:]
expected = hmac.new(
secret.encode(), f"{ts}.{body.decode()}".encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(given, expected):
return False
# Reject stale deliveries (±5 min tolerance)
return abs(int(time.time()) - int(ts)) <= 300
# In Flask:
@app.route('/webhook', methods=['POST'])
def webhook():
if not verify_webhook(request.data, request.headers, WEBHOOK_SECRET):
return 'Invalid signature', 401
event_type = request.headers.get('X-Mintarex-Event-Type')
event_id = request.headers.get('X-Mintarex-Event-Id')
data = request.json # { timestamp, ...event fields }
print(event_type, event_id, data)
return 'OK', 200
Real-time Streaming (SSE)
Mintarex provides Server-Sent Events (SSE) streams for real-time price updates and account events. SSE uses a two-step authentication flow:
- Get a stream token —
POST /v1/stream/token(authenticated via HMAC like any other endpoint) - Connect to SSE — Pass the token as a query parameter to the SSE endpoint on the same base URL.
Step 1: Get Stream Token
POST /v1/stream/token
Returns a single-use, 60-second token. Requires stream:market or stream:account scope.
// Response
{ "token": "a1b2c3d4e5f6...64 hex chars...", "expires_in": 60 }
Step 2: Connect to Price Stream
GET https://institutional.mintarex.com/v1/stream/prices?token={token}&instruments=BTC_USD,ETH_USD
Real-time price updates. Requires stream:market scope.
| Parameter | Type | Description |
|---|---|---|
token | string | Stream token from Step 1 |
instruments | string | Comma-separated pairs (e.g. BTC_USD,ETH_EUR,SOL_AED) |
Step 2 (alt): Connect to Account Stream
GET https://institutional.mintarex.com/v1/stream/account?token={token}
Real-time account events (trade fills, deposits, withdrawals). Requires stream:account scope.
Account stream event types
trade.executed— trade filleddeposit.detected— deposit seen on-chaindeposit.confirmed— deposit credited to balancewithdrawal.requested— withdrawal submittedwithdrawal.approved— withdrawal approved by adminwithdrawal.completed— withdrawal confirmed on-chainwithdrawal.cancelled— withdrawal cancelled or rejected
Node.js example
const EventSource = require('eventsource');
// Step 1: Get token (using your signedRequest helper)
const tokenReq = signedRequest('POST', '/v1/stream/token', '');
const tokenRes = await fetch(BASE + '/v1/stream/token', {
method: 'POST', headers: tokenReq.headers,
});
const { token } = await tokenRes.json();
// Step 2: Connect to SSE (same host as API)
const url = `https://institutional.mintarex.com/v1/stream/prices?token=${token}&instruments=BTC_USD,ETH_USD`;
const es = new EventSource(url);
es.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(data.instrument, data.price, data.change_24h);
};
Stream limits
- Max 3 concurrent SSE connections per API key
- Connections auto-close after 1 hour (reconnect with a new token)
- Heartbeat sent every 15 seconds (detect stale connections)
- Stream token is single-use and expires in 60 seconds
Sandbox Environment
The sandbox environment lets you test your API integration without moving real funds. Sandbox keys use the same base URL, same endpoints, and same authentication — the only difference is that trades and withdrawals are simulated.
Getting Started
- Create a sandbox key — Go to Developer Settings, toggle to Sandbox mode, and click Create Key. Your sandbox key will have the prefix
mxn_test_. - Use the same base URL —
https://institutional.mintarex.com. No separate sandbox URL. - Authenticate identically — HMAC-SHA256 signatures, nonce replay protection, IP whitelisting — all enforced the same as production.
- Test your integration — Request quotes, execute trades, manage withdrawal addresses, register webhooks. All responses include
"sandbox": true.
What's Different in Sandbox
| Feature | Production | Sandbox |
|---|---|---|
| API Key prefix | mxn_live_ | mxn_test_ |
| Balances | Real wallet balances | Pre-funded test balances (10 BTC, 100 ETH, 1M in your registered fiat, etc.) |
| RFQ Quotes | Real market prices with OTC spread | Same real market prices, same spread calculation |
| Trade Execution | Real wallet debits/credits, ledger entries | Simulated — no wallet changes, no ledger entries. Trade recorded with is_sandbox = 1 |
| Crypto Withdrawals | Real fund lock, admin approval, on-chain broadcast | Simulated — instant completed status, no real funds locked |
| Deposit Addresses | Real on-chain addresses | Clearly fake test addresses (e.g. 0x...C0FFEE) |
| Deposits | Detected by on-chain scanners | Not applicable — returns empty list |
| Withdrawal Addresses | Isolated to production | Isolated to sandbox — separate allowlist |
| Webhooks | Isolated to production | Isolated to sandbox — separate endpoints |
| Transaction Limits | Enforced (daily/monthly caps) | Skipped — trade freely without hitting limits |
| Trade History | Shows only production trades | Shows only sandbox trades |
| SSE Streams | Real price data + account events | Same real price data, account events include sandbox: true |
| Fiat Currency | Your registered fiat only | Same — restricted to your registered fiat |
Sandbox Responses
All sandbox responses include a "sandbox": true field so your code can distinguish test data from production:
// Sandbox quote response
{
"quote_id": "550e8400-...",
"base": "BTC",
"quote": "USD",
"price": "74424.24242424",
"base_amount": "0.99000000",
"quote_amount": "74000.00",
"expires_in_ms": 30000,
"sandbox": true
}
Data Isolation
Sandbox and production data are completely isolated. Sandbox trades, withdrawal addresses, webhooks, and activity never appear in production views, and vice versa. You can safely test without affecting your live trading operations.
Changelog
2026-04-14 — v1.4.0 (Sandbox Environment + Fiat Restriction)
- Sandbox environment — Create sandbox API keys (
mxn_test_) to test your integration without moving real funds. All endpoints work identically; trades, withdrawals, and balances are simulated. See Sandbox Environment. - Dashboard sandbox toggle — Switch between Live and Sandbox modes on the Developer Settings page. Each mode shows isolated data — keys, addresses, webhooks, and activity are separate.
- Fiat currency restriction — RFQ quotes are now restricted to your account's registered fiat currency. If your account uses USD, you can only trade crypto/USD pairs. Attempting crypto/EUR returns
fiat_not_allowed. - Pre-funded test balances — Sandbox accounts start with 10 BTC, 100 ETH, 500 SOL, 1M USDT, 1M USDC, and 1M in your registered fiat.
- Sandbox responses — All sandbox API responses include
"sandbox": truefor easy identification.
2026-04-14 — v1.3.0 (Email Confirmation for Sensitive Operations)
- Webhook create requires email confirmation —
POST /v1/webhooksnow returns202withpending_confirmation. Webhook is created after email confirmation. Signing secret delivered via separate email. - Webhook delete requires email confirmation —
DELETE /v1/webhooks/{uuid}now returns202. Webhook deleted after email confirmation. - Address revocation requires email confirmation —
DELETE /v1/crypto/withdrawal-addresses/{uuid}now returns202. Address revoked after email confirmation. - All confirmation emails use branded Mintarex templates with anti-phishing code, IP address logging, and 24-hour expiry
- API keys can only be created and deleted from the dashboard (not via API) — unchanged, confirmed by design
2026-04-14 — v1.2.0 (Security Hardening)
- Concurrent quote limit: maximum 5 active quotes per API key (prevents quote-holding abuse)
- Address format validation: withdrawal addresses are validated against blockchain-native checksums for 100+ networks
- Memo/tag warnings: adding addresses on memo-required networks (XRP, XLM, Stellar) without a memo returns a
warningfield - Strict amount typing:
amountmust be a JSON string (integers and arrays rejected), no leading zeros, max 18 decimal places - Content-Type enforcement:
application/jsonrequired on all POST/PUT/PATCH requests (415 on mismatch) - Unknown field rejection: all POST endpoints reject unrecognized body fields with 400
- Webhook URL hardening: credentials, unicode domains, URL shorteners, redirect services, and oversized URLs rejected
- Sort parameter validation: only
ascanddescaccepted (400 on invalid values) - Idempotency key sanitization: control characters stripped automatically
- Internal fields stripped from
/v1/account/limitsresponse - New error codes:
too_many_active_quotes(429),invalid_address(400),unsupported_media_type(415)
2026-04-13 — v1.1.0
- Crypto-to-crypto swaps: RFQ now supports crypto/crypto pairs (e.g. ETH/BTC) with
from_networkandto_networkparameters - Transaction limits enforced on API trades (same daily/monthly limits as dashboard)
- API key prefix changed from
mx_live_tomxn_live_(33 characters total) - Simplified accept flow: only
idempotency_keyrequired in request body (no echo-back fields) - Dashboard Developer page for self-service key management with MFA protection
- Webhook management via dashboard UI
- Fixed error response format documentation (flat
error+message, not nested) - Fixed webhook event types to match actual implementation
- Added comprehensive trading error codes to documentation
2026-04-09 — v1.0.0 (Initial Release)
- Account endpoints: balances, single balance, fees, limits
- RFQ trading: quote with all-in spread pricing + accept flow
- Trade history: paginated list + single trade detail
- Public reference data: instruments (4,066 pairs), networks, fee schedule
- Crypto wallet: deposit addresses, deposits, withdrawals with address whitelisting
- HMAC-SHA256 authentication with nonce + timestamp replay protection
- AES-256-GCM encrypted API key storage with HKDF key derivation
- All-in pricing model: OTC spread baked into quoted price, no separate fees
- Price feed via Varnish cache (same source as dashboard Markets page)
- Webhook delivery with HMAC signature verification
- SSE streaming for prices and account events
- IETF-standard rate limit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset)
- Full request audit logging to api_request_log table
- Code samples in 7 languages: Node.js, Python, Go, Java, C#, PHP, cURL
Support
Need help integrating? We're here to help.
- Email: [email protected]
- API Status: institutional.mintarex.com/health
- Website: mintarex.com