> 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/java/guides/webhooks.md).

# Webhooks

Verify and handle Crypto Chief webhooks in Java 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.

## Spring Boot

Read the raw bytes off the request, then verify and decode in one step. `WebhookVerifier.parse(...)` returns the typed event you ask for:

```java
import com.cryptochief.processing.webhook.PayoutWebhookEvent;
import com.cryptochief.processing.webhook.WebhookSignatureException;
import com.cryptochief.processing.webhook.WebhookVerifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/webhook")
public class WebhookController {

    @Value("${cryptochief.api-key}")
    private String apiKey;

    @PostMapping("/payout")
    public ResponseEntity<String> handle(
            @RequestBody byte[] raw,
            @RequestHeader("Signature") String signature) {
        try {
            var event = WebhookVerifier.parse(apiKey, raw, signature, PayoutWebhookEvent.class);
            if ("paid".equals(event.status())) {
                // payout.paid — reconcile your ledger
            }
            return ResponseEntity.ok("");
        } catch (WebhookSignatureException e) {
            return ResponseEntity.status(401).body("bad signature");
        }
    }
}
```

## Servlet (Tomcat, Jetty)

```java
import com.cryptochief.processing.webhook.PayInWebhookEvent;
import com.cryptochief.processing.webhook.WebhookSignatureException;
import com.cryptochief.processing.webhook.WebhookVerifier;
import jakarta.servlet.http.*;

public class WebhookServlet extends HttpServlet {
    private final String apiKey = System.getenv("CRYPTO_CHIEF_API_KEY");

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        byte[] raw = req.getInputStream().readAllBytes();
        String signature = req.getHeader("Signature");
        try {
            var event = WebhookVerifier.parse(apiKey, raw, signature, PayInWebhookEvent.class);
            if ("paid".equals(event.status())) {
                // invoice.paid — fulfill order event.orderId()
            }
            resp.setStatus(200);
        } catch (WebhookSignatureException e) {
            resp.sendError(401, "bad signature");
        }
    }
}
```

## Plain JDK server

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

```java
import com.cryptochief.processing.webhook.PayoutWebhookEvent;
import com.cryptochief.processing.webhook.WebhookSignatureException;
import com.cryptochief.processing.webhook.WebhookVerifier;
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;

var server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/webhook", exchange -> {
    byte[] raw = exchange.getRequestBody().readAllBytes();
    String signature = exchange.getRequestHeaders().getFirst("Signature");
    try {
        var event = WebhookVerifier.parse(apiKey, raw, signature, PayoutWebhookEvent.class);
        System.out.println("payout " + event.uuid() + " → " + event.status());
        exchange.sendResponseHeaders(200, 0);
        exchange.getResponseBody().write("ok".getBytes());
        exchange.close();
    } catch (WebhookSignatureException e) {
        exchange.sendResponseHeaders(401, -1);
        exchange.close();
    }
});
server.start();
```

## Manual verification

For any other stack, verify the raw bytes yourself:

```java
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/java/guides/sweep-callbacks.md).

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

## How absence is spelled

The two deposit-side events disagree about how a missing value looks, and a `String` accessor cannot tell you which convention it just handed you.

`StaticDepositWebhookEvent.contract()` is an **empty string** for a native coin transfer — not `null`. That is the opposite of `WalletCoinBalance.contract()` on a wallet's balances, which is `null` for a native coin, so a helper shared between the two has to test for both.

```java
boolean isToken = event.contract() != null && !event.contract().isEmpty();
```

`SweepWebhookEvent` goes the other way: optional fields are **omitted** rather than sent empty, so `assetContract()` and `gasPumpTxHash()` arrive as `null` — a native sweep has no contract, and a null gas-pump hash means no gas had to be fronted. Sweep *history* sends the same absence as `""`; see [Auto-sweep settings](/processing/java/guides/auto-sweep-settings.md#sent-is-not-settled).

On a static deposit, `confirmedAt()` is `null` until the deposit confirms and `paidAt()` is `null` until it is paid, `amountFiat()` may be empty when no conversion was available, and `blockNumber()` is a nullable `Long` — present only after block inclusion.
