API Documentation
K
Mintarex Institutional API

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.

Institutional access only. API keys are available for corporate accounts with approved KYC. Contact [email protected] to get started.

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

TypeCurrencies
FiatUSD, EUR, GBP, CAD, AED, INR
CryptoBTC, ETH, USDT, USDC, SOL, XRP, ADA, DOGE, LTC, MATIC, TON, and 500+ more
Networks90+ blockchain networks (EVM chains + Bitcoin, Solana, Cardano, XRP, TON, Sui, Aptos, etc.)
Important: Network identifiers use short keys (e.g. 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:

CoinNetwork KeyChain Name
BTCbtcBitcoin
ETHethEthereum (ERC-20)
ETHarbArbitrum One
ETHbaseBase
ETHopOptimism
ETHmaticPolygon
USDTethEthereum (ERC-20)
USDTarbArbitrum One
USDTsolSolana (SPL)
USDTarbArbitrum One
USDCethEthereum (ERC-20)
USDCsolSolana (SPL)
USDCbaseBase
SOLsolSolana
XRPxrpXRP Ledger
LTCltcLitecoin
DOGEdogeDogecoin
ADAadaCardano
TONtonThe Open Network
MATICmaticPolygon

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:

HeaderDescription
MX-API-KEYYour public API key (e.g. mxn_live_a8f3b2c1d4e5f6a7b8c9d0e1)
MX-SIGNATUREHMAC-SHA256 signature of the request (64 hex characters)
MX-TIMESTAMPCurrent Unix timestamp in seconds
MX-NONCEUnique UUID v4 per request (replay protection)
Timestamp window: Requests with a timestamp more than 30 seconds from server time will be rejected. Ensure your clock is NTP-synchronized.

Optional headers

HeaderDescription
MX-IDEMPOTENCY-KEYUUID v4 for safe retries on POST requests. Same key returns cached response within 24h.

Generating the API Key

  1. Log in to the Mintarex Dashboard with a corporate account
  2. Complete corporate KYC verification (must be approved)
  3. Navigate to Settings → API Keys
  4. Click Create API Key
  5. Select your desired permissions (scopes)
  6. Add at least one IP address to the whitelist (required for production keys)
  7. Your API key and secret will be displayed once — save the secret immediately
Save your secret! The API secret is shown only once at creation time. It cannot be recovered. If lost, create a new key.

Key format

API Key:    mxn_live_a8f3b2c1d4e5f6a7b8c9d0e1   (33 characters, public)
API Secret: YWJjZGVmZ2hpamtsbW5vcH...          (64 characters, secret — save immediately)

Permission scopes

ScopeDescription
read:accountView account info, fees, limits
read:walletView balances, deposit addresses
read:tradeView trade history
read:marketView market data and instruments
trade:quoteRequest RFQ quotes
trade:executeExecute trades (accept quotes)
withdraw:cryptoSubmit crypto withdrawals (requires pre-whitelisted addresses)
webhook:manageCreate/manage webhook endpoints
stream:marketSubscribe to price streams (SSE)
stream:accountSubscribe 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
ComponentDescriptionExample
METHODHTTP method (uppercase)GET
PATHFull path including query string/v1/account/balances?currency_type=crypto
TIMESTAMPUnix timestamp in seconds1712582345
NONCEUUID v4 (unique per request)550e8400-e29b-41d4-a716-446655440000
BODY_HASHSHA-256 hex of request bodye3b0c44298fc1c14... (empty body hash for GET)
For GET/DELETE requests: Use SHA-256 hash of empty string: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Step 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.

LanguagePackageSourceStatus
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,
});
Requires Node.js 18.17+. Tested on 20, 22, and 24 LTS. Zero runtime dependencies — uses native 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"],
)
Requires Python 3.11+. Fully typed (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"),
})
Requires Go 1.24+. Single runtime dependency (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.

BucketRequests /min
Read300
Write100
Quote60
Execute30
Withdraw15

Endpoint classification

BucketEndpoints
ReadAll GET requests (balances, history, fees, limits)
WritePOST/DELETE requests not in other buckets (webhooks, address management)
QuotePOST /v1/rfq — requesting a quote
ExecutePOST /v1/rfq/{id}/accept — executing a trade
WithdrawPOST /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:

HeaderDescription
RateLimit-LimitMaximum requests allowed in the current window
RateLimit-RemainingRequests remaining in the current window
RateLimit-ResetSeconds until the window resets
Retry-AfterSeconds 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

StatusMeaning
200Success
202Accepted — operation requires email confirmation (webhook create/delete, address revoke)
400Bad request (invalid parameters)
401Authentication failed
403Forbidden (IP blocked, insufficient scope, feature not enabled)
404Resource not found
413Request body too large (max 100KB)
422Unprocessable (e.g. idempotency key reused with different body)
429Rate limit exceeded
500Internal server error
502Upstream service unavailable
503Service temporarily unavailable

Authentication error codes

CodeDescription
missing_auth_headersRequired headers missing (MX-API-KEY, MX-SIGNATURE, MX-TIMESTAMP, MX-NONCE)
invalid_key_formatKey doesn't match mxn_live_ or mxn_test_ + 24 hex chars
invalid_keyAPI key not found
key_disabledKey disabled by owner
key_suspendedKey suspended by administrator
key_revokedKey permanently revoked
key_expiredKey past expiry date
key_auto_disabledKey temporarily locked due to repeated auth failures (auto-unlocks after 5 minutes)
timestamp_skewTimestamp outside 30-second window
nonce_replayNonce already used (possible replay attack)
invalid_signatureHMAC signature verification failed
ip_not_whitelistedClient IP not in key's whitelist
insufficient_scopeKey lacks required permission scope
trading_not_enabledTrading not enabled for this key (contact support)
withdrawals_not_enabledWithdrawals not enabled for this key (contact support)
rate_limit_exceededToo many requests — check RateLimit-* headers

Trading error codes

CodeHTTPDescription
invalid_parameter400Invalid or missing request parameter
same_currency400Base and quote currencies must be different
unsupported_pair400One or more currencies not supported
fiat_disabled400Fiat currency temporarily disabled for new trades
no_network400Could not determine network — specify explicitly
invalid_network400Network not supported for this currency
price_unavailable400Unable to get market price — try again
amount_too_small400Trade amount too small (after spread)
amount_too_large400Amount exceeds maximum allowed
min_amount400Below minimum trade amount for this currency
max_amount400Above maximum trade amount for this currency
daily_limit400Daily trading limit reached
monthly_limit400Monthly trading limit reached
insufficient_balance400Not enough funds to execute trade
quote_expired_or_not_found410Quote has expired or does not exist
quote_already_consumed409Quote has already been executed
idempotency_key_conflict409Idempotency key used with a different quote
too_many_active_quotes429Maximum 5 concurrent active quotes per key — wait for existing quotes to expire
per_key_concurrency_limit429Too many concurrent trade executions per API key
invalid_address400Address format invalid for the specified network
unsupported_media_type415Content-Type must be application/json
service_unavailable503Temporary 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
Best practice: Always use idempotency keys for trade execution and withdrawal requests. If your connection drops mid-request, you can safely retry without risk of duplicate execution.

Get Balances

GET /v1/account/balances

Returns all wallet balances. Requires read:wallet scope.

Query parameters

ParameterTypeRequiredDescription
currency_typestringNofiat or crypto
include_emptybooleanNoInclude 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.

Zero slippage. Once quoted, the price is locked. You get exactly the quoted price when you accept.
All-in pricing. The quoted price includes our OTC spread. There are no separate fees or hidden charges. The price you see is the price you pay.
Crypto-to-crypto swaps supported. Both crypto/fiat pairs (e.g. BTC/USD) and crypto/crypto pairs (e.g. ETH/BTC) are supported. For swaps, specify 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" }
FieldTypeRequiredDescription
basestringYesBase currency — must be crypto (BTC, ETH, SOL, etc.)
quotestringYesQuote currency — fiat (USD, EUR) or crypto for swaps (BTC, USDT)
sidestringYesbuy (receive base, pay quote) or sell (pay base, receive quote)
amountstringYesTrade 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_typestringYesbase or quote — which currency the amount is denominated in
networkstringNoNetwork for the crypto side (e.g. eth, btc, sol). Auto-detected if omitted.
from_networkstringNoSource network for crypto-to-crypto swaps (base currency network). Auto-detected if omitted.
to_networkstringNoDestination 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

FieldTypeDescription
quote_idstringUnique quote identifier (UUID)
networkstringNetwork identifier for the primary crypto currency
pricestringExecutable price with spread included (all-in, no separate fee)
base_amountstringAmount of base currency in the trade
quote_amountstringAmount of quote currency in the trade
expires_atstringISO 8601 timestamp when the quote expires
expires_in_msintegerMilliseconds until expiry (30000)
is_swapbooleanPresent only for crypto-to-crypto swaps
from_networkstringBase currency network (swaps only)
to_networkstringQuote currency network (swaps only)
Note: The 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" }
FieldTypeRequiredDescription
idempotency_keystringYesUUID v4 for safe retries (max 64 chars). Same key returns same result.
Simplified accept. You do not need to echo back price, amounts, or signatures. The server stores the authoritative quote data — just pass the quote_id in the URL and an idempotency key in the body.

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

ParameterTypeDescription
basestringFilter by base currency (e.g. BTC)
quotestringFilter by quote currency (e.g. USD)
sidestringFilter by side (buy or sell)
limitintegerResults per page (default 50, max 200)
offsetintegerResults to skip
fromstringStart date (ISO 8601)
tostringEnd 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

ParameterTypeRequiredDescription
coinstringYesCoin symbol (e.g. BTC, ETH, USDT)
networkstringNoNetwork 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/Tag: Some networks (XRP, XLM, ATOM, HBAR) require a memo or tag in addition to the address. Check the 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

ParameterTypeRequiredDescription
coinstringNoFilter by coin (e.g. BTC)
statusstringNodetected, pending_confirmations, confirming, crediting, completed, failed
fromISO 8601NoStart date
toISO 8601NoEnd date
limitintegerNo1-200, default 50
offsetintegerNoDefault 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

FieldTypeRequiredDescription
coinstringYesCoin symbol
networkstringYesNetwork identifier
amountstringYesPlain decimal string (e.g. "0.1"). No scientific notation, no leading zeros, max 18 decimal places. Must be a string type — integers rejected.
addressstringYesMust be in the active withdrawal allowlist. Address format is validated against the network's native checksum.
address_tagstringNoMemo/tag (max 100 chars, for chains that require it)
idempotency_keystringYesUnique 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."
}
Address whitelisting required. Withdrawals can only be sent to pre-whitelisted addresses. New addresses require email confirmation + a 24-hour cooling period before first use. See Add Allowlist Address.

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.

