POST /v1/addresses
{ "external_user_id": "user_60" }
Create an address
One per customer, made the first time they open your wallet page, and theirs for good.
Give every customer their own deposit address, get credited at 15 confirmations, and withdraw whenever you like. One fee: 0.5% per deposit.
No memo fields, no matching payments by amount, no manual reconciliation.
POST /v1/addresses
{ "external_user_id": "user_60" }
One per customer, made the first time they open your wallet page, and theirs for good.
You see the payment within seconds and can show its confirmations counting up.
→ deposit.confirmed X-Signature: t=…,v1=…
At 15 confirmations your balance moves and a signed webhook reaches your server.
Built like a ledger, not a wallet: every movement is a double entry, and nothing is edited after the fact.
Seen in seconds as detected; credited at 15 confirmations. A reorganised block reverses the credit by itself.
Created on demand, reused forever, and free to create. Every payment arrives already labelled.
HMAC-signed with a timestamp, retried with backoff until your server answers 2xx.
An independent pass compares what is on chain with what we owe every merchant.
Money only leaves to addresses you registered, and a new one waits 48 hours first.
A key that creates addresses cannot move money. Payouts need a key that says so.
Show the address, a QR code and the one warning that matters. The confirmations count up on their own; your customer never has to ask support "did it arrive?".
Send only USDT on BNB Smart Chain (BEP-20). Other networks or tokens will be lost. Minimum 1 USDT.
Pick a step, pick your language. Every example is ready to paste into your site.
curl https://api.wirnora.com/v1/addresses \ -H "Authorization: Bearer $WIRNORA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: address:user_60" \ -d '{"external_user_id": "user_60"}'
$ch = curl_init('https://api.wirnora.com/v1/addresses');curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('WIRNORA_API_KEY'), 'Content-Type: application/json', 'Idempotency-Key: address:' . $userId, ], CURLOPT_POSTFIELDS => json_encode(['external_user_id' => $userId]),]);$address = json_decode(curl_exec($ch), true)['data']['address'];
const res = await fetch('https://api.wirnora.com/v1/addresses', { method: 'POST', headers: { Authorization: `Bearer ${process.env.WIRNORA_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': `address:${userId}`, }, body: JSON.stringify({ external_user_id: userId }),});const { data } = await res.json();showDepositAddress(data.address);
import os, requestsr = requests.post( "https://api.wirnora.com/v1/addresses", headers={ "Authorization": f"Bearer {os.environ['WIRNORA_API_KEY']}", "Idempotency-Key": f"address:{user_id}", }, json={"external_user_id": user_id}, timeout=10,)address = r.json()["data"]["address"]
body, _ := json.Marshal(map[string]string{"external_user_id": userID})req, _ := http.NewRequest("POST", "https://api.wirnora.com/v1/addresses", bytes.NewReader(body))req.Header.Set("Authorization", "Bearer "+os.Getenv("WIRNORA_API_KEY"))req.Header.Set("Content-Type", "application/json")req.Header.Set("Idempotency-Key", "address:"+userID)res, err := http.DefaultClient.Do(req)if err != nil { return err}defer res.Body.Close()
# t and v1 come from the X-Signature header,# body is the raw request bytes, before any JSON parsingprintf '%s.%s' "$t" "$body" \ | openssl dgst -sha256 -hmac "$WIRNORA_WEBHOOK_SECRET" -hex \ | awk '{print $2}' # must equal one of the v1 values
$raw = file_get_contents('php://input');$t = null; $sigs = [];foreach (explode(',', $_SERVER['HTTP_X_SIGNATURE'] ?? '') as $part) { [$k, $v] = array_pad(explode('=', $part, 2), 2, ''); if ($k === 't') $t = $v; elseif ($k === 'v1') $sigs[] = $v;}$expected = hash_hmac('sha256', $t . '.' . $raw, getenv('WIRNORA_WEBHOOK_SECRET'));$fresh = abs(time() - (int) $t) <= 300;$valid = (bool) array_filter($sigs, fn ($s) => hash_equals($expected, $s));http_response_code($fresh && $valid ? 200 : 400);
import crypto from 'node:crypto';app.post('/webhooks/wirnora', express.raw({ type: '*/*' }), (req, res) => { const parts = req.get('X-Signature').split(',').map((p) => p.split('=')); const t = parts.find(([k]) => k === 't')?.[1]; const expected = crypto.createHmac('sha256', process.env.WIRNORA_WEBHOOK_SECRET) .update(`${t}.${req.body}`).digest('hex'); const fresh = Math.abs(Date.now() / 1000 - Number(t)) <= 300; const valid = parts.some(([k, v]) => k === 'v1' && v.length === expected.length && crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected))); res.sendStatus(fresh && valid ? 200 : 400);});
import hashlib, hmac, os, timefrom flask import abort, request@app.post("/webhooks/wirnora")def wirnora(): raw = request.get_data() parts = [p.split("=", 1) for p in request.headers["X-Signature"].split(",")] t = next(v for k, v in parts if k == "t") expected = hmac.new(os.environ["WIRNORA_WEBHOOK_SECRET"].encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest() if abs(time.time() - int(t)) > 300 or not any( k == "v1" and hmac.compare_digest(v, expected) for k, v in parts): abort(400) return "", 200
func wirnora(w http.ResponseWriter, r *http.Request) { raw, _ := io.ReadAll(r.Body) var t string var sigs []string for _, p := range strings.Split(r.Header.Get("X-Signature"), ",") { k, v, _ := strings.Cut(p, "=") if k == "t" { t = v } else if k == "v1" { sigs = append(sigs, v) } } mac := hmac.New(sha256.New, []byte(os.Getenv("WIRNORA_WEBHOOK_SECRET"))) mac.Write([]byte(t + "." + string(raw))) expected := hex.EncodeToString(mac.Sum(nil)) ts, _ := strconv.ParseInt(t, 10, 64) valid := false for _, s := range sigs { valid = valid || hmac.Equal([]byte(s), []byte(expected)) } if !valid || math.Abs(float64(time.Now().Unix()-ts)) > 300 { w.WriteHeader(http.StatusBadRequest) return } w.WriteHeader(http.StatusOK)}
curl "https://api.wirnora.com/v1/deposits?status=confirmed&limit=50&offset=0" \ -H "Authorization: Bearer $WIRNORA_API_KEY"
$offset = 0;do { $ch = curl_init("https://api.wirnora.com/v1/deposits?status=confirmed&limit=50&offset=$offset"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('WIRNORA_API_KEY')], ]); $page = json_decode(curl_exec($ch), true); foreach ($page['data'] as $d) creditOnce($d['id'], $d['net_amount']); $offset += $page['meta']['count'];} while ($page['meta']['has_more']);
let offset = 0;let page;do { const res = await fetch( `https://api.wirnora.com/v1/deposits?status=confirmed&limit=50&offset=${offset}`, { headers: { Authorization: `Bearer ${process.env.WIRNORA_API_KEY}` } }, ); page = await res.json(); for (const d of page.data) await creditOnce(d.id, d.net_amount); offset += page.meta.count;} while (page.meta.has_more);
offset = 0while True: page = requests.get( "https://api.wirnora.com/v1/deposits", params={"status": "confirmed", "limit": 50, "offset": offset}, headers={"Authorization": f"Bearer {os.environ['WIRNORA_API_KEY']}"}, timeout=10, ).json() for d in page["data"]: credit_once(d["id"], d["net_amount"]) offset += page["meta"]["count"] if not page["meta"]["has_more"]: break
for offset := 0; ; { url := fmt.Sprintf("https://api.wirnora.com/v1/deposits?status=confirmed&limit=50&offset=%d", offset) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("WIRNORA_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { return err } var page DepositPage json.NewDecoder(res.Body).Decode(&page) res.Body.Close() for _, d := range page.Data { creditOnce(d.ID, d.NetAmount) } offset += page.Meta.Count if !page.Meta.HasMore { break }}
{
"data": {
"id": "a3f1c2d8-…",
"external_user_id": "user_60",
"address": "0x8dfe…1426",
"asset": "USDT",
"required_confirmations": 15
}
}
# Answer 2xx quickly; retries happen otherwise.
# Deduplicate on X-Event-Id, then read the deposit itself:
GET /v1/deposits/{id} → credit once, when status = "confirmed"
{
"data": [{ "id": "7c0e…", "status": "confirmed", "confirmations": 15,
"amount": "100000000000000000000", "fee": "500000000000000000",
"net_amount": "99500000000000000000" }],
"meta": { "limit": 50, "offset": 0, "count": 1, "has_more": false }
}
USDT on BNB Smart Chain is live today. Ethereum, TRON and TON come next, with their own coins and stablecoins, then Bitcoin and more. Each one is held to the same checks as the first.
Ethereum, TRON and TON first, then the rest. Each opens only after it passes the same solvency and reorg checks.
Today, USDT on BNB Smart Chain (BEP-20) only. Anything sent on another network, or any other token, is not credited. Ethereum, TRON and TON come next, with their native coins and stablecoins, then Bitcoin and other coins; see the roadmap above.
At 15 confirmations, usually well under a minute. Before that it shows as detected, with its confirmations counting up.
Email support@wirnora.com with your business name and website. We create your account and send you a sign-in link. Self-serve sign-up is on the roadmap.
No business verification is required to start.
1 USDT. Smaller transfers are ignored, so tell your customers.
Register a payout address once; after 48 hours you can withdraw to it at any time. There is no fee from us, only the network fee.
No monthly fee · 0.5% per deposit · free withdrawals