API Docs

Accept your first crypto payment

Get API keys

It takes three steps

1. Create a store and generate a key pair (Client ID + Secret Key). 2. POST an invoice — you get a hosted checkout_url to send your customer to. 3. Fulfill the order when the signed invoice.finished webhook lands on your server.

Show me the code
Authentication

Every request must be signed with your Secret Key. Create API keys in StoresManage. The public key is your Client ID; the secret key is shown only once when generated.

Signature = HMAC-SHA256(secret_key, raw_request_body) — always sign the exact bytes you send. Never ship the secret key to a browser.

HeaderRequiredDescription
X-API-KeyyesYour public key (Client ID), e.g. pk_…
X-SignatureyesHex HMAC-SHA256 of the raw body signed with your secret key
Idempotency-KeyrecommendedUnique string; retries with the same key do not duplicate invoices
Content-Typefor POSTapplication/json
Endpoints
MethodPathDescription
GET/api/v1/currenciesList crypto currencies enabled for your store
POST/api/v1/invoicesCreate a payment invoice
GET/api/v1/invoices/{id}Fetch an invoice by its invoice_id or UUID
GET/api/v1/webhooksList webhook deliveries (cursor-paginated)
POST/api/v1/webhooks/{id}/replayRequeue a failed delivery for retry
POST/api/v1/webhooks/secret/rotateRotate the webhook signing secret
GET/api/v1/ledgerLedger audit trail (credits/debits, fees)
GET/api/v1/payoutsPayout history
POST/api/v1/payouts/withdrawWithdraw your settled balance to your wallet
GET/api/v1/statementsReconciliation statement (balances + period activity)
GET/healthService health (public)
Reference & tooling

The full machine-readable reference covers every endpoint, schema, error and header — including authentication, cursor pagination, idempotency and the versioning policy.

POST /api/v1/invoices

Request body

FieldTypeRequiredDescription
order_idstring ≤190yesYour order reference
price_fiatdecimal(30,8)yesAmount in fiat
fiat_currencyISO-4217yese.g. USD, EUR
crypto_currencystringnoRequest a specific crypto (e.g. BTC); otherwise the store default is used
callback_urlURLyesWhere webhook events are delivered
return_urlURLnoWhere the payer is redirected after payment
BODY='{"order_id":"order_1234","price_fiat":"99.50","fiat_currency":"USD","callback_url":"https://shop.example.com/webhooks/minigateway"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "sk_your_secret_key" | awk '{print $2}')

curl -X POST https://ohyespay.com/api/v1/invoices \
  -H "X-API-Key: pk_your_public_key" \
  -H "X-Signature: $SIG" \
  -H "Idempotency-Key: order-1234" \
  -H "Content-Type: application/json" \
  -d "$BODY"
 'order_1234',
    'price_fiat'    => '99.50',
    'fiat_currency' => 'USD',
    'callback_url'  => 'https://shop.example.com/webhooks/minigateway',
], JSON_UNESCAPED_SLASHES);

$headers = [
    'X-API-Key: ' . $publicKey,                                    // pk_...
    'X-Signature: ' . hash_hmac('sha256', $body, $secretKey),      // sk_...
    'Idempotency-Key: order-1234',
    'Content-Type: application/json',
];

$ch = curl_init('https://ohyespay.com/api/v1/invoices');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $body,
    CURLOPT_HTTPHEADER     => $headers,
    CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($ch);
import crypto from 'node:crypto';

// Sign the RAW string body — never re-encode before signing.
const sign = (body, secret) =>
    crypto.createHmac('sha256', secret).update(body).digest('hex');

const body = JSON.stringify({
    order_id: 'order_1234',
    price_fiat: '99.50',
    fiat_currency: 'USD',
    callback_url: 'https://shop.example.com/webhooks/minigateway',
});

const res = await fetch('https://ohyespay.com/api/v1/invoices', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-API-Key': PUBLIC_KEY,                              // pk_...
        'X-Signature': sign(body, SECRET_KEY),                // sk_...
        'Idempotency-Key': crypto.randomUUID(),
    },
    body,
});

const invoice = await res.json();
console.log(invoice.checkout_url); // send the customer here
GET /api/v1/invoices/{id}

Pass either the short invoice_id (e.g. inv_1f) or the full uuid returned at creation. The id is scoped to your store.

Also useful for polling status client-side: statuses are pending, confirming, partially_paid, overpaid, finished, expired.

GET /api/v1/currencies

Returns the crypto currencies enabled for your store: code, network, name, decimals. Configure them under StoresManageAccepted currencies.

Webhooks

When an invoice changes state, a signed POST is sent to your callback_url. The body is the full invoice object plus an event field (invoice.created, invoice.confirming, invoice.finished, invoice.expired). Webhooks are signed with X-MiniGateway-Signature — the same HMAC-SHA256 scheme you use for API requests. Return HTTP 2xx fast; OhYesPay retries on non-2xx. Deduplicate by invoice_id + event.