Address validation: Addresses are validated against the blockchain's native format (checksum, prefix, length) for the specified network. Invalid addresses are rejected with 400 invalid_address. Use GET /v1/networks?coin=BTC to find valid network identifiers.
Memo/tag networks: For XRP, XLM, and Stellar, the 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."
}
Flow: Add address → Check email → Click confirm link → 24h cooling period → Address becomes usable for withdrawals.

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."
}
Two-step flow: After calling this endpoint, check your email and click the confirmation link. The address will be revoked only after email confirmation. This prevents unauthorized revocation if an API key is compromised.

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@host rejected)
  • 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

  1. Call POST /v1/webhooks with your URL and event types — returns 202 with pending_confirmation
  2. Check your email and click the confirmation link
  3. The webhook is created and the signing secret is sent to your email — save it immediately
  4. Mintarex signs each delivery with HMAC-SHA256 using the signing secret
  5. Your server verifies the X-Mintarex-Signature header and responds with 200 OK within 10 seconds
  6. Failed deliveries retry with exponential backoff (30s, 1m, 2m, 4m, 8m, 16m, 32m, 64m — max 8 attempts)
Email confirmation required. Webhook creation, deletion, and withdrawal address revocation all require email confirmation. This prevents unauthorized operations if an API key is compromised. The signing secret is delivered via email after confirmation — it is never shown in the API response or on the confirmation page.

