Integration Guide

Everything you need to start accepting USDT and USDC payments on Ethereum and Tron. No KYC required.

Base URL: https://api.pay.kyc.rip — For testing use https://testnet.pay.kyc.rip

Getting Started

1

Register

Create a merchant account with a single API call. No email, no identity — just a key.

curl -X POST https://api.pay.kyc.rip/merchant/register

Save the merchant_key and webhook_secret from the response. They are shown only once.

2

Configure wallets

Set your ETH and/or Tron wallet addresses where you will receive payments.

curl -X PUT https://api.pay.kyc.rip/merchant/settings \
  -H "X-Merchant-Key: mk_your-key" \
  -H "Content-Type: application/json" \
  -d '{"eth_address":"0xYOUR_WALLET","tron_address":"TYOUR_WALLET"}'
3

Start accepting payments

Create invoices and redirect customers to the payment page. That's it.

Accept Payments

Create an invoice with POST /invoice/create. The chain field is optional — if omitted, the customer chooses on the payment page.

curl -X POST https://api.pay.kyc.rip/invoice/create \
  -H "X-Merchant-Key: mk_your-key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 25.00,
    "description": "Order #1234",
    "order_id": "ORD-1234",
    "expiry_minutes": 30
  }'
const res = await fetch('https://api.pay.kyc.rip/invoice/create', {
  method: 'POST',
  headers: {
    'X-Merchant-Key': 'mk_your-key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: 25.00,
    description: 'Order #1234',
    order_id: 'ORD-1234',
    expiry_minutes: 30,
  }),
});
const invoice = await res.json();
console.log(invoice.payment_url);
import requests

resp = requests.post(
    "https://api.pay.kyc.rip/invoice/create",
    headers={"X-Merchant-Key": "mk_your-key"},
    json={
        "amount": 25.00,
        "description": "Order #1234",
        "order_id": "ORD-1234",
        "expiry_minutes": 30,
    },
)
invoice = resp.json()
print(invoice["payment_url"])
$ch = curl_init('https://api.pay.kyc.rip/invoice/create');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'X-Merchant-Key: mk_your-key',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'amount'        => 25.00,
        'description'   => 'Order #1234',
        'order_id'      => 'ORD-1234',
        'expiry_minutes' => 30,
    ]),
]);
$invoice = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $invoice['payment_url'];
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

func main() {
    body, _ := json.Marshal(map[string]any{
        "amount":         25.00,
        "description":    "Order #1234",
        "order_id":       "ORD-1234",
        "expiry_minutes": 30,
    })
    req, _ := http.NewRequest("POST",
        "https://api.pay.kyc.rip/invoice/create",
        bytes.NewReader(body))
    req.Header.Set("X-Merchant-Key", "mk_your-key")
    req.Header.Set("Content-Type", "application/json")

    resp, _ := http.DefaultClient.Do(req)
    var invoice map[string]any
    json.NewDecoder(resp.Body).Decode(&invoice)
    fmt.Println(invoice["payment_url"])
}
using System.Net.Http;
using System.Text;
using System.Text.Json;

var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Merchant-Key", "mk_your-key");

var payload = JsonSerializer.Serialize(new {
    amount = 25.00,
    description = "Order #1234",
    order_id = "ORD-1234",
    expiry_minutes = 30
});
var content = new StringContent(payload, Encoding.UTF8, "application/json");

var res = await client.PostAsync("https://api.pay.kyc.rip/invoice/create", content);
var invoice = JsonSerializer.Deserialize<JsonElement>(await res.Content.ReadAsStringAsync());
Console.WriteLine(invoice.GetProperty("payment_url"));
import java.net.http.*;
import java.net.URI;

var body = """
    {"amount":25.00,"description":"Order #1234",
     "order_id":"ORD-1234","expiry_minutes":30}
    """;
var req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.pay.kyc.rip/invoice/create"))
    .header("X-Merchant-Key", "mk_your-key")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

var res = HttpClient.newHttpClient()
    .send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());

Response:

{
  "invoice_id": "d4e5f6...",
  "payment_url": "https://pay.kyc.rip/pay/d4e5f6...?network=mainnet",
  "adjusted_amount": "25.001",
  "order_id": "ORD-1234",
  "status": "pending",
  "expires_at": 1711001800
}

The adjusted_amount includes micro-cents for unique identification on-chain. The payment_url is a hosted payment page your customer can use.

Payment Page

Option A: Redirect

Send your customer to the payment_url returned in the invoice response.

// After creating the invoice
window.location.href = invoice.payment_url;

Add a return_url query parameter to redirect the customer back to your site after payment:

https://pay.kyc.rip/pay/INVOICE_ID?network=mainnet&return_url=https://yoursite.com/thanks

Option B: Embed as iframe