{"event":"invoice.finished","invoice_id":"inv_1f","uuid":"...","status":"finished","price_fiat":"99.50","price_crypto":"0.0017313","address":"bc1q...","paid_amount":"0.0017313","paid_at":"2026-08-01T12:00:00+00:00"}
# MiniGateway signs every delivery with the SAME secret key.
SIG=$(printf '%s' "$RAW_BODY" | openssl dgst -sha256 -hmac "sk_your_secret_key" | awk '{print $2}')
test "$SIG" = "$HTTP_X_MINIGATEWAY_SIGNATURE" && echo "verified ✔"
$raw    = file_get_contents('php://input');
$sig    = $_SERVER['HTTP_X_MINIGATEWAY_SIGNATURE'] ?? '';
$expect = hash_hmac('sha256', $raw, $secretKey);

if (!hash_equals($expect, $sig)) {
    http_response_code(401);
    exit('invalid signature');
}
$payload = json_decode($raw, true);
import crypto from 'node:crypto';

const sign = (body, secret) =>
    crypto.createHmac('sha256', secret).update(body).digest('hex');

const raw = await readRequestBody(req);                       // Buffer
const sig = req.headers['x-minigateway-signature'];
if (sign(raw, SECRET_KEY) !== sig) {
    res.writeHead(401).end('invalid signature');
    return;
}
const payload = JSON.parse(raw.toString('utf8'));
Errors

Errors use a consistent shape:

{"error":{"code":"unauthorized","message":"Invalid API key","details":[]}}
HTTPcodeMeaning
400invalid_requestMalformed body or invalid field
401unauthorizedMissing/invalid key or signature
403forbiddenMerchant or store is suspended
404not_foundResource not found
409no_currenciesStore has no enabled crypto currencies
422idempotency_conflictSame Idempotency-Key with a different body
429rate_limitedToo many requests
503pricing_unavailableNo exchange rate available
Full examples

Ready-to-run projects live in the repository under examples/php-simple-checkout/ and examples/telegram-bot-nodejs/.

PHP — Simple Checkout (CLI)

Creates an invoice, prints the hosted checkout link. Works in any plain-PHP deployment — no framework required.

 'checkout-' . time(),
    'price_fiat'    => '49.99',
    'fiat_currency' => 'USD',
    'callback_url'  => 'https://your-store.example.com/webhooks/minigateway',
    'return_url'    => 'https://your-store.example.com/order/' . time(),
], JSON_UNESCAPED_SLASHES);

// Sign the RAW body with the Secret API Key. Never sign a re-encoded string.
$signature = hash_hmac('sha256', $body, $secretKey);

$ch = curl_init($baseUrl . '/api/v1/invoices');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $body,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'X-API-Key: ' . $apiKey,
        'X-Signature: ' . $signature,
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 20,
]);

$response = curl_exec($ch);
$invoice  = json_decode($response, true);

if (($invoice['checkout_url'] ?? null) !== null) {
    echo "Send your customer to:\n{$invoice['checkout_url']}\n";
    echo "\nInvoice id: {$invoice['invoice_id']}\n";
}

Node.js — Telegram bot (Telegraf)

A store channel on Telegram: customers pick a VIP tier from an inline keyboard, pay on the hosted checkout page, and the bot activates access when the invoice is paid.

{
  "name": "minigateway-telegram-bot",
  "version": "1.0.0",
  "type": "module",
  "main": "bot.js",
  "scripts": { "start": "node bot.js" },
  "engines": { "node": ">=18" },
  "dependencies": { "telegraf": "^4.16.3" }
}
// examples/telegram-bot-nodejs/bot.js  (excerpt)
import crypto from 'node:crypto';
import { Telegraf, Markup } from 'telegraf';

const bot = new Telegraf(process.env.BOT_TOKEN);

const sign = (body, secretKey) =>
    crypto.createHmac('sha256', secretKey).update(body).digest('hex');

async function createInvoice(orderId, priceFiat) {
    const body = JSON.stringify({
        order_id: orderId,
        price_fiat: priceFiat,
        fiat_currency: 'USD',
        callback_url: CALLBACK_URL,
    });
    const res = await fetch(`${BASE_URL}/api/v1/invoices`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-API-Key': PUBLIC_KEY,
            'X-Signature': sign(body, SECRET_KEY),
            'Idempotency-Key': crypto.randomUUID(),
        },
        body,
    });
    return res.json();
}

// /buy  →  inline keyboard with the price tiers
bot.command('buy', async (ctx) => {
    const tiers = [
        ['🥉 Bronze — 5 USDT', '5.00'],
        ['🥈 Silver — 15 USDT', '15.00'],
        ['🥇 Gold — 50 USDT', '50.00'],
    ];
    await ctx.reply('Choose a VIP tier:',
        Markup.inlineKeyboard(tiers.map(([label, amount]) =>
            [Markup.button.callback(label, `buy:${amount}`)]
        )),
    );
});

// tapping a tier creates an invoice and posts the hosted checkout link
bot.action(/^buy:(.+)$/, async (ctx) => {
    await ctx.answerCbQuery();
    const invoice = await createInvoice(`tg-${ctx.from.id}-${Date.now()}`, ctx.match[1]);
    await ctx.reply(`Pay ${ctx.match[1]} USD within 15 minutes.\n\n🔗 [Open checkout](${invoice.checkout_url})`,
        Markup.inlineKeyboard([Markup.button.callback('✅ I paid', 'check')]),
    );
});
// Webhook verification for the Telegram bot (point callback_url here)
export function verifyWebhook(rawBody, signatureHeader) {
    const expected = crypto.createHmac('sha256', SECRET_KEY)
        .update(rawBody).digest('hex');
    return signatureHeader === expected;
}