Why you'd need this
WebSocket connections are the right call when you need every update the instant it happens and you're fine holding a persistent connection open to get it. But a lot of systems don't need that. They need to know when a specific thing happens, a goal, a match ending, a status change, and are otherwise fine doing nothing. That's what webhooks are for, and Go is a genuinely good fit for a small, fast, always-on receiver that just needs to sit there and respond to POST requests without much overhead.
This is a short walkthrough of standing up a minimal webhook receiver in Go for match events.
Before you start
You'll need an API key to register a webhook endpoint. There's a free tier that's enough to test this end to end: https://orbistats.com/signup.html
If you want to see what an actual event payload looks like before writing the handler, the sandbox is useful for that even without an account: https://orbistats.com/developers/sandbox.html
A minimal receiver
go
package main
import (
"encoding/json"
"io"
"log"
"net/http"
)
type MatchEvent struct {
Type string json:"type"
MatchID string json:"match_id"
Minute int json:"minute"
Team string json:"team,omitempty"
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "could not read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
var event MatchEvent
if err := json.Unmarshal(body, &event); err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
return
}
log.Printf("received event: %s for match %s at minute %d", event.Type, event.MatchID, event.Minute)
w.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("/webhooks/orbistats", webhookHandler)
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
A note before you copy this: the MatchEvent struct fields above (type, match_id, minute, team) are written based on the general documented pattern for match event payloads, not a payload I've inspected directly. Verify the actual event schema and available event types against the webhooks documentation before wiring this into anything real: https://orbistats.com/api/webhooks-api.html
Verifying the request actually came from the provider
An endpoint that accepts any POST from the internet and trusts it is a problem waiting to happen. Most webhook implementations sign the payload with a secret so you can verify it wasn't forged. A rough shape for that check, assuming a signature header and HMAC:
go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func verifySignature(payload []byte, signature string, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
Check the webhooks docs for the actual header name and signing method used before assuming this exact HMAC approach applies as written: https://orbistats.com/api/webhooks-api.html. Skipping signature verification on a public endpoint is the kind of shortcut that's fine in local testing and a real problem once the URL is live.
Responding fast, processing slow
One thing worth building in from the start: your handler should acknowledge the webhook quickly and do the actual work asynchronously, rather than making the sender wait while you update a database or trigger something downstream. A channel and a worker goroutine is enough for most cases:
go
eventChan := make(chan MatchEvent, 100)
go func() {
for event := range eventChan {
// actual processing happens here, off the request path
log.Printf("processing %s", event.Type)
}
}()
Slow processing inside the handler itself is a common reason webhook deliveries start timing out and getting retried more than expected.
When you actually want WebSocket instead
Webhooks are event-driven and low-effort to run, but they're not the right fit if you need continuous state, like a live score ticking every few seconds rather than discrete events. That's a different transport and a different mental model: https://orbistats.com/developers/documentation.html
Where to go from here
This covers receiving events, not the full range of event types or the registration flow for setting up the webhook URL in the first place. Both are in the full reference: https://orbistats.com/developers/api-reference.html
Curious how others are handling webhook retries and idempotency in Go, are you deduplicating on event ID, or just accepting that occasional duplicate processing is cheaper to handle downstream than to prevent upstream?

Top comments (0)