<iframe
  src="https://pay.kyc.rip/pay/INVOICE_ID?network=mainnet"
  width="420"
  height="680"
  style="border:none;border-radius:12px"
></iframe>
The payment page handles chain selection, QR codes, real-time status updates, and expiry countdown automatically.

Webhooks

Webhooks notify your server in real time when payment events occur. You can configure multiple endpoints with per-event filtering in the dashboard, or use a single webhook_url via the settings API.

Payload format

Each webhook is a POST request with these headers:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 hex digest of the body
X-Webhook-EventEvent name (e.g. invoice.confirmed)
X-Webhook-AttemptDelivery attempt number (1-5)
X-Webhook-TimestampUnix timestamp of this delivery attempt
Content-Typeapplication/json

Events:

EventWhen
invoice.detectedPayment transaction seen on-chain
invoice.confirmingTransaction has some confirmations
invoice.confirmedFully confirmed — safe to fulfill order
invoice.expiredInvoice expired without payment
payout.createdNew payout task dispatched to vault
payout.confirmedPayout confirmed and ready to sign
payout.executingVault acquired execution lock — signing in progress
payout.completedPayout executed on-chain (fires per order_id)
payout.cancelledPayout cancelled (includes reason field)
Multi-endpoint: You can configure multiple webhook endpoints, each subscribing to specific events or wildcards (invoice.*, payout.*, *). Manage endpoints via the dashboard or the API.

Retry strategy

If your endpoint returns a non-2xx status or the request times out (10s), we retry with exponential backoff:

AttemptDelayCumulative
1Immediate0s
230 seconds~30s
32 minutes~2.5 min
45 minutes~7.5 min
510 minutes~17.5 min

After 5 failed attempts, the event is moved to a dead-letter queue. You will receive a Telegram notification on the 3rd failure if notifications are configured.

Best practices:

Example payload:

{
  "event": "invoice.confirmed",
  "invoice_id": "d4e5f6...",
  "merchant_id": "a1b2c3...",
  "chain": "tron",
  "token": "usdt",
  "token_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
  "amount": 25.0,
  "adjusted_amount": "25.001",
  "paid_amount": "23.50",  // only present if differs from adjusted_amount
  "order_id": "ORD-1234",
  "status": "confirmed",
  "tx_hash": "abc123...",
  "confirmations": 20,
  "required_confirmations": 20,
  "payer_address": "T...",
  "timestamp": 1711001200
}

Note: The paid_amount field is only present when the actual on-chain payment differs from adjusted_amount (e.g. manual mark-paid by support). In normal auto-confirmed invoices, the amounts always match and this field is omitted.

Example payout payload:

{
  "event": "payout.completed",
  "task_id": "p1q2r3...",
  "merchant_id": "a1b2c3...",
  "order_id": "PAYROLL-2026-04",
  "chain": "tron",
  "token": "usdt",
  "amount": 1250.00,
  "recipients": [
    { "address": "T9yD14Nj...", "amount": 750.00, "memo": "Salary March" },
    { "address": "TLa2f6VP...", "amount": 500.00 }
  ],
  "recipients_count": 2,
  "tx_hash": "abc123...",
  "fee_deducted": 2.5,
  "timestamp": 1711001200
}

Note: order_id is an optional merchant-provided correlation string, accepted on POST /payout (and legacy POST /payout/create / POST /payout/batch). Webhook callbacks fire per order_id — batched tasks produce one callback per distinct order. The recipients array includes each recipient’s address, amount, and optional memo. Cancelled payouts include an additional reason field.

Verify webhook signature

Always verify the X-Webhook-Signature header using your webhook_secret. Compute HMAC-SHA256 of the raw request body and compare.

# Generate a signature locally to test
BODY='{"event":"invoice.confirmed",...}'
SECRET="your_webhook_secret"
SIG=$(echo -n $BODY | openssl dgst -sha256 -hmac $SECRET | cut -d' ' -f2)
echo $SIG
const crypto = require('crypto');

function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// Express example
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  const sig = req.headers['x-webhook-signature'];
  if (!verifyWebhook(req.body, sig, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body);
  // process event...
  res.sendStatus(200);
});
import hmac, hashlib

def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# Flask example
@app.route("/webhook", methods=["POST"])
def webhook():
    sig = request.headers.get("X-Webhook-Signature", "")
    if not verify_webhook(request.data, sig, WEBHOOK_SECRET):
        return "Invalid signature", 401
    event = request.get_json()
    # process event...
    return "OK", 200
$rawBody   = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$secret    = 'your_webhook_secret';

$expected = hash_hmac('sha256', $rawBody, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    die('Invalid signature');
}

$event = json_decode($rawBody, true);
// process $event...
http_response_code(200);
echo 'OK';
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "io"
    "net/http"
)

