kamiPay LogokamiPay Docs

Introduction

Get started with kamiPay webhooks

Use webhooks to notify your application about kamiPay events.

kamiPay uses webhooks to push real-time notifications to you about a payment receive. All webhooks use HTTPS and deliver a JSON payload that can be used by your application.

Steps to receive webhooks

You can start receiving real-time events in your app using the following steps:

1. Description

The Status Update webhook provides a way to receive events when changes occur in a payment within the system. Whenever a payment change event occurs, a notification will be sent to the client's URL with the content of the affected payment.

2. Delivery Method

The webhook sends events using the HTTP POST method.

3. Event Content

The event content will be a JSON message that includes the details of the payment affected by the change. For example, a Charge webhook, with its keys in the order they arrive:

{
  "pix_id": "ptxr_01kr3m9p7n2s4d8h6e5b3t7w1k",
  "status": "done",
  "tx_id": "0xa3f9b2c1e7d4865094bd28fa1c3e6b85907df42a3b9c1de80f5a672bc41e9d3f",
  "timestamp": "2026-04-27 15:51:40.171821-03:00",
  "type": "charge",
  "qr_type": "dynamic",
  "kamipay_id": "dqr_01kr3m9q5h7w2v4n6b8s3d5e9p",
  "data": {
    "bank_txid": "E29384756202604271548aB7cD3xY9mZ",
    "bank_account_nr": "29384756-0001-0001349872",
    "internal_pix_id": "8a3f1c9d72b46e08fd5a912ec47b3f06",
    "amount_brl": "5.28",
    "amount_usdt": "1.033355",
    "address_out": "0x7a3F9b2C1e8D5462bA9c7F3e6D85907df41A2c3B",
    "name": "Satoshi Nakamoto"
  }
}

4. Authentication

Every webhook is signed with your signature_key, which comes with your credentials. The X-Kamipay-Auth header carries the signature: an HMAC-SHA256 of the payload, keyed with your signature_key, as a lowercase hex string.

What kamiPay signs is the payload re-serialized as compact JSON, not the body as it arrives. To verify a webhook:

  1. Parse the body.
  2. Re-serialize it as compact JSON: no spaces after , and :, non-ASCII characters unescaped (UTF-8), and the keys in the order received.
  3. Compute the HMAC-SHA256 of that string with your signature_key.
  4. Compare it with X-Kamipay-Auth in constant time.

Don't hash the raw body. It arrives with spaces after , and :, and with non-ASCII characters escaped (\u00e3 for ã), so an HMAC over it never matches. Sorting the keys, or rebuilding the JSON from a map or a typed object, changes the bytes too and breaks the signature.

Amounts arrive as strings ("amount_brl": "5.28"), so parsing and re-serializing keeps them exact.

5. Client-Side Code Example

In your local application, create a new route that can accept POST requests and verifies the signature before anything else.

For example, an API route on Next.js. JSON.stringify already re-serializes the parsed body the way kamiPay signs it: compact, non-ASCII unescaped, keys in the order received.

// app/api/webhooks/route.ts
import crypto from "crypto";

export async function POST(req: Request) {
  // X-Kamipay-Auth carries the HMAC-SHA256 of the payload, keyed with your signature_key.
  const signature = Buffer.from(req.headers.get("X-Kamipay-Auth") ?? "");

  const body = await req.json();

  // Re-serialize the payload as compact JSON and sign it with your signature_key.
  const expected = Buffer.from(
    crypto
      .createHmac("sha256", process.env.KAMIPAY_SIGNATURE_KEY!)
      .update(JSON.stringify(body))
      .digest("hex"),
  );

  // Compare in constant time. If they match, the payload was not tampered with in transit.
  if (signature.length !== expected.length || !crypto.timingSafeEqual(signature, expected)) {
    return new Response("Webhook Error: Unauthorized", { status: 403 });
  }

  return new Response(null, { status: 200 });
}

For example, a Flask route. Python's json.dumps adds spaces and escapes non-ASCII characters by default, so pass compact separators and ensure_ascii=False.

import hashlib
import hmac
import json
import os

from flask import Flask, request

SIGNATURE_KEY = os.environ["KAMIPAY_SIGNATURE_KEY"]

app = Flask(__name__)


def is_valid_signature(raw_body: bytes, signature: str) -> bool:
    # Re-serialize the payload as compact JSON: no spaces, non-ASCII unescaped,
    # keys in the order received (json.loads keeps it).
    payload = json.loads(raw_body)
    message = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
    expected = hmac.new(SIGNATURE_KEY.encode("utf-8"), message, hashlib.sha256).hexdigest()
    # Compare in constant time.
    return hmac.compare_digest(expected.encode("utf-8"), signature.encode("utf-8"))


@app.post("/webhooks/kamipay")
def kamipay_webhook():
    if not is_valid_signature(request.get_data(), request.headers.get("X-Kamipay-Auth", "")):
        return "Webhook Error: Unauthorized", 403
    return "", 200

Other languages: keep the keys in the order received. In Go, map[string]any re-serializes with its keys sorted, and in Java a HashMap or a typed object may not keep the order either, so parse into an order-preserving structure (for example, Jackson's ObjectMapper.readTree). Turn off HTML escaping as well: Go's encoding/json and Gson escape characters such as <, > and & by default, and kamiPay doesn't.

Always validate the signature of incoming webhook requests to ensure they are authentic and sent by kamiPay.

Webhook Types

kamiPay provides two main types of webhooks to track different payment flows. The Charge webhook notifies you when a customer sends a Pix payment. The Pay webhook provides updates about outgoing payments made from your account to third parties. Select one of the options below for detailed specifications:

On this page