kokiwa

연동 예제

언어마다 결제 생성, 입금 주소 발급, 웹훅 처리를 모두 담은 완전한 파일 하나를 제공합니다. 복사해서 키를 설정하고, 데이터베이스를 갱신하는 두 함수만 바꾸면 됩니다.

같은 예제와 텔레그램 봇, Next.js 결제 예제는 GitHub에 있습니다: github.com/maxwellzen1/kokiwa-examples

시작하기 전에

  1. 판매자 대시보드에서 API 키를 만드세요. 테스트넷의 테스트 키(sk_test_)로 시작하는 것을 권장합니다.
  2. 서버를 가리키는 웹훅 URL(예: https://your-shop.com/kokiwa/webhook)을 추가하고 서명 시크릿(whsec_)을 복사하세요.
  3. 서버 환경 변수에 KOKIWA_KEY와 KOKIWA_WEBHOOK_SECRET으로 설정하세요. 프런트엔드 코드에는 절대 넣지 마세요.

각 예제가 하는 일

  • createPayment: 주문에 대한 USDT 결제를 만들고 checkout_url을 반환합니다. 고객을 이 주소로 보내세요.
  • depositAddress: 충전용으로 고객의 고정 BNB Chain 입금 주소를 반환합니다.
  • verifySignature: 원본 요청 본문으로 Kokiwa-Signature를 확인하고 5분이 지난 요청은 거부합니다.
  • handleEvent: payment_intent.paid에서 주문을 결제 완료로, deposit.confirmed에서 충전을 반영합니다.

꼭 지켜야 할 점

  • 같은 이벤트가 여러 번 올 수 있습니다. markOrderPaid와 creditCustomer는 주문이나 deposit.id를 기준으로 반복해도 안전해야 합니다.
  • 주문 결제에도 deposit.confirmed가 전송됩니다. 예제는 payment_intent가 null일 때만 반영하므로 주문이 두 번 계산되지 않습니다.
  • Idempotency-Key를 order-{id}로 두면 재시도 시 같은 결제를 돌려받습니다. 결제가 만료된 뒤 고객이 다시 결제하면 order-{id}-2처럼 새 키를 쓰세요.
  • 2xx를 빨리 반환하세요. 2xx가 아닌 응답이나 시간 초과는 약 3일 동안 재시도됩니다.

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는 두 파일로 나뉩니다. kokiwa.php에는 함수가, webhook.php는 등록할 웹훅 URL입니다.

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();
    }
}

모든 파라미터는 API 문서에, 모든 이벤트는 웹훅 페이지에 있습니다. API · Webhooks

Connect a wallet with WalletConnect