MoradaPay

Merchant guide

Everything you need to accept crypto with MoradaPay: setup, what your customers see, and how to connect your website.

How it works

MoradaPay is non-custodial: your customers pay straight into your own wallet. We never hold your money, so we can't freeze it, lose it or delay it. Our job is to show the customer exactly what to pay, watch the blockchain, and tell your website the moment a payment is confirmed.

  1. Your website (or you, by hand) creates an invoice for an amount in USD.
  2. The customer opens the checkout page, picks a coin and network, and pays from any wallet or exchange.
  3. We detect the payment on-chain, wait for enough confirmations, mark the invoice paid and send your website a signed webhook.
  4. You deliver the order. The money is already in your wallet.

Payments are in stablecoins (USDC / USDT), so $1 is 1 USDC and you carry no price risk.

Getting started (10 minutes)

  1. Sign up with email or Google and confirm your email.
  2. Turn on 2FA under Security (Google Authenticator, Authy…). Save the recovery codes somewhere safe.
  3. Create an account: choose Merchant for a shop (API + webhooks) or Personal for payment links only. You can have several, e.g. one per website.
  4. In Settings, paste your wallet address (starts with 0x). The same address works on every supported network.
  5. Tick the coins you accept. All are on by default.
  6. Merchant accounts: copy your API key (shown once) and set your webhook URL.
Use a wallet you control: MetaMask, Rabby, Trust Wallet, Ledger, a Safe… Not an exchange deposit address. Exchanges often don't credit tokens arriving on every network, and you can't refund from them.

Supported coins

NetworkCoinsConfirmations we wait for
EthereumUSDC, USDT6
BaseUSDC, USDT10
PolygonUSDC, USDT30
BNB ChainUSDC, USDT (18 decimals)15
ArbitrumUSDC, USDT20
OptimismUSDC, USDT10
AvalancheUSDC, USDT3

"USDT BEP20" is USDT on BNB Chain. "USDT ERC20" is USDT on Ethereum. Ethereum has the highest network fees for your customers, while Base, Polygon, Arbitrum, Optimism and BNB Chain cost a few cents. Confirmation usually takes from a few seconds to about two minutes.

What your customers see

  • The price in USD, then a choice of coin and network.
  • The exact amount to send (for example 25.004817 USDC), your address, a QR code, copy buttons and a one-click "Pay with browser wallet" button. The last digits make the payment recognisable, so it must be sent exactly.
  • A 30-minute timer. Payments arriving up to 24 hours late are still recognised.
  • A live status that switches to Payment received and, if you set one, sends them back to your website.

Getting paid without code

Invoices from the dashboard

On your account's Overview, enter an amount and description, then send the checkout link to your customer.

Payment links

Under Payment links, create a reusable link with a fixed price ("Logo design $150") or let the payer choose the amount (tips, donations). Share it anywhere. Each visitor gets their own invoice.

Notifications

You get an email for every payment, plus the full history under Invoices and Incoming payments, with CSV export for accounting.

Connect your website

  1. When the customer clicks "Pay with crypto", your server calls POST /api/invoices with the amount and your order id.
  2. Redirect the customer to the checkout_url in the response.
  3. When paid, we POST an invoice.paid webhook to your server. Check the signature, then mark the order paid.
  4. The customer comes back to your redirect_url. Show a thank-you page, but don't fulfil the order based on the redirect. Only the webhook, or a GET /api/invoices/{id} check, tells you it's paid.
Your API key must stay on your server. Never put it in browser JavaScript or a mobile app.

API reference

Base URL: https://moradapay.com. Send your API key on every request:

Authorization: Bearer sk_your_api_key

Interactive docs: /api/docs. Limit: 1,000 invoices per hour per account.

Create an invoice: POST /api/invoices

{
  "amount": "25.00",                         // USD, required, max 2 decimals
  "order_id": "1234",                        // optional, your reference
  "description": "2x T-shirt",               // optional, shown to the customer
  "redirect_url": "https://shop.com/thanks", // optional, after payment
  "coin": "bsc:USDT"                         // optional: skip the coin choice ("swap" = any token)
}

Response (also the data of every webhook):

{
  "id": "6f1c0a9e2b7d4c3a1e5f8b90",
  "status": "pending",
  "order_id": "1234",
  "price_usd": "25.00",
  "fee_usd": "0.13",              "fee_paid_by": "merchant",
  "total_usd": "25.00",           // what the customer pays in USD
  "coin": null,                   // e.g. "base:USDC" once chosen
  "amount": null,                 // exact amount to send, e.g. "25.004817"
  "received_amount": null,        // what actually arrived
  "remaining_amount": null,       // when underpaid
  "overpaid_amount": null,
  "pay_address": null,
  "tx_hash": null,
  "checkout_url": "https://pay.moradapay.com/pay/6f1c0a9e2b7d4c3a1e5f8b90",
  "created_at": "...", "expires_at": "...", "paid_at": null
}

Other endpoints

  • GET /api/invoices/{id}: one invoice.
  • GET /api/invoices?status=paid&limit=50: recent invoices.
  • GET /api/coins: the coins your account accepts.

