kokiwa

Ví dụ tích hợp

Mỗi ngôn ngữ một tệp hoàn chỉnh để tạo thanh toán, cấp địa chỉ nạp và xử lý webhook. Sao chép, đặt khóa của bạn và thay hai hàm cập nhật cơ sở dữ liệu.

Các ví dụ này, cùng bot Telegram và trang thanh toán Next.js, có trên GitHub: github.com/maxwellzen1/kokiwa-examples

Trước khi bắt đầu

  1. Tạo khóa API trong trang quản trị người bán. Nên bắt đầu bằng khóa thử (sk_test_) trên testnet.
  2. Thêm URL webhook trỏ đến máy chủ của bạn, ví dụ https://your-shop.com/kokiwa/webhook, và sao chép khóa bí mật ký (whsec_).
  3. Đặt chúng thành KOKIWA_KEY và KOKIWA_WEBHOOK_SECRET trong biến môi trường máy chủ. Không bao giờ đưa vào mã front-end.

Mỗi ví dụ làm gì

  • createPayment: Tạo thanh toán USDT cho đơn hàng và trả về checkout_url. Chuyển khách đến đó.
  • depositAddress: Trả về địa chỉ nạp BNB Chain cố định của khách để nạp tiền.
  • verifySignature: Kiểm tra Kokiwa-Signature với nội dung yêu cầu gốc và từ chối yêu cầu cũ hơn 5 phút.
  • handleEvent: Đánh dấu đơn đã thanh toán khi có payment_intent.paid và ghi có khoản nạp khi có deposit.confirmed.

Những điểm cần làm đúng

  • Một sự kiện có thể đến nhiều lần. markOrderPaid và creditCustomer phải chạy lại an toàn, theo khóa đơn hàng hoặc deposit.id.
  • deposit.confirmed cũng được gửi cho thanh toán đơn hàng. Các ví dụ chỉ ghi có khi payment_intent là null, nên đơn hàng không bị tính hai lần.
  • Với Idempotency-Key order-{id}, gửi lại sẽ trả về cùng một thanh toán. Nếu thanh toán hết hạn và khách trả lại, hãy dùng khóa mới như order-{id}-2.
  • Trả về 2xx nhanh. Phản hồi không phải 2xx hoặc hết thời gian sẽ được gửi lại trong khoảng 3 ngày.

Node.js

kokiwa.mjs

// kokiwa integration for Node.js 18 or later, with no dependencies.
// Run: KOKIWA_KEY=sk_test_... KOKIWA_WEBHOOK_SECRET=whsec_... node kokiwa.mjs
import crypto from "node:crypto";
import http from "node:http";

const API = process.env.KOKIWA_API ?? "https://api.kokiwa.com";
const KEY = process.env.KOKIWA_KEY;
const SECRET = process.env.KOKIWA_WEBHOOK_SECRET;

async function kokiwa(method, path, body, idempotencyKey) {
  const headers = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };
  if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
  const res = await fetch(API + path, { method, headers, body: body && JSON.stringify(body) });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
  return data;
}

// 1. Create a payment for an order, then send the customer to the returned URL.
export async function createPayment(orderId, amount, customerId) {
  const intent = await kokiwa("POST", "/v1/payment_intents", {
    currency: "USDT",
    amount, // a string, such as "25.00"
    customer: customerId,
    metadata: { order_id: orderId },
    success_url: `https://your-shop.com/orders/${orderId}`,
  }, `order-${orderId}`); // a retry returns the same payment instead of a second one
  return intent.checkout_url;
}

// 2. A customer's permanent deposit address on BNB Chain, for top-ups.
export async function depositAddress(customerId) {
  const result = await kokiwa("POST", "/v1/deposit_addresses", { customer: customerId });
  return result.address;
}

