Integration Guide
Everything you need to start accepting USDT and USDC payments on Ethereum and Tron. No KYC required.
https://api.pay.kyc.rip —
For testing use https://testnet.pay.kyc.rip
Getting Started
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.
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"}'
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>
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:
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 hex digest of the body |
X-Webhook-Event | Event name (e.g. invoice.confirmed) |
X-Webhook-Attempt | Delivery attempt number (1-5) |
X-Webhook-Timestamp | Unix timestamp of this delivery attempt |
Content-Type | application/json |
Events:
| Event | When |
|---|---|
invoice.detected | Payment transaction seen on-chain |
invoice.confirming | Transaction has some confirmations |
invoice.confirmed | Fully confirmed — safe to fulfill order |
invoice.expired | Invoice expired without payment |
payout.created | New payout task dispatched to vault |
payout.confirmed | Payout confirmed and ready to sign |
payout.executing | Vault acquired execution lock — signing in progress |
payout.completed | Payout executed on-chain (fires per order_id) |
payout.cancelled | Payout cancelled (includes reason field) |
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:
| Attempt | Delay | Cumulative |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 30 seconds | ~30s |
| 3 | 2 minutes | ~2.5 min |
| 4 | 5 minutes | ~7.5 min |
| 5 | 10 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:
- Return
200 OKquickly — process the event asynchronously - Use
X-Webhook-Attemptheader to detect retries - Implement idempotency using
invoice_idortask_idto handle duplicate deliveries - Use the signature verification to reject forged requests
- Webhook logs are available in the dashboard under Webhooks
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");
}
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:
| Status | Meaning |
|---|---|
pending | Waiting for payment |
detected | Transaction seen on-chain, not yet confirmed |
confirming | Has some confirmations, waiting for threshold |
confirmed | Fully confirmed — safe to deliver |
expired | Expired without payment |
failed | Payment failed |
Payouts (Optional)
Send USDT to one or more recipients. Payouts require a two-step challenge confirmation for security.
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' },
],
}),
});
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
- Add addresses to the pool via
POST /merchant/addresseswith achainandaddress - Set a distribution strategy via
PUT /merchant/address-strategy - Invoices are automatically assigned a
pay_addressfrom the pool based on the active strategy - Default strategy:
least_active— assigns to the address with the fewest pending invoices
Strategies
| Strategy | Description |
|---|---|
least_active | Assigns to address with fewest pending invoices (default) |
round_robin | Even distribution across addresses in order |
weighted | Random selection based on address weight |
PUT /merchant/settings continue to work unchanged. See the API Reference for full endpoint details.
Best Practices
| Practice | Why |
|---|---|
| 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.
https://testnet.pay.kyc.rip
Get Testnet Tokens
| Chain | What | How |
|---|---|---|
| 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
- Register on the testnet merchant dashboard at pay.kyc.rip/access (switch to Testnet mode)
- Set your testnet wallet addresses in Settings
- Create a test invoice via API or dashboard
- Pay with testnet USDT or USDC — the system detects and confirms automatically
- Verify the webhook was delivered and the status is confirmed
- Switch to mainnet when ready — same code, different API URL
- 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.
| Language | Status |
|---|---|
| Node.js / TypeScript | Coming soon |
| Python | Coming soon |
| PHP | Coming soon |
| Go | Coming soon |
For the full API reference covering all endpoints, parameters, and response schemas, see the API Documentation.