Invoice statuses

statusmeaningwhat you do
pendingwaiting for paymentnothing yet
paidfully paid and confirmed (incl. small rounding within your tolerance)deliver the order
underpaidpart arrived; the customer can still send the restwait, or accept / refund in the dashboard
expirednothing paid in timecancel the order (a late payment within 24h still turns it paid)
refundedyou refunded an underpaymentcancel the order

Webhooks

Set your URL in Settings (must be https://). We send a JSON POST for these events:

  • invoice.paid: deliver the order.
  • invoice.underpaid, invoice.expired, invoice.refunded: for your records.
  • test: from the "Send test webhook" button.

Body: {"event": "invoice.paid", "data": { …the invoice… }}. Headers:

X-Gateway-Event:     invoice.paid
X-Gateway-Timestamp: 1767225600
X-Gateway-Signature: hex( HMAC_SHA256( webhook_secret, timestamp + "." + raw_body ) )
  • Always verify the signature with your webhook secret (in Settings), and reject timestamps older than 5 minutes.
  • Reply with any 2xx quickly. If not, we retry up to 8 times (after 1, 2, 4, 8… minutes). The log and a "Resend" button are under Webhooks.
  • The same event can arrive twice, so make your handler idempotent (only fulfil an order once).

Code examples

Create an invoice and redirect

<?php
$ch = curl_init("https://moradapay.com/api/invoices");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("GATEWAY_API_KEY"), "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode([
        "amount" => number_format($order->total, 2, ".", ""),
        "order_id" => (string) $order->id,
        "redirect_url" => "https://yourshop.com/thanks?order=" . $order->id,
    ]),
]);
$invoice = json_decode(curl_exec($ch), true);
header("Location: " . $invoice["checkout_url"]);

Webhook handler

<?php
$body = file_get_contents("php://input");
$ts   = $_SERVER["HTTP_X_GATEWAY_TIMESTAMP"] ?? "";
$sig  = $_SERVER["HTTP_X_GATEWAY_SIGNATURE"] ?? "";
$expected = hash_hmac("sha256", $ts . "." . $body, getenv("GATEWAY_WEBHOOK_SECRET"));
if (!hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) { http_response_code(401); exit; }

$event = json_decode($body, true);
if ($event["event"] === "invoice.paid") {
    markOrderPaid($event["data"]["order_id"], $event["data"]["id"]);  // only once per order
}
http_response_code(200);

Wrong amounts & refunds

  • Tiny shortfall (customer rounded 25.004817 to 25): counts as paid, within your tolerance (default 0.5%, set in Settings).
  • Underpaid: the invoice becomes underpaid, the checkout asks the customer for the rest, and you get an email. You can also accept it as paid or refund it from the invoice page.
  • Overpaid: the invoice is paid and flagged, and you can refund the extra.
  • Unmatched payments: if we can't tell which invoice a transfer is for (for example two similar invoices were open), it appears under Incoming payments and you assign it with one click.
  • Refunds are sent from your wallet, using the button on the invoice page or another wallet (then paste the transaction hash). We confirm them on-chain. Customers can leave a refund address on the checkout. Use it when they paid from an exchange.
There are no chargebacks in crypto: once paid, a payment can only be returned by you.

Fees

  • Platform fee: 0.5% per payment. In Settings you choose who pays it: you, or your customer (added to the checkout price).
  • Network ("gas") fees are paid by the customer when they send the payment.
  • On networks with automatic splitting, the fee is taken when the payment is forwarded to you, so there's nothing to pay. On other networks it adds up under Fees, and you pay it there in any coin. Accounts pause new invoices if unpaid fees get too high or too old.

Pay with any token

If you enable it in Settings, customers can pay with ETH, BNB, WBTC, POL, AVAX and hundreds of other tokens from 60+ networks. Their token is converted in the same transaction (via LI.FI), and you receive at least the full price in the stablecoin you chose ("receive as"). The customer pays the conversion costs. This needs a browser wallet such as MetaMask or Rabby.

Security checklist

  • Turn on 2FA and keep the recovery codes offline.
  • Keep the API key and webhook secret on your server only. If one leaks, generate a new one in Settings.
  • Verify every webhook signature, and fulfil orders only from invoice.paid (webhook or API), never from the redirect.
  • Double-check your wallet address after pasting it, since some malware swaps copied addresses.
  • Use a hardware wallet or Safe for larger amounts.

FAQ

The customer sent the right coin on the wrong network.

It still arrives in your wallet if your address exists there, which it does on all EVM networks, but we can't match it to the order automatically. You'll see it in your wallet and can refund it or deliver by hand.

Can I change my wallet address?

Yes, any time in Settings. New invoices use the new address. Open invoices keep the address they were created with.

How fast is it?

As soon as the network confirms: usually seconds on Base, Polygon, Arbitrum, Optimism, Avalanche and BNB Chain, and about a minute or two on Ethereum.

Do I need to hold ETH or BNB?

Not to receive. Only to send a refund, which needs a little of the network's coin for gas.

Is there a test mode?

Test networks are available on development installs only. On this live system everything uses real coins, so try a $1 invoice first.