// 3. Check the Kokiwa-Signature header against the raw request body.
export function verifySignature(rawBody, header, secret = SECRET) {
  const parts = header.split(",").map((part) => part.trim().split("="));
  const t = parts.find(([key]) => key === "t")?.[1] ?? "";
  const signatures = parts.filter(([key]) => key === "v1").map(([, value]) => value ?? "");
  if (!/^\d+$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
  return signatures.some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}

// 4. Act on events. They can arrive twice, so each step must be safe to repeat.
async function handleEvent(event) {
  const { payment_intent: intent, deposit } = event.data;
  if (event.type === "payment_intent.paid") {
    await markOrderPaid(intent.metadata.order_id, intent.id);
  } else if (event.type === "deposit.confirmed" && deposit.payment_intent === null) {
    // a top-up to a deposit address; payments for orders are handled above
    await creditCustomer(deposit.customer, deposit.amount, deposit.id);
  }
}

// Replace these with your own database updates.
async function markOrderPaid(orderId, paymentIntentId) {
  console.log("order paid", orderId, paymentIntentId);
}
async function creditCustomer(customerId, amount, depositId) {
  console.log("credit", customerId, amount, depositId);
}

http.createServer((req, res) => {
  if (req.method !== "POST" || req.url !== "/kokiwa/webhook") return res.writeHead(404).end();
  const chunks = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", async () => {
    const raw = Buffer.concat(chunks);
    if (!verifySignature(raw, req.headers["kokiwa-signature"] ?? "")) return res.writeHead(400).end();
    try {
      await handleEvent(JSON.parse(raw));
      res.writeHead(200).end();
    } catch (err) {
      console.error(err);
      res.writeHead(500).end(); // kokiwa retries later
    }
  });
}).listen(3000);

Python

kokiwa.py

# kokiwa integration for Python 3.9 or later, with Flask and requests.
# pip install flask requests
# Run: KOKIWA_KEY=sk_test_... KOKIWA_WEBHOOK_SECRET=whsec_... flask --app kokiwa run --port 3000
import hashlib
import hmac
import os
import time

import requests
from flask import Flask, request

API = os.environ.get("KOKIWA_API", "https://api.kokiwa.com")
KEY = os.environ["KOKIWA_KEY"]
SECRET = os.environ["KOKIWA_WEBHOOK_SECRET"].encode()

app = Flask(__name__)


def kokiwa(method, path, body=None, idempotency_key=None):
    headers = {"Authorization": f"Bearer {KEY}"}
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key
    res = requests.request(method, API + path, json=body, headers=headers, timeout=15)
    data = res.json()
    if not res.ok:
        raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
    return data


# 1. Create a payment for an order, then send the customer to the returned URL.
def create_payment(order_id, amount, customer_id):
    intent = kokiwa("POST", "/v1/payment_intents", {
        "currency": "USDT",
        "amount": amount,  # a string, such as "25.00"
        "customer": customer_id,
        "metadata": {"order_id": order_id},
        "success_url": f"https://your-shop.com/orders/{order_id}",
    }, idempotency_key=f"order-{order_id}")  # a retry returns the same payment instead of a second one
    return intent["checkout_url"]


# 2. A customer's permanent deposit address on BNB Chain, for top-ups.
def deposit_address(customer_id):
    return kokiwa("POST", "/v1/deposit_addresses", {"customer": customer_id})["address"]


# 3. Check the Kokiwa-Signature header against the raw request body.
def verify_signature(raw_body: bytes, header: str, secret: bytes = SECRET) -> bool:
    t, signatures = "", []
    for part in header.split(","):
        key, _, value = part.strip().partition("=")
        if key == "t":
            t = value
        elif key == "v1":
            signatures.append(value)
    if not t.isdigit() or abs(time.time() - int(t)) > 300:
        return False
    expected = hmac.new(secret, t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in signatures)


# 4. Act on events. They can arrive twice, so each step must be safe to repeat.
def handle_event(event):
    data = event["data"]
    if event["type"] == "payment_intent.paid":
        intent = data["payment_intent"]
        mark_order_paid(intent["metadata"]["order_id"], intent["id"])
    elif event["type"] == "deposit.confirmed" and data["deposit"]["payment_intent"] is None:
        # a top-up to a deposit address; payments for orders are handled above
        deposit = data["deposit"]
        credit_customer(deposit["customer"], deposit["amount"], deposit["id"])


# Replace these with your own database updates.
def mark_order_paid(order_id, payment_intent_id):
    print("order paid", order_id, payment_intent_id)


def credit_customer(customer_id, amount, deposit_id):
    print("credit", customer_id, amount, deposit_id)


@app.post("/kokiwa/webhook")
def webhook():
    raw = request.get_data()
    if not verify_signature(raw, request.headers.get("Kokiwa-Signature", "")):
        return "", 400
    handle_event(request.get_json(force=True))
    return "", 200

PHP

PHP gồm hai tệp: kokiwa.php chứa các hàm, webhook.php là URL bạn đăng ký.

kokiwa.php

<?php
// kokiwa integration for PHP 8.0 or later, with the curl extension.
// Set KOKIWA_KEY and KOKIWA_WEBHOOK_SECRET in the environment, then require this file.

const KOKIWA_TOLERANCE = 300; // seconds