Webhook delivery headers

HeaderDescription
X-Mintarex-SignatureSignature in the form v1=<64-hex> — HMAC-SHA256 of {timestamp}.{rawBody} using your webhook signing secret
X-Mintarex-TimestampUnix timestamp (seconds) of the delivery — used in the signed payload
X-Mintarex-Event-TypeEvent type (e.g. trade.executed)
X-Mintarex-Event-IdUnique event identifier (the same ID is sent on every retry of the same event — use for idempotent processing)
X-Mintarex-Delivery-IdUnique 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 flag: Events originating from the sandbox environment include "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

EventDescription
trade.executedRFQ trade was filled successfully
deposit.detectedCrypto deposit seen on-chain (unconfirmed)
deposit.confirmedDeposit reached required confirmations and credited to balance
withdrawal.requestedCrypto withdrawal submitted and pending admin approval
withdrawal.approvedWithdrawal approved by admin, queued for broadcast
withdrawal.completedWithdrawal confirmed on-chain
withdrawal.cancelledWithdrawal 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:

  1. Get a stream tokenPOST /v1/stream/token (authenticated via HMAC like any other endpoint)
  2. 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.

ParameterTypeDescription
tokenstringStream token from Step 1
instrumentsstringComma-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 filled
  • deposit.detected — deposit seen on-chain
  • deposit.confirmed — deposit credited to balance
  • withdrawal.requested — withdrawal submitted
  • withdrawal.approved — withdrawal approved by admin
  • withdrawal.completed — withdrawal confirmed on-chain
  • withdrawal.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

  1. 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_.
  2. Use the same base URLhttps://institutional.mintarex.com. No separate sandbox URL.
  3. Authenticate identically — HMAC-SHA256 signatures, nonce replay protection, IP whitelisting — all enforced the same as production.
  4. Test your integration — Request quotes, execute trades, manage withdrawal addresses, register webhooks. All responses include "sandbox": true.

