Wocha Docs
Examples

Go webhook handler

HTTP handler using the Go webhook SDK

Example HTTP handler that verifies Wocha webhook signatures and dispatches by event type.

Setup

go get github.com/wocha-dev/wocha/sdks/go/webhook

Set your signing secret:

export WOCHA_WEBHOOK_SECRET=whsec_...

HTTP handler

package main
 
import (
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
 
	greetwebhook "github.com/wocha-dev/wocha/sdks/go/webhook"
)
 
func main() {
	http.HandleFunc("/webhooks/wocha", greetWebhookHandler)
	log.Println("listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
 
func greetWebhookHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
 
	payload, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "failed to read body", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()
 
	secret := os.Getenv("WOCHA_WEBHOOK_SECRET")
	signature := r.Header.Get("X-Wocha-Signature")
 
	event, err := greetwebhook.Verify(payload, signature, secret)
	if err != nil {
		http.Error(w, "invalid signature", http.StatusUnauthorized)
		return
	}
 
	if err := handleWebhookEvent(event); err != nil {
		log.Printf("[webhook] handler error: %v", err)
		http.Error(w, "handler failed", http.StatusInternalServerError)
		return
	}
 
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("OK"))
}
 
func handleWebhookEvent(event *greetwebhook.Event) error {
	switch event.EventType {
	case "user.created":
		return provisionAccount(event.Data)
	case "user.updated":
		return syncUserProfile(event.Data)
	case "user.deleted":
		return deleteAccount(event.Data)
	case "session.created":
		return trackSession(event.Data)
	case "session.revoked":
		return invalidateSessionCache(event.Data)
	case "org.created", "organisation.created":
		return createOrganisationRecord(event.Data)
	case "org.member.added", "organisation.member_added":
		return syncMembership(event.Data)
	case "org.member.removed", "organisation.member_removed":
		return removeMembership(event.Data)
	case "connection.created":
		return syncConnection(event.Data)
	case "application.created":
		return registerApplication(event.Data)
	case "application.deleted":
		return unregisterApplication(event.Data)
	default:
		log.Printf("[webhook] unhandled event: %s", event.EventType)
		return nil
	}
}
 
func provisionAccount(data json.RawMessage) error {
	var body struct {
		ID    string `json:"id"`
		Email string `json:"email"`
	}
	if err := json.Unmarshal(data, &body); err != nil {
		return err
	}
	// Your provisioning logic
	return nil
}
 
func syncUserProfile(data json.RawMessage) error {
	return nil
}
 
func deleteAccount(data json.RawMessage) error {
	var body struct {
		ID string `json:"id"`
	}
	if err := json.Unmarshal(data, &body); err != nil {
		return err
	}
	// Your cleanup logic
	return nil
}
 
func trackSession(data json.RawMessage) error {
	return nil
}
 
func invalidateSessionCache(data json.RawMessage) error {
	var body struct {
		ID string `json:"id"`
	}
	if err := json.Unmarshal(data, &body); err != nil {
		return err
	}
	// Your cache invalidation logic
	return nil
}
 
func createOrganisationRecord(data json.RawMessage) error {
	return nil
}
 
func syncMembership(data json.RawMessage) error {
	return nil
}
 
func removeMembership(data json.RawMessage) error {
	return nil
}
 
func syncConnection(data json.RawMessage) error {
	return nil
}
 
func registerApplication(data json.RawMessage) error {
	return nil
}
 
func unregisterApplication(data json.RawMessage) error {
	return nil
}

Signature-only verification

If you prefer to verify and parse separately:

if !greetwebhook.VerifySignature(payload, signature, secret) {
	http.Error(w, "invalid signature", http.StatusUnauthorized)
	return
}
 
event, err := greetwebhook.Parse(payload)
if err != nil {
	http.Error(w, "invalid payload", http.StatusBadRequest)
	return
}

Custom tolerance

Reject signatures older than 10 minutes:

event, err := greetwebhook.Verify(payload, signature, secret, greetwebhook.VerifyOptions{
	Tolerance: 10 * time.Minute,
})

Important notes

  1. Read raw bytes — use io.ReadAll(r.Body) before JSON unmarshaling.
  2. Return 2xx quickly — enqueue heavy work; Wocha retries on timeout.
  3. Idempotency — deduplicate by event.IdempotencyKey before side effects.
  4. Legacy names — handle both org.* and organisation.* event types.

Local testing

wocha webhook listen --forward-to http://localhost:8080/webhooks/wocha
wocha webhook test http://localhost:8080/webhooks/wocha user.created

On this page