function kokiwa(string $method, string $path, ?array $body = null, ?string $idempotencyKey = null): array
{
    $api = getenv('KOKIWA_API') ?: 'https://api.kokiwa.com';
    $headers = ['Authorization: Bearer ' . getenv('KOKIWA_KEY'), 'Content-Type: application/json'];
    if ($idempotencyKey !== null) {
        $headers[] = 'Idempotency-Key: ' . $idempotencyKey;
    }
    $ch = curl_init($api . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 15,
    ]);
    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    }
    $response = curl_exec($ch);
    if ($response === false) {
        throw new RuntimeException(curl_error($ch));
    }
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $data = json_decode($response, true);
    if ($status >= 400) {
        throw new RuntimeException($data['error']['code'] . ': ' . $data['error']['message']);
    }
    return $data;
}

// 1. Create a payment for an order, then send the customer to the returned URL.
function kokiwa_create_payment(string $orderId, string $amount, string $customerId): string
{
    $intent = kokiwa('POST', '/v1/payment_intents', [
        'currency' => 'USDT',
        'amount' => $amount, // a string, such as "25.00"
        'customer' => $customerId,
        'metadata' => ['order_id' => $orderId],
        'success_url' => 'https://your-shop.com/orders/' . $orderId,
    ], 'order-' . $orderId); // a retry returns the same payment instead of a second one
    return $intent['checkout_url'];
}

// 2. A customer's permanent deposit address on BNB Chain, for top-ups.
function kokiwa_deposit_address(string $customerId): string
{
    return kokiwa('POST', '/v1/deposit_addresses', ['customer' => $customerId])['address'];
}

// 3. Check the Kokiwa-Signature header against the raw request body.
function kokiwa_verify_signature(string $rawBody, string $header, string $secret): bool
{
    $t = '';
    $signatures = [];
    foreach (explode(',', $header) as $part) {
        [$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
        if ($key === 't') {
            $t = $value;
        } elseif ($key === 'v1') {
            $signatures[] = $value;
        }
    }
    if (!ctype_digit($t) || abs(time() - (int) $t) > KOKIWA_TOLERANCE) {
        return false;
    }
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    foreach ($signatures as $signature) {
        if (hash_equals($expected, $signature)) {
            return true;
        }
    }
    return false;
}

webhook.php

<?php
// The webhook URL you register in the kokiwa dashboard points here.
require __DIR__ . '/kokiwa.php';

$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_KOKIWA_SIGNATURE'] ?? '';
if (!kokiwa_verify_signature($raw, $header, getenv('KOKIWA_WEBHOOK_SECRET'))) {
    http_response_code(400);
    exit;
}

// 4. Act on events. They can arrive twice, so each step must be safe to repeat.
$event = json_decode($raw, true);
$data = $event['data'];
if ($event['type'] === 'payment_intent.paid') {
    mark_order_paid($data['payment_intent']['metadata']['order_id'], $data['payment_intent']['id']);
} elseif ($event['type'] === 'deposit.confirmed' && $data['deposit']['payment_intent'] === null) {
    // a top-up to a deposit address; payments for orders are handled above
    credit_customer($data['deposit']['customer'], $data['deposit']['amount'], $data['deposit']['id']);
}
http_response_code(200);

// Replace these with your own database updates.
function mark_order_paid(string $orderId, string $paymentIntentId): void
{
    error_log("order paid $orderId $paymentIntentId");
}

function credit_customer(string $customerId, string $amount, string $depositId): void
{
    error_log("credit $customerId $amount $depositId");
}

Go

main.go

// kokiwa integration for Go 1.22 or later, standard library only.
// Run: KOKIWA_KEY=sk_test_... KOKIWA_WEBHOOK_SECRET=whsec_... go run .
package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

var (
	api    = envOr("KOKIWA_API", "https://api.kokiwa.com")
	key    = os.Getenv("KOKIWA_KEY")
	secret = []byte(os.Getenv("KOKIWA_WEBHOOK_SECRET"))
	client = &http.Client{Timeout: 15 * time.Second}
)

func kokiwa(method, path string, body any, idempotencyKey string, out any) error {
	payload, err := json.Marshal(body)
	if err != nil {
		return err
	}
	req, err := http.NewRequest(method, api+path, bytes.NewReader(payload))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+key)
	req.Header.Set("Content-Type", "application/json")
	if idempotencyKey != "" {
		req.Header.Set("Idempotency-Key", idempotencyKey)
	}
	res, err := client.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		var e struct {
			Error struct{ Code, Message string } `json:"error"`
		}
		json.NewDecoder(res.Body).Decode(&e)
		return fmt.Errorf("%s: %s", e.Error.Code, e.Error.Message)
	}
	return json.NewDecoder(res.Body).Decode(out)
}

