Exemplos de integração
Um arquivo completo por linguagem que cria pagamentos, gera endereços de depósito e trata webhooks. Copie, configure suas chaves e substitua as duas funções que atualizam seu banco de dados.
Os mesmos exemplos, além de um bot do Telegram e um checkout com Next.js, estão no GitHub: github.com/maxwellzen1/kokiwa-examples
Antes de começar
- Crie uma chave de API no painel do lojista. Comece com uma chave de teste (sk_test_) na testnet.
- Adicione uma URL de webhook que aponte para o seu servidor, como https://your-shop.com/kokiwa/webhook, e copie o segredo de assinatura (whsec_).
- Defina-as como KOKIWA_KEY e KOKIWA_WEBHOOK_SECRET no ambiente do servidor. Nunca as coloque no código do front-end.
O que cada exemplo faz
createPayment: Cria um pagamento em USDT para um pedido e retorna checkout_url. Envie o cliente para lá.depositAddress: Retorna o endereço de depósito fixo do cliente na BNB Chain, para recargas.verifySignature: Confere Kokiwa-Signature com o corpo original e rejeita requisições com mais de 5 minutos.handleEvent: Marca o pedido como pago em payment_intent.paid e credita recargas em deposit.confirmed.
Pontos de atenção
- Um evento pode chegar mais de uma vez. markOrderPaid e creditCustomer precisam poder ser repetidos com segurança, usando o pedido ou deposit.id como chave.
- deposit.confirmed também é enviado em pagamentos de pedidos. Os exemplos só creditam quando payment_intent é null, então um pedido nunca conta duas vezes.
- Com Idempotency-Key order-{id}, uma nova tentativa retorna o mesmo pagamento. Se o pagamento expirar e o cliente pagar de novo, use uma chave nova como order-{id}-2.
- Responda 2xx rápido. Uma resposta diferente de 2xx ou um tempo esgotado é tentado de novo por cerca de 3 dias.
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
O PHP usa dois arquivos: kokiwa.php tem as funções e webhook.php é a URL que você registra.
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();
}
}
Todos os parâmetros estão na referência da API, e todos os eventos na página de webhooks. API · Webhooks