---
title: "Go SDK — Webhooks"
description: "Event webhooks with the Metalhost Go SDK — subscriptions, HMAC verification, and delivery logs."
url: "https://metalhost.net/docs/developers/guides/sdk-webhooks"
---

# Go SDK — Webhooks

`WebhooksServiceClient` registers HTTPS endpoints for resource events. Deliveries are signed with HMAC-SHA256 (`X-Metalhost-Signature`).

## Event types

Convention: `{domain}.{resource}.{verb}`

- `compute.vm.created` / `compute.vm.deleted` / `compute.vm.state_changed`
- `storage.disk.created` / `storage.disk.deleted`
- `ops.operation.completed`
- `billing.invoice.finalized`

Subscribe to specific types or wildcards: `compute.vm.*`

## Create a subscription

```
hooks := webhooksv1connect.NewWebhooksServiceClient(httpClient, base)
resp, err := hooks.CreateSubscription(ctx, connect.NewRequest(
    &webhooksv1.CreateSubscriptionRequest{
        Name:        "webhook-subscriptions/deploy-hook",
        ProjectName: "projects/my-app",
        EndpointUrl: "https://hooks.example.com/metalhost",
        EventTypes:  []string{"compute.vm.*", "ops.operation.completed"},
    },
))
secret := resp.Msg.GetSecret() // HMAC signing secret — shown once
```

`endpointUrl` must be HTTPS in production. Subscription name: `webhook-subscriptions/{slug}`.

## Manage subscriptions

```
hooks.ListSubscriptions(ctx, connect.NewRequest(&webhooksv1.ListSubscriptionsRequest{ProjectName: project}))
hooks.UpdateSubscription(ctx, ...) // pause/resume via state
hooks.DeleteSubscription(ctx, connect.NewRequest(&webhooksv1.DeleteSubscriptionRequest{Name: subName}))
```

## Delivery log

```
hooks.ListDeliveries(ctx, connect.NewRequest(&webhooksv1.ListDeliveriesRequest{
    SubscriptionName: subName,
    PageSize: 50,
}))
```

Paginated delivery records include status code, response snippet and retry count. Follow `nextPageToken` with `pageToken`; one response is not the entire retained history.

## Timestamped signatures

> **SDK v1.1.2**
>
> `metalhost.VerifyWebhook(headers, rawBody, secret, time.Now())` verifies V2 signatures, the signed delivery and attempt IDs, and a five-minute clock window. It does not fall back to the legacy signature below.

Bound the raw body to 1 MiB before reading, verify before JSON decoding, and atomically persist it with a unique verified delivery ID before acknowledging. Retries change attempt IDs, not delivery IDs. An in-memory deduplication map is not sufficient across restarts or replicas. See the SDK's `examples/webhookreceiver` package and [rotation and overlap guidance](https://metalhost.net/docs/developers/observability.md#webhooks).

## Legacy signatures

```
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

func verify(payload []byte, signatureHeader, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signatureHeader))
}
```

This legacy raw-body signature does not authenticate a timestamp or delivery ID and cannot enforce a replay age. Retain it only for older deployments; migrate to V2 before relying on replay-window protection.

Also check `X-Metalhost-Subscription` header matches your subscription id. Reject replays with event id deduplication on your side.

> **operation.completed**
>
> Pair `ops.operation.completed` with VM create/delete automation instead of polling `GetOperation` in tight loops.

## What's next

- [Concepts → Operations](https://metalhost.net/docs/developers/concepts.md#operations)
- [API reference → WebhooksService](https://metalhost.net/docs/developers/api.md)
