> For the complete documentation index, see [llms.txt](https://docs-sdk.crypto-chief.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-sdk.crypto-chief.com/processing/kotlin/guides/webhooks.md).

# Webhooks

Verify and handle Crypto Chief webhooks in Kotlin with typed event payloads.

Outbound webhooks are signed with the **same algorithm** as outgoing requests, so the same API key verifies them. The verification re-canonicalizes the body, so it tolerates key-order drift — but you must pass the **raw** request body, not a re-serialized object.

## Ktor

Read the raw bytes off the request, then verify and decode in one step. `WebhookHandler.handle<T>` returns the typed event you ask for:

```kotlin
import com.cryptochief.processing.webhook.PayoutWebhookEvent
import com.cryptochief.processing.webhook.WebhookHandler
import com.cryptochief.processing.webhook.WebhookSignatureException
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Application.webhookRouting(apiKey: String) = routing {
    post("/webhook/payout") {
        val raw = call.receiveStream().readAllBytes() // EXACT bytes — do not re-encode
        val signature = call.request.header("Signature")
        try {
            val event = WebhookHandler.handle<PayoutWebhookEvent>(apiKey, raw, signature)
            if (event.status == "paid") {
                // payout.paid — reconcile your ledger
            }
            call.respond(HttpStatusCode.OK)
        } catch (e: WebhookSignatureException) {
            call.respond(HttpStatusCode.Unauthorized)
        }
    }
}
```

## Spring Boot

```kotlin
import com.cryptochief.processing.webhook.PayInWebhookEvent
import com.cryptochief.processing.webhook.WebhookHandler
import com.cryptochief.processing.webhook.WebhookSignatureException
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/webhook")
class WebhookController(@Value("\${cryptochief.api-key}") private val apiKey: String) {

    @PostMapping("/payin")
    fun handle(
        @RequestBody raw: ByteArray,
        @RequestHeader("Signature") signature: String?,
    ): ResponseEntity<String> = try {
        val event = WebhookHandler.handle<PayInWebhookEvent>(apiKey, raw, signature)
        if (event.status == "paid") {
            // invoice.paid — fulfill order event.orderId
        }
        ResponseEntity.ok("")
    } catch (e: WebhookSignatureException) {
        ResponseEntity.status(401).body("bad signature")
    }
}
```

## Plain JDK server

The verification helpers do no I/O, so the JDK's built-in `HttpServer` works too:

```kotlin
import com.cryptochief.processing.webhook.PayoutWebhookEvent
import com.cryptochief.processing.webhook.WebhookHandler
import com.cryptochief.processing.webhook.WebhookSignatureException
import com.sun.net.httpserver.HttpServer
import java.net.InetSocketAddress

val server = HttpServer.create(InetSocketAddress(8080), 0)
server.createContext("/webhook") { exchange ->
    val raw = exchange.requestBody.readAllBytes()
    val signature = exchange.requestHeaders.getFirst("Signature")
    try {
        val event = WebhookHandler.handle<PayoutWebhookEvent>(apiKey, raw, signature)
        println("payout ${event.uuid} → ${event.status}")
        exchange.sendResponseHeaders(200, 0)
        exchange.responseBody.use { it.write("ok".toByteArray()) }
    } catch (e: WebhookSignatureException) {
        exchange.sendResponseHeaders(401, -1)
        exchange.close()
    }
}
server.start()
```

## Manual verification

For any other stack, verify the raw bytes yourself:

```kotlin
import com.cryptochief.processing.webhook.WebhookVerifier

if (!WebhookVerifier.verify(apiKey, rawBody, signatureHeader)) {
    // reject with 401
}
```

## Event types

Typed payloads: `PayoutWebhookEvent`, `TransactionWebhookEvent`, `PayInWebhookEvent`, `StaticDepositWebhookEvent`, `SweepWebhookEvent`. Event-name prefixes are `payout.*`, `transaction.*`, `invoice.*` (pay-ins), `static_deposit.*`, and `sweep.*`. Payout and transaction webhooks fire **only on terminal status**.

`sweep.confirmed` is the moment swept funds are confirmed in your master wallet — the other half of a deposit's life, and what treasury reporting should key off. See [Sweep callbacks](/processing/kotlin/guides/sweep-callbacks.md).

{% hint style="warning" %}
**Absent and empty mean different things from one event to the next.** A `static_deposit.*` payload sends `contract` as an **empty string** for a native-coin transfer, so `contract == null` is not the test for "this was a coin, not a token" — use `contract.isNullOrEmpty()`. Its `amountFiat` is likewise empty when no conversion rate was available, `blockNumber` arrives only after the transaction is in a block, and `confirmedAt` / `paidAt` stay `null` until the deposit reaches those points.

A `sweep.confirmed` payload does the opposite: fields the platform has no value for are **omitted**, so a native sweep decodes with `assetContract` as `null`, and a `null` `gasPumpTxHash` means no gas had to be fronted.
{% endhint %}

{% hint style="info" %}
`WebhookVerifier.SENDER_IPS` lists the addresses webhooks are delivered from — whitelist them at your edge for defence in depth.
{% endhint %}
