Accept your first crypto payment
Get API keysEvery request must be signed with your Secret Key. Create API keys in Stores → Manage. 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.
| Header | Required | Description |
|---|---|---|
| X-API-Key | yes | Your public key (Client ID), e.g. pk_… |
| X-Signature | yes | Hex HMAC-SHA256 of the raw body signed with your secret key |
| Idempotency-Key | recommended | Unique string; retries with the same key do not duplicate invoices |
| Content-Type | for POST | application/json |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/currencies | List crypto currencies enabled for your store |
| POST | /api/v1/invoices | Create a payment invoice |
| GET | /api/v1/invoices/{id} | Fetch an invoice by its invoice_id or UUID |
| GET | /api/v1/webhooks | List webhook deliveries (cursor-paginated) |
| POST | /api/v1/webhooks/{id}/replay | Requeue a failed delivery for retry |
| POST | /api/v1/webhooks/secret/rotate | Rotate the webhook signing secret |
| GET | /api/v1/ledger | Ledger audit trail (credits/debits, fees) |
| GET | /api/v1/payouts | Payout history |
| POST | /api/v1/payouts/withdraw | Withdraw your settled balance to your wallet |
| GET | /api/v1/statements | Reconciliation statement (balances + period activity) |
| GET | /health | Service health (public) |
The full machine-readable reference covers every endpoint, schema, error and header — including authentication, cursor pagination, idempotency and the versioning policy.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| order_id | string ≤190 | yes | Your order reference |
| price_fiat | decimal(30,8) | yes | Amount in fiat |
| fiat_currency | ISO-4217 | yes | e.g. USD, EUR |
| crypto_currency | string | no | Request a specific crypto (e.g. BTC); otherwise the store default is used |
| callback_url | URL | yes | Where webhook events are delivered |
| return_url | URL | no | Where 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
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.
Returns the crypto currencies enabled for your store: code, network,
name, decimals. Configure them under
Stores → Manage → Accepted currencies.
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 use a consistent shape:
{"error":{"code":"unauthorized","message":"Invalid API key","details":[]}}
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed body or invalid field |
| 401 | unauthorized | Missing/invalid key or signature |
| 403 | forbidden | Merchant or store is suspended |
| 404 | not_found | Resource not found |
| 409 | no_currencies | Store has no enabled crypto currencies |
| 422 | idempotency_conflict | Same Idempotency-Key with a different body |
| 429 | rate_limited | Too many requests |
| 503 | pricing_unavailable | No exchange rate available |
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;
}