func verifyWebhook(body []byte, sig, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(sig))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    sig := r.Header.Get("X-Webhook-Signature")
    if !verifyWebhook(body, sig, secret) {
        http.Error(w, "Invalid signature", 401)
        return
    }
    // process body...
    w.WriteHeader(200)
}
using System.Security.Cryptography;
using System.Text;

bool VerifyWebhook(string rawBody, string signature, string secret) {
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
    var expected = Convert.ToHexString(hash).ToLowerInvariant();
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected),
        Encoding.UTF8.GetBytes(signature)
    );
}

// ASP.NET Core minimal API example
app.MapPost("/webhook", async (HttpContext ctx) => {
    using var reader = new StreamReader(ctx.Request.Body);
    var body = await reader.ReadToEndAsync();
    var sig = ctx.Request.Headers["X-Webhook-Signature"].ToString();
    if (!VerifyWebhook(body, sig, WEBHOOK_SECRET))
        return Results.StatusCode(401);
    // process body...
    return Results.Ok();
});
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;

boolean verifyWebhook(String rawBody, String signature, String secret)
        throws Exception {
    var mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
    var hash = mac.doFinal(rawBody.getBytes());
    var expected = HexFormat.of().formatHex(hash);
    return MessageDigest.isEqual(
        expected.getBytes(), signature.getBytes()
    );
}

// Spring Boot example
@PostMapping("/webhook")
public ResponseEntity<String> webhook(
        @RequestBody String body,
        @RequestHeader("X-Webhook-Signature") String sig) throws Exception {
    if (!verifyWebhook(body, sig, WEBHOOK_SECRET))
        return ResponseEntity.status(401).body("Invalid signature");
    // process body...
    return ResponseEntity.ok("OK");
}
Important: Always verify the payment via the API after receiving a webhook. Webhooks can be replayed — the API is the source of truth.

Verify Payment

After receiving a webhook (or before fulfilling an order), verify the invoice status via the API using your order_id.

curl https://api.pay.kyc.rip/invoice/by-order/ORD-1234 \
  -H "X-Merchant-Key: mk_your-key"
const res = await fetch(
  'https://api.pay.kyc.rip/invoice/by-order/ORD-1234',
  { headers: { 'X-Merchant-Key': 'mk_your-key' } }
);
const invoice = await res.json();

if (invoice.status === 'confirmed') {
  // fulfill the order
}
resp = requests.get(
    "https://api.pay.kyc.rip/invoice/by-order/ORD-1234",
    headers={"X-Merchant-Key": "mk_your-key"},
)
invoice = resp.json()

if invoice["status"] == "confirmed":
    # fulfill the order
    pass
$ch = curl_init('https://api.pay.kyc.rip/invoice/by-order/ORD-1234');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['X-Merchant-Key: mk_your-key'],
]);
$invoice = json_decode(curl_exec($ch), true);
curl_close($ch);

if ($invoice['status'] === 'confirmed') {
    // fulfill the order
}
req, _ := http.NewRequest("GET",
    "https://api.pay.kyc.rip/invoice/by-order/ORD-1234", nil)
req.Header.Set("X-Merchant-Key", "mk_your-key")

resp, _ := http.DefaultClient.Do(req)
var invoice map[string]any
json.NewDecoder(resp.Body).Decode(&invoice)

if invoice["status"] == "confirmed" {
    // fulfill the order
}
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Merchant-Key", "mk_your-key");

var res = await client.GetAsync(
    "https://api.pay.kyc.rip/invoice/by-order/ORD-1234");
var json = await res.Content.ReadAsStringAsync();
var invoice = JsonSerializer.Deserialize<JsonElement>(json);

if (invoice.GetProperty("status").GetString() == "confirmed") {
    // fulfill the order
}
var req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.pay.kyc.rip/invoice/by-order/ORD-1234"))
    .header("X-Merchant-Key", "mk_your-key")
    .GET().build();

var res = HttpClient.newHttpClient()
    .send(req, HttpResponse.BodyHandlers.ofString());

// Parse with org.json or Jackson
var invoice = new org.json.JSONObject(res.body());
if (invoice.getString("status").equals("confirmed")) {
    // fulfill the order
}

Invoice statuses:

StatusMeaning
pendingWaiting for payment
detectedTransaction seen on-chain, not yet confirmed
confirmingHas some confirmations, waiting for threshold
confirmedFully confirmed — safe to deliver
expiredExpired without payment
failedPayment failed

Payouts (Optional)

Send USDT to one or more recipients. Payouts require a two-step challenge confirmation for security.

Vault Required: Payouts are signed locally using the PayGate Vault desktop app. The Vault holds your private keys and signs transactions — funds never pass through our servers. Download the Vault, connect it to your merchant account, and it will automatically receive and process payout tasks.