// 1. Create a payment for an order, then send the customer to the returned URL.
func createPayment(orderID, amount, customerID string) (string, error) {
	var intent struct {
		CheckoutURL string `json:"checkout_url"`
	}
	err := kokiwa("POST", "/v1/payment_intents", map[string]any{
		"currency":    "USDT",
		"amount":      amount, // a string, such as "25.00"
		"customer":    customerID,
		"metadata":    map[string]string{"order_id": orderID},
		"success_url": "https://your-shop.com/orders/" + orderID,
	}, "order-"+orderID, &intent) // a retry returns the same payment instead of a second one
	return intent.CheckoutURL, err
}

// 2. A customer's permanent deposit address on BNB Chain, for top-ups.
func depositAddress(customerID string) (string, error) {
	var result struct {
		Address string `json:"address"`
	}
	err := kokiwa("POST", "/v1/deposit_addresses", map[string]string{"customer": customerID}, "", &result)
	return result.Address, err
}

// 3. Check the Kokiwa-Signature header against the raw request body.
func verifySignature(rawBody []byte, header string, secret []byte) bool {
	var t string
	var signatures []string
	for _, part := range strings.Split(header, ",") {
		k, v, _ := strings.Cut(strings.TrimSpace(part), "=")
		switch k {
		case "t":
			t = v
		case "v1":
			signatures = append(signatures, v)
		}
	}
	ts, err := strconv.ParseInt(t, 10, 64)
	if err != nil || time.Since(time.Unix(ts, 0)).Abs() > 5*time.Minute {
		return false
	}
	mac := hmac.New(sha256.New, secret)
	mac.Write([]byte(t + "."))
	mac.Write(rawBody)
	expected := hex.EncodeToString(mac.Sum(nil))
	for _, s := range signatures {
		if hmac.Equal([]byte(s), []byte(expected)) {
			return true
		}
	}
	return false
}

type event struct {
	Type string `json:"type"`
	Data struct {
		PaymentIntent *struct {
			ID       string            `json:"id"`
			Metadata map[string]string `json:"metadata"`
		} `json:"payment_intent"`
		Deposit *struct {
			ID            string  `json:"id"`
			Customer      string  `json:"customer"`
			Amount        string  `json:"amount"`
			PaymentIntent *string `json:"payment_intent"`
		} `json:"deposit"`
	} `json:"data"`
}

// 4. Act on events. They can arrive twice, so each step must be safe to repeat.
func handleEvent(e event) error {
	switch {
	case e.Type == "payment_intent.paid":
		return markOrderPaid(e.Data.PaymentIntent.Metadata["order_id"], e.Data.PaymentIntent.ID)
	case e.Type == "deposit.confirmed" && e.Data.Deposit.PaymentIntent == nil:
		// a top-up to a deposit address; payments for orders are handled above
		d := e.Data.Deposit
		return creditCustomer(d.Customer, d.Amount, d.ID)
	}
	return nil
}

// Replace these with your own database updates.
func markOrderPaid(orderID, paymentIntentID string) error {
	log.Println("order paid", orderID, paymentIntentID)
	return nil
}

func creditCustomer(customerID, amount, depositID string) error {
	log.Println("credit", customerID, amount, depositID)
	return nil
}

