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

# Webhooks

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

Webhooks are signed with your API key using HMAC-SHA256.

| Header               | Value                                                                               |
| -------------------- | ----------------------------------------------------------------------------------- |
| `X-Webhook-Delivery` | Delivery id, 1–128 characters `[A-Za-z0-9_-]`; the same on every attempt and resend |
| `X-CC-Timestamp`     | Unix time of the attempt, seconds                                                   |
| `X-CC-Signature`     | `v1=` + 64 hex characters                                                           |

The string to sign is four lines joined with `\n`, with no line break after the last one:

```
CC-HMAC-SHA256-WEBHOOK-V1
<X-CC-Timestamp>
<X-Webhook-Delivery>
<lowercase hex SHA-256 of the body>
```

`X-CC-Signature` is `v1=` followed by the lowercase hex HMAC-SHA256 of that string under your API key.

`body` is the raw request body. The SDK checks, in order: the three headers, the timestamp window (300 seconds), the signature (constant-time, hex in any case).

A retry or resend carries the same `X-Webhook-Delivery` and a new `X-CC-Timestamp`. Deduplicate by the delivery id — see [Webhook deliveries](/processing/go/guides/webhook-deliveries.md#keep-the-delivery-id).

## Typed handler

`WebhookHandler` reads the raw body (up to 1 MiB), verifies it and decodes the event:

```go
mux.Handle("/webhook/payout", cryptochief.WebhookHandler[cryptochief.PayoutWebhookEvent](
    apiKey,
    func(w http.ResponseWriter, r *http.Request, evt cryptochief.PayoutWebhookEvent) {
        log.Printf("payout %s (order %s) → %s, %s to %s",
            evt.UUID, evt.OrderID, evt.Status, evt.AmountToReceive, evt.ToAddress)
    },
))
```

The handler answers `200` on its own if yours writes nothing, `401` if verification fails (the event is not decoded), `400` if the body does not decode into the event type, `405` for a method other than `POST`, and `500` if the API key is empty.

## Manual verification

For a custom HTTP stack, pass the raw body and the request headers:

```go
body, err := io.ReadAll(r.Body)
if err != nil {
    http.Error(w, "read body", http.StatusBadRequest)
    return
}
if err := cryptochief.VerifyWebhook(apiKey, body, r.Header); err != nil {
    http.Error(w, "bad signature", http.StatusUnauthorized)
    return
}
```

`VerifyWebhook` returns one of three errors; match them with `errors.Is`:

| Error                 | Cause                                                                     |
| --------------------- | ------------------------------------------------------------------------- |
| `ErrWebhookHeaders`   | A signature header is missing, repeated or malformed                      |
| `ErrWebhookTimestamp` | `X-CC-Timestamp` differs from the current time by more than the tolerance |
| `ErrWebhookSignature` | The signature does not match the body                                     |

An empty API key returns an error that matches none of them. `WithWebhookTolerance(d)` and `WithWebhookClock(now)` set the tolerance and the time source, in `VerifyWebhook` and `WebhookHandler` alike:

```go
err := cryptochief.VerifyWebhook(apiKey, body, r.Header,
    cryptochief.WithWebhookTolerance(60*time.Second),
    cryptochief.WithWebhookClock(time.Now),
)
switch {
case errors.Is(err, cryptochief.ErrWebhookTimestamp):
    // check the server clock
case err != nil:
    http.Error(w, "bad signature", http.StatusUnauthorized)
    return
}
```

`SignWebhookV1(apiKey, timestamp, deliveryID, body)` returns the `X-CC-Signature` value, for testing a receiver.

## 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 reach the network's finality depth in your master wallet — the other half of a deposit's life, and what treasury reporting should key off. See [Sweep callbacks](/processing/go/guides/sweep-callbacks.md).

Confirmation fields on events:

| Event type                | Field                                    | Meaning                                                                                                                                                   |
| ------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PayoutWebhookEvent`      | `Confirmations`                          | `*int`. The lowest count among the sources; `nil` while no source has a transaction. See [Send a payout](/processing/go/guides/payouts.md#confirmations). |
| `PayoutWebhookEvent`      | `RequiredConfirmations`                  | The network's finality depth, at least `1`; `0` when not sent.                                                                                            |
| `TransactionWebhookEvent` | `Confirmations`, `RequiredConfirmations` | Always sent. See [Sign & execute](/processing/go/guides/sign-execute.md#confirm).                                                                         |
| `SweepWebhookEvent`       | `Confirmations` (`sweep_confirmations`)  | At least `RequiredConfirmations`. See [Sweep callbacks](/processing/go/guides/sweep-callbacks.md#payload).                                                |
| `SweepWebhookEvent`       | `RequiredConfirmations`                  | The network's finality depth; `0` when not sent.                                                                                                          |

`Sources` and `ServiceOperations` on a payout event are raw JSON. Decode them into `[]PayoutSource` and `[]PayoutServiceOperation` to read each entry's `Confirmations`:

```go
var sources []cryptochief.PayoutSource
if len(evt.Sources) > 0 {
    if err := json.Unmarshal(evt.Sources, &sources); err != nil {
        return err
    }
}
```

## Absent, null and empty are one thing in Go

The platform is not consistent about how it says "no value": a native static deposit sends `"contract": ""`, while a native sweep omits `asset_contract` altogether. Every one of these fields decodes into a Go `string`, so both arrive as `""` and you test the same way — `evt.Contract == ""` means a native-coin transfer either way.

The timestamps behave likewise. `ConfirmedAt` and `PaidAt` on a static deposit are empty until the deposit is confirmed and paid; `BlockNumber` is `0` until the transaction is in a block, and `AmountFiat` is empty when no conversion rate was available. Check for the zero value rather than assuming the key was there.

The exception is a payout's confirmation count: `0` is a real count, so those fields are `*int` and `nil` means absent. `RequiredConfirmations` is never below `1`, so it is a plain `int` and `0` means absent.

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