What's Different in Sandbox

FeatureProductionSandbox
API Key prefixmxn_live_mxn_test_
BalancesReal wallet balancesPre-funded test balances (10 BTC, 100 ETH, 1M in your registered fiat, etc.)
RFQ QuotesReal market prices with OTC spreadSame real market prices, same spread calculation
Trade ExecutionReal wallet debits/credits, ledger entriesSimulated — no wallet changes, no ledger entries. Trade recorded with is_sandbox = 1
Crypto WithdrawalsReal fund lock, admin approval, on-chain broadcastSimulated — instant completed status, no real funds locked
Deposit AddressesReal on-chain addressesClearly fake test addresses (e.g. 0x...C0FFEE)
DepositsDetected by on-chain scannersNot applicable — returns empty list
Withdrawal AddressesIsolated to productionIsolated to sandbox — separate allowlist
WebhooksIsolated to productionIsolated to sandbox — separate endpoints
Transaction LimitsEnforced (daily/monthly caps)Skipped — trade freely without hitting limits
Trade HistoryShows only production tradesShows only sandbox trades
SSE StreamsReal price data + account eventsSame real price data, account events include sandbox: true
Fiat CurrencyYour registered fiat onlySame — 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.

Tip: Use the Sandbox Mode toggle on your Developer Settings page to switch between viewing sandbox and production keys, activity, and data.

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": true for easy identification.

2026-04-14 — v1.3.0 (Email Confirmation for Sensitive Operations)

  • Webhook create requires email confirmationPOST /v1/webhooks now returns 202 with pending_confirmation. Webhook is created after email confirmation. Signing secret delivered via separate email.
  • Webhook delete requires email confirmationDELETE /v1/webhooks/{uuid} now returns 202. Webhook deleted after email confirmation.
  • Address revocation requires email confirmationDELETE /v1/crypto/withdrawal-addresses/{uuid} now returns 202. 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 warning field
  • Strict amount typing: amount must be a JSON string (integers and arrays rejected), no leading zeros, max 18 decimal places
  • Content-Type enforcement: application/json required 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 asc and desc accepted (400 on invalid values)
  • Idempotency key sanitization: control characters stripped automatically
  • Internal fields stripped from /v1/account/limits response
  • 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_network and to_network parameters
  • Transaction limits enforced on API trades (same daily/monthly limits as dashboard)
  • API key prefix changed from mx_live_ to mxn_live_ (33 characters total)
  • Simplified accept flow: only idempotency_key required 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.

Response time: We typically respond to integration support requests within 4 business hours.