func webhook(w http.ResponseWriter, r *http.Request) {
	raw, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil || !verifySignature(raw, r.Header.Get("Kokiwa-Signature"), secret) {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	var e event
	if err := json.Unmarshal(raw, &e); err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	if err := handleEvent(e); err != nil {
		log.Println(err)
		w.WriteHeader(http.StatusInternalServerError) // kokiwa retries later
		return
	}
	w.WriteHeader(http.StatusOK)
}

func envOr(name, fallback string) string {
	if v := os.Getenv(name); v != "" {
		return v
	}
	return fallback
}

func main() {
	http.HandleFunc("POST /kokiwa/webhook", webhook)
	log.Fatal(http.ListenAndServe(":3000", nil))
}

Java

Kokiwa.java

// kokiwa integration for Java 17 or later, with Jackson (com.fasterxml.jackson.core:jackson-databind).
// Run: KOKIWA_KEY=sk_test_... KOKIWA_WEBHOOK_SECRET=whsec_... java Kokiwa.java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.HexFormat;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class Kokiwa {
    static final String API = System.getenv().getOrDefault("KOKIWA_API", "https://api.kokiwa.com");
    static final String KEY = System.getenv("KOKIWA_KEY");
    static final String SECRET = System.getenv("KOKIWA_WEBHOOK_SECRET");
    static final HttpClient CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(15)).build();
    static final ObjectMapper JSON = new ObjectMapper();

    static JsonNode kokiwa(String method, String path, Object body, String idempotencyKey) throws Exception {
        HttpRequest.Builder req = HttpRequest.newBuilder(URI.create(API + path))
            .timeout(Duration.ofSeconds(15))
            .header("Authorization", "Bearer " + KEY)
            .header("Content-Type", "application/json")
            .method(method, HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(body)));
        if (idempotencyKey != null) req.header("Idempotency-Key", idempotencyKey);
        HttpResponse<String> res = CLIENT.send(req.build(), HttpResponse.BodyHandlers.ofString());
        JsonNode data = JSON.readTree(res.body());
        if (res.statusCode() >= 400) {
            throw new IllegalStateException(data.at("/error/code").asText() + ": " + data.at("/error/message").asText());
        }
        return data;
    }

    // 1. Create a payment for an order, then send the customer to the returned URL.
    static String createPayment(String orderId, String amount, String customerId) throws Exception {
        JsonNode intent = kokiwa("POST", "/v1/payment_intents", Map.of(
            "currency", "USDT",
            "amount", amount, // a string, such as "25.00"
            "customer", customerId,
            "metadata", Map.of("order_id", orderId),
            "success_url", "https://your-shop.com/orders/" + orderId
        ), "order-" + orderId); // a retry returns the same payment instead of a second one
        return intent.get("checkout_url").asText();
    }

    // 2. A customer's permanent deposit address on BNB Chain, for top-ups.
    static String depositAddress(String customerId) throws Exception {
        return kokiwa("POST", "/v1/deposit_addresses", Map.of("customer", customerId), null).get("address").asText();
    }

    // 3. Check the Kokiwa-Signature header against the raw request body.
    static boolean verifySignature(byte[] rawBody, String header, String secret) throws Exception {
        String t = "";
        java.util.List<String> signatures = new java.util.ArrayList<>();
        for (String part : header.split(",")) {
            String[] kv = part.trim().split("=", 2);
            if (kv.length < 2) continue;
            if (kv[0].equals("t")) t = kv[1];
            else if (kv[0].equals("v1")) signatures.add(kv[1]);
        }
        if (!t.matches("\\d+") || Math.abs(System.currentTimeMillis() / 1000 - Long.parseLong(t)) > 300) return false;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        mac.update((t + ".").getBytes(StandardCharsets.UTF_8));
        byte[] expected = HexFormat.of().formatHex(mac.doFinal(rawBody)).getBytes(StandardCharsets.UTF_8);
        for (String s : signatures) {
            if (MessageDigest.isEqual(expected, s.getBytes(StandardCharsets.UTF_8))) return true;
        }
        return false;
    }

    // 4. Act on events. They can arrive twice, so each step must be safe to repeat.
    static void handleEvent(JsonNode event) {
        String type = event.get("type").asText();
        JsonNode data = event.get("data");
        if (type.equals("payment_intent.paid")) {
            JsonNode intent = data.get("payment_intent");
            markOrderPaid(intent.at("/metadata/order_id").asText(), intent.get("id").asText());
        } else if (type.equals("deposit.confirmed") && data.at("/deposit/payment_intent").isNull()) {
            // a top-up to a deposit address; payments for orders are handled above
            JsonNode deposit = data.get("deposit");
            creditCustomer(deposit.get("customer").asText(), deposit.get("amount").asText(), deposit.get("id").asText());
        }
    }

    // Replace these with your own database updates.
    static void markOrderPaid(String orderId, String paymentIntentId) {
        System.out.println("order paid " + orderId + " " + paymentIntentId);
    }

    static void creditCustomer(String customerId, String amount, String depositId) {
        System.out.println("credit " + customerId + " " + amount + " " + depositId);
    }

    static void webhook(HttpExchange ex) throws java.io.IOException {
        int status;
        try {
            byte[] raw = ex.getRequestBody().readAllBytes();
            String header = ex.getRequestHeaders().getFirst("Kokiwa-Signature");
            if (!"POST".equals(ex.getRequestMethod()) || header == null || !verifySignature(raw, header, SECRET)) {
                status = 400;
            } else {
                handleEvent(JSON.readTree(raw));
                status = 200;
            }
        } catch (Exception e) {
            e.printStackTrace();
            status = 500; // kokiwa retries later
        }
        ex.sendResponseHeaders(status, -1);
        ex.close();
    }

    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0);
        server.createContext("/kokiwa/webhook", Kokiwa::webhook);
        server.start();
    }
}

Mọi tham số có trong tài liệu API, mọi sự kiện có trong trang Webhook. API · Webhooks

Connect a wallet with WalletConnect