Webhooks#
Webhooks notify a backend service of yours when an event of interest happens on a private BSR instance. Common uses:
- Trigger downstream CI/CD on a successful
buf push. - Notify a Slack channel or issue tracker when modules change.
- Tag a Git repository in response to a BSR commit.
Webhooks are in alpha with a single supported event, WEBHOOK_EVENT_REPOSITORY_PUSH, fired on a successful buf push.
The buf.alpha.webhook.v1alpha1 and buf.alpha.registry.v1alpha1 packages and the buf beta registry webhook CLI commands are part of that alpha surface; expect them to change.
This feature is private-instance only and isn’t available on the public buf.build.
In every example below, replace your-bsr-instance.example.com with the domain of your private BSR instance.
How webhooks work#
Two pieces are involved: an event listener you run, and a webhook subscription on the BSR.
The callback contract for the listener:
- HTTPS URL.
- Path suffix
/buf.alpha.webhook.v1alpha1.EventService/Event, with no query string or fragment. - Accepts
Content-Type: application/protoand decodes the body as anEventRequest. - Returns the matching
EventResponse.
The BSR delivers events in batches of up to 20 every 5 seconds by default. On on-prem deployments these limits are configurable. Delivery is best-effort: events aren’t signed and aren’t retried, so treat the channel as at-most-once and reconcile from the BSR if you need stronger guarantees.
Enable webhooks on your instance#
Webhooks are disabled by default on private BSR instances and enabled by default on on-prem deployments. To turn them on for a private BSR instance, contact Support or your Buf representative.
Write an event listener#
The example below uses generated Go and the Connect library to implement the Webhook Event service:
$ go get buf.build/gen/go/bufbuild/buf/protocolbuffers/go
$ go get buf.build/gen/go/bufbuild/buf/connectrpc/go
package main
import (
"context"
"fmt"
"net/http"
webhookconnect "buf.build/gen/go/bufbuild/buf/connectrpc/gosimple/buf/alpha/webhook/v1alpha1/webhookv1alpha1connect"
registryv1alpha1 "buf.build/gen/go/bufbuild/buf/protocolbuffers/go/buf/alpha/registry/v1alpha1"
webhookv1alpha1 "buf.build/gen/go/bufbuild/buf/protocolbuffers/go/buf/alpha/webhook/v1alpha1"
)
type webhookEventHandler struct{}
func (h *webhookEventHandler) Event(
_ context.Context,
req *webhookv1alpha1.EventRequest,
) (*webhookv1alpha1.EventResponse, error) {
// Handle the type-safe incoming request for the push event:
switch req.GetEvent() {
case registryv1alpha1.WebhookEvent_WEBHOOK_EVENT_REPOSITORY_PUSH:
pushEvent := req.GetPayload().GetRepositoryPush()
fmt.Println("received repo push event:", pushEvent)
default:
fmt.Println("unknown event:", req.GetEvent())
}
// Webhook listener has an empty response
return &webhookv1alpha1.EventResponse{}, nil
}
// Connect handler based on: https://connectrpc.com/docs/go/getting-started#implement-handler
func main() {
mux := http.NewServeMux()
mux.Handle(webhookconnect.NewEventServiceHandler(&webhookEventHandler{}))
http.ListenAndServe("localhost:8080", mux)
}
If you can’t use Connect, any HTTP server that satisfies the callback contract above works: accept application/proto, decode the body as EventRequest, return the matching EventResponse.
Manage subscriptions#
Subscriptions are scoped to a single repository. Each repository event allows a single webhook, so a repository has at most one webhook subscription. Creating, listing, and deleting subscriptions requires the Admin role on the repository.
With the Buf CLI#
The recommended path:
$ buf beta registry webhook create \
--owner="<the organization that owns the repository>" \
--repository="<the repository name>" \
--callback-url="https://your.callback.url/buf.alpha.webhook.v1alpha1.EventService/Event" \
--event="WEBHOOK_EVENT_REPOSITORY_PUSH" \
--remote="your-bsr-instance.example.com"
$ buf beta registry webhook list \
--owner="<the organization that owns the repository>" \
--repository="<the repository name>" \
--remote="your-bsr-instance.example.com"
$ buf beta registry webhook delete \
--id="the-webhook-id-that-will-be-deleted" \
--remote="your-bsr-instance.example.com"
With a Go Connect client#
For programmatic management from a Go service, generate the WebhookService client and use it directly:
package main
import (
"context"
"log"
"net/http"
registryv1alpha1 "buf.build/gen/go/bufbuild/buf/connectrpc/go/buf/alpha/registry/v1alpha1"
registryconnect "buf.build/gen/go/bufbuild/buf/connectrpc/go/buf/alpha/registry/v1alpha1/registryv1alpha1connect"
connect "connectrpc.com/connect"
)
func main() {
connectClient := registryconnect.NewWebhookServiceClient(
http.DefaultClient,
"https://your-bsr-instance.example.com",
)
createReq := connect.NewRequest(®istryv1alpha1.CreateWebhookRequest{
WebhookEvent: registryv1alpha1.WebhookEvent_WEBHOOK_EVENT_REPOSITORY_PUSH,
OwnerName: "ORG_NAME_OR_USERNAME",
RepositoryName: "REPOSITORY_NAME",
CallbackUrl: "https://your.callback.url/buf.alpha.webhook.v1alpha1.EventService/Event",
})
createReq.Header().Add("Authorization", "Bearer YOUR_API_TOKEN")
createResp, err := connectClient.CreateWebhook(context.Background(), createReq)
if err != nil {
log.Fatalf("creating webhook failed: %v", err)
}
webhook := createResp.Msg.Webhook
if webhook == nil {
log.Fatal("nil webhook response")
}
log.Println("new webhook created!", webhook)
listReq := connect.NewRequest(®istryv1alpha1.ListWebhooksRequest{
OwnerName: "ORG_NAME_OR_USERNAME",
RepositoryName: "REPOSITORY_NAME",
})
listReq.Header().Add("Authorization", "Bearer YOUR_API_TOKEN")
listResp, err := connectClient.ListWebhooks(context.Background(), listReq)
if err != nil {
log.Fatalf("list webhooks failed: %v", err)
}
log.Println("existing webhooks:", listResp)
deleteReq := connect.NewRequest(®istryv1alpha1.DeleteWebhookRequest{
WebhookId: webhook.WebhookId,
})
deleteReq.Header().Add("Authorization", "Bearer YOUR_API_TOKEN")
deleteResp, err := connectClient.DeleteWebhook(context.Background(), deleteReq)
if err != nil {
log.Fatalf("delete webhook failed: %v", err)
}
log.Println("webhook deleted:", deleteResp)
}
With curl#
Because the BSR is also written with Connect handlers, the same RPCs work over plain HTTP with JSON.
$ curl --location --request POST 'https://your-bsr-instance.example.com/buf.alpha.registry.v1alpha1.WebhookService/CreateWebhook' \
--header 'Authorization: Bearer <BSR api token>' \
--header 'Content-Type: application/json' \
--data-raw '{
"owner_name": "<the organization that owns the repository>",
"repository_name": "<the repository name>",
"webhook_event": "WEBHOOK_EVENT_REPOSITORY_PUSH",
"callback_url": "https://your.callback.url/buf.alpha.webhook.v1alpha1.EventService/Event"
}'
$ curl --location --request POST 'https://your-bsr-instance.example.com/buf.alpha.registry.v1alpha1.WebhookService/ListWebhooks' \
--header 'Authorization: Bearer <your BSR api token>' \
--header 'Content-Type: application/json' \
--data-raw '{
"owner_name": "<the organization that owns the repository>",
"repository_name": "<the repository name>"
}'
$ curl --location --request POST 'https://your-bsr-instance.example.com/buf.alpha.registry.v1alpha1.WebhookService/DeleteWebhook' \
--header 'Authorization: Bearer <your BSR api token>' \
--header 'Content-Type: application/json' \
--data-raw '{
"webhook_id": "the-webhook-id-that-will-be-deleted"
}'
Test your listener#
Listeners accept application/proto, not JSON, so a useful local test sends an EventRequest already encoded as Protobuf binary.
The snippet below pipes a JSON EventRequest through buf beta convert to produce the binary payload, then posts it to your callback URL with curl:
$ echo '{
"event": "WEBHOOK_EVENT_REPOSITORY_PUSH",
"payload": {
"repositoryPush": {
"eventTime": "2022-07-11T15:07:30Z",
"repository": {
"id": "my-repo-id",
"name": "my-repo-name",
"createTime": "2022-07-10T15:07:30Z",
"updateTime": "2022-07-10T18:07:30Z",
"userId": "the-user-id",
"visibility": "VISIBILITY_PUBLIC"
},
"repositoryCommit": {
"author": "the-author-username",
"commitSequenceId": 10,
"createTime": "2022-07-11T15:07:30Z",
"id": "the-commit-id",
"name": "the-commit-name",
"digest": "the-commit-digest",
"tags": [
{
"author": "the-tag-author",
"commitName": "the-commit-hash",
"id": "the-tag-id",
"createTime": "2022-07-11T15:07:30Z",
"name": "the-tag-name"
}
]
}
}
}
}' | \
buf beta convert buf.build/bufbuild/buf \
--input=-#format=json \
--type buf.alpha.webhook.v1alpha1.EventRequest | \
curl -X POST https://your.callback.url/buf.alpha.webhook.v1alpha1.EventService/Event \
-H "Content-Type: application/proto" \
--data-binary @-
Payload reference#
EventRequest: the body of every webhook delivery; forWEBHOOK_EVENT_REPOSITORY_PUSH, therepositoryPushpayload includes the commit (with author) and the repository it was pushed to.EventResponse: the response your listener returns.WebhookEvent: enum of supported events; today onlyWEBHOOK_EVENT_REPOSITORY_PUSH.