Download: macOS | Windows | Linux

Single payout

curl -X POST https://api.pay.kyc.rip/payout \
  -H "X-Merchant-Key: mk_your-key" \
  -H "Content-Type: application/json" \
  -d '{
    "payouts": [
      {"address": "TXyz...", "amount": 100.00, "chain": "tron", "token": "usdt", "order_id": "WD-567", "memo": "Withdrawal #567"}
    ]
  }'
const res = await fetch('https://api.pay.kyc.rip/payout', {
  method: 'POST',
  headers: {
    'X-Merchant-Key': 'mk_your-key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    payouts: [
      { address: 'TXyz...', amount: 100.00, chain: 'tron', token: 'usdt', order_id: 'WD-567', memo: 'Withdrawal #567' },
    ],
  }),
});
const data = await res.json();
console.log(data.accepted, data.total_amount);

Batch payout (mixed chains)

curl -X POST https://api.pay.kyc.rip/payout \
  -H "X-Merchant-Key: mk_your-key" \
  -H "Content-Type: application/json" \
  -d '{
    "payouts": [
      {"address": "0xABC...", "amount": 200, "chain": "eth", "token": "usdc", "order_id": "INV-1001", "memo": "Invoice payment"},
      {"address": "TXyz...", "amount": 150, "chain": "tron", "token": "usdt", "order_id": "INV-1002", "memo": "Refund"}
    ]
  }'
const res = await fetch('https://api.pay.kyc.rip/payout', {
  method: 'POST',
  headers: {
    'X-Merchant-Key': 'mk_your-key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    payouts: [
      { address: '0xABC...', amount: 200, chain: 'eth', token: 'usdc', order_id: 'INV-1001' },
      { address: 'TXyz...', amount: 150, chain: 'tron', token: 'usdt', order_id: 'INV-1002' },
    ],
  }),
});
One endpoint, flat format: POST /payout accepts one or many items — each self-contained with its own chain, token, order_id, and memo. We handle grouping and batching internally. Each order_id gets its own payout.completed webhook callback, even if items were batched into one on-chain transaction.

Address Pool (Optional)

Merchants can add multiple receiving addresses per chain. When multiple addresses are configured, invoices are automatically distributed across them to reduce amount collisions on high-volume accounts.

How it works

Strategies

StrategyDescription
least_activeAssigns to address with fewest pending invoices (default)
round_robinEven distribution across addresses in order
weightedRandom selection based on address weight
This feature is backward compatible. Merchants using a single address via PUT /merchant/settings continue to work unchanged. See the API Reference for full endpoint details.

Best Practices

PracticeWhy
Always verify webhooks server-side Check X-Webhook-Signature with HMAC-SHA256 to prevent spoofing
Always verify via API before fulfilling Call GET /invoice/by-order/:orderId to confirm status is confirmed
Use order_id for idempotency Duplicate order_id returns the existing invoice instead of creating a new one
Use return_url for better UX Redirect customers back to your site after payment completes
Enable request signing in production Set require_signature: true to prevent unauthorized API usage
Configure IP whitelist Restrict API access to your server IPs via ip_whitelist in settings
Set up Telegram notifications Add your telegram_chat_id in settings for real-time alerts
Use testnet first Develop against testnet.pay.kyc.rip before going live

Testing on Testnet

Test the full payment flow without real money using our testnet environment.

Testnet API: https://testnet.pay.kyc.rip

Get Testnet Tokens

ChainWhatHow
Sepolia ETH Gas for transactions sepolia-faucet.pk910.de (PoW mining)
Sepolia USDT Test USDT (free mint) Call mint(1000000000) on our TestUSDT contract for 1,000 USDT
Sepolia USDC Test USDC (Circle official) Get test USDC from faucet.circle.com (select Sepolia)
Nile TRX Energy for transactions nileex.io
Nile USDT Test TRC-20 USDT Enter your Tron address on nileex.io

Test Flow

  1. Register on the testnet merchant dashboard at pay.kyc.rip/access (switch to Testnet mode)
  2. Set your testnet wallet addresses in Settings
  3. Create a test invoice via API or dashboard
  4. Pay with testnet USDT or USDC — the system detects and confirms automatically
  5. Verify the webhook was delivered and the status is confirmed
  6. Switch to mainnet when ready — same code, different API URL
  7. Use @saggy_ai_bot on Telegram to track testnet orders. For mainnet use @rip_pay_bot

SDKs & Libraries

Official SDKs are coming soon. In the meantime, the API is a standard REST API — use any HTTP client in your language of choice.

LanguageStatus
Node.js / TypeScriptComing soon
PythonComing soon
PHPComing soon
GoComing soon

For the full API reference covering all endpoints, parameters, and response schemas, see the API Documentation.