Send Webhooks with Go
This guide will show you how to send webhooks with Go using Hookdeck's Outpost — an open-source event dispatcher. Outpost gives you a world-class webhook and event destination infrastructure in minutes for 1/10th the cost of alternatives.
Outpost supports Event Destinations, allowing you to deliver events to webhook endpoints, as well as to queues, and popular brokers or event gateways. These include AWS SQS, RabbitMQ, GCP Pub/Sub, Amazon EventBridge, Kafka, and more—all from a single platform.
The team behind it has drawn on their experience running Hookdeck Event Gateway and delivering over 100 billion events to thousands of customers.
Why use Outpost?
Sending webhooks can be trickier than you think. From managing fan‑out load, retries and backoff, to endpoint health management, and security/operational overhead. Outpost takes care of all of that so you only need to think about is how to send a webhook.
How to Send Webhooks with Go + Outpost
Start by deploying an Outpost instance locally, via Docker, or deploy to production. For guidance, check out the Outpost quickstart or the GitHub repository.
Go Support in Outpost
You can interact with Outpost using the Go SDK or the REST API. Each SDK provides a convenient way to interact with the Outpost API, including publishing events, managing topics, and configuring destinations.
go get github.com/hookdeck/outpost/sdks/outpost-go
Set Your Admin API Key
Before you can start making calls to the Outpost API you need to set an API key. The API uses bearer token authentication with a token of your choice configured through the API_KEY environment variable.
Send Webhooks (Publish Events)
In order to send a webhook, you have to instantiate Outpost, create a tenant, and create a destination for that tenant. Then you’re ready to publish events to it.
Create an Outpost instance
outpostgo.New()
You can set the security parameters through the security optional parameter when initializing the SDK client instance. The selected scheme will be used by default to authenticate with the API for all operations that support it. For example:
outpostgo.New(
outpostgo.WithSecurity(components.Security{
AdminAPIKey: outpostgo.String("<YOUR_BEARER_TOKEN_HERE>"),
}),
)
Create a new tenant.
A tenant in Outpost represents a user/team/organization in your product. Use the Upsert() method to idempotently create or update a tenant: s.Tenants.Upsert(ctx)
Create a destination for that tenant.
The Create() method is used to specify a new destination for the tenant. A destination is a specific instance of a destination type. For example, a webhook destination with a particular URL.
The request body structure depends on the destination type. Here we’re creating a destination for a webhook, but it could be SQS, RabbitMQ, etc.
s.Destinations.Create(ctx, components.CreateDestinationCreateWebhook(
components.DestinationCreateWebhook{
TenantID: outpostgo.String("<TENANT_ID>"),
ID: outpostgo.String("user-provided-id"),
Type: components.DestinationCreateWebhookTypeWebhook,
Topics: components.CreateTopicsTopicsEnum(
components.TopicsEnumWildcard,
),
Config: components.WebhookConfig{
URL: "https://example.com/webhook-receiver",
},
},
))
Note that we specify topics. A topic is a way to categorize events and is a common concept found in Pub/Sub messaging.
Publish an event for the created tenant.
You can now use the Publish() method to send an event to the specified topic, routed to a specific destination.
s.Publish.Event(ctx, components.PublishRequest{
ID: outpostgo.String("evt_custom_123"),
TenantID: outpostgo.String("<TENANT_ID>"),
DestinationID: outpostgo.String("<DESTINATION_ID>"),
Topic: outpostgo.String("topic.name"),
Metadata: map[string]string{
"source": "crm",
},
Data: map[string]any{
"user_id": "userid",
"status": "active",
},
})
ID: Is optional but recommended. If left empty, it will be generated by hashing the topic, data, and timestamp.
TenantID: The tenant ID to publish the event must match an existing tenant, otherwise it will be ignored.
DestinationID: Optional. Used to force delivery to a specific destination regardless of the topic.
Topic: Optional. Assumed to match ANY topic if left empty. If set, it must match one of the configured topics.
Metadata: Arbitrary key-value mapping for event metadata.
Data: The Data JSON object will be sent as the webhook body. Now, every time you publish an event to this tenant, Outpost will send an HTTP POST request with the data payload to the destination URL.
Complete Example
Combining the examples above, you should end up with something that looks like the following:
main.go
package main
import (
"log"
"os"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Println("No .env file found, proceeding without it")
}
adminAPIKey := os.Getenv("ADMIN_API_KEY")
serverURL := os.Getenv("SERVER_URL")
if serverURL == "" {
serverURL = "http://localhost:3333"
log.Printf("SERVER_URL not set, defaulting to %s", serverURL)
}
if adminAPIKey == "" {
log.Println("Warning: ADMIN_API_KEY environment variable not set. Some examples might fail.")
}
if len(os.Args) < 2 {
log.Println("Usage: go run . <example_name>")
log.Println("Available examples: manage, auth, create-destination, publish-event")
os.Exit(1)
}
exampleToRun := os.Args[1]
switch exampleToRun {
case "manage":
if adminAPIKey == "" {
log.Fatal("ADMIN_API_KEY environment variable must be set to run the 'manage' example.")
}
log.Println("--- Running Manage Outpost Resources Example ---")
manageOutpostResources(adminAPIKey, serverURL)
case "auth":
log.Println("--- Running Auth Example ---")
runAuthExample()
case "create-destination":
if adminAPIKey == "" {
log.Fatal("ADMIN_API_KEY environment variable must be set to run the 'create-destination' example.")
}
log.Println("--- Running Create Destination Example ---")
runCreateDestinationExample()
case "publish-event":
if adminAPIKey == "" {
log.Fatal("ADMIN_API_KEY environment variable must be set to run the 'publish-event' example.")
}
log.Println("--- Running Publish Event Example ---")
runPublishEventExample()
default:
log.Printf("Unknown example: %s\n", exampleToRun)
log.Println("Available examples: manage, auth, create-destination, publish-event")
os.Exit(1)
}
}
auth.go
package main
import (
"context"
"fmt"
"log"
"os"
outpostgo "github.com/hookdeck/outpost/sdks/outpost-go"
"github.com/hookdeck/outpost/sdks/outpost-go/models/components"
"github.com/joho/godotenv"
)
func withJwt(ctx context.Context, jwt string, serverURL string, tenantID string) {
log.Println("--- Running with Tenant JWT ---")
apiServerURL := fmt.Sprintf("%s/api/v1", serverURL)
jwtClient := outpostgo.New(
outpostgo.WithSecurity(components.Security{
TenantJwt: outpostgo.String(jwt),
}),
outpostgo.WithServerURL(apiServerURL),
)
destRes, err := jwtClient.Destinations.List(ctx, outpostgo.String(tenantID), nil, nil)
if err != nil {
log.Fatalf("Failed to list destinations with JWT: %v", err)
}
if destRes != nil && destRes.Destinations != nil {
log.Printf("Successfully listed %d destinations using JWT.", len(destRes.Destinations))
} else {
log.Println("List destinations with JWT returned no data or an unexpected response structure.")
}
}
func withAdminApiKey(ctx context.Context, serverURL string, adminAPIKey string, tenantID string) {
log.Println("--- Running with Admin API Key ---")
apiServerURL := fmt.Sprintf("%s/api/v1", serverURL)
adminClient := outpostgo.New(
outpostgo.WithSecurity(components.Security{
AdminAPIKey: outpostgo.String(adminAPIKey),
}),
outpostgo.WithServerURL(apiServerURL),
)
healthRes, err := adminClient.Health.Check(ctx)
if err != nil {
log.Fatalf("Health check failed: %v", err)
}
if healthRes != nil && healthRes.Res != nil {
log.Printf("Health check successful. Details: %s", *healthRes.Res)
} else {
log.Println("Health check returned no data or an unexpected response structure.")
}
destRes, err := adminClient.Destinations.List(ctx, outpostgo.String(tenantID), nil, nil)
if err != nil {
log.Fatalf("Failed to list destinations with Admin Key: %v", err)
}
if destRes != nil && destRes.Destinations != nil {
log.Printf("Successfully listed %d destinations using Admin Key for tenant %s.", len(destRes.Destinations), tenantID)
} else {
log.Println("List destinations with Admin Key returned no data or an unexpected response structure.")
}
tokenRes, err := adminClient.Tenants.GetToken(ctx, outpostgo.String(tenantID))
if err != nil {
log.Fatalf("Failed to get tenant token: %v", err)
}
if tokenRes != nil && tokenRes.TenantToken != nil && tokenRes.TenantToken.Token != nil {
log.Printf("Successfully obtained tenant JWT for tenant %s.", tenantID)
withJwt(ctx, *tokenRes.TenantToken.Token, serverURL, tenantID)
} else {
log.Println("Get tenant token returned no data or an unexpected response structure.")
}
}
// Renamed main to runAuthExample to avoid conflict
func runAuthExample() {
err := godotenv.Load()
if err != nil {
log.Println("No .env file found, proceeding without it")
}
serverURL := os.Getenv("SERVER_URL")
adminAPIKey := os.Getenv("ADMIN_API_KEY")
tenantID := os.Getenv("TENANT_ID")
if serverURL == "" {
log.Fatal("SERVER_URL environment variable not set")
}
if adminAPIKey == "" {
log.Fatal("ADMIN_API_KEY environment variable not set")
}
if tenantID == "" {
log.Fatal("TENANT_ID environment variable not set")
}
ctx := context.Background()
withAdminApiKey(ctx, serverURL, adminAPIKey, tenantID)
log.Println("--- Auth example finished ---")
}
create_destination.go
package main
import (
"context"
"fmt"
"log"
"os"
outpostgo "github.com/hookdeck/outpost/sdks/outpost-go"
"github.com/hookdeck/outpost/sdks/outpost-go/models/components"
"github.com/manifoldco/promptui"
)
func runCreateDestinationExample() {
adminAPIKey := os.Getenv("ADMIN_API_KEY")
if adminAPIKey == "" {
log.Fatal("Please set the ADMIN_API_KEY environment variable.")
}
tenantID := os.Getenv("TENANT_ID")
if tenantID == "" {
tenantID = "hookdeck"
}
serverURL := os.Getenv("OUTPOST_URL")
if serverURL == "" {
serverURL = "http://localhost:3333"
}
client := outpostgo.New(
outpostgo.WithSecurity(components.Security{
AdminAPIKey: &adminAPIKey,
}),
outpostgo.WithServerURL(fmt.Sprintf("%s/api/v1", serverURL)),
)
_, err := client.Tenants.Upsert(context.Background(), &tenantID)
if err != nil {
log.Fatalf("Error upserting tenant: %v", err)
}
promptConnectionString := promptui.Prompt{
Label: "Enter Azure Service Bus Connection String",
}
connectionString, err := promptConnectionString.Run()
if err != nil {
log.Fatalf("Prompt failed %v\n", err)
}
promptTopicOrQueue := promptui.Prompt{
Label: "Enter Azure Service Bus Topic or Queue name",
}
topicOrQueueName, err := promptTopicOrQueue.Run()
if err != nil {
log.Fatalf("Prompt failed %v\n", err)
}
topics := components.CreateTopicsTopicsEnum(components.TopicsEnumWildcard)
destination := components.CreateDestinationCreateAzureServicebus(
components.DestinationCreateAzureServiceBus{
Topics: topics,
Config: components.AzureServiceBusConfig{
Name: topicOrQueueName,
},
Credentials: components.AzureServiceBusCredentials{
ConnectionString: connectionString,
},
},
)
createRes, err := client.Destinations.Create(context.Background(), destination, &tenantID)
if err != nil {
log.Fatalf("Error creating destination: %v", err)
}
if createRes.Destination != nil {
fmt.Println("Destination created successfully:")
// Using a simple print for brevity, a real application might use JSON marshalling
fmt.Printf(" ID: %s\n", createRes.Destination.DestinationAzureServiceBus.ID)
fmt.Printf(" Name: %s\n", createRes.Destination.DestinationAzureServiceBus.Config.Name)
fmt.Printf(" Type: %s\n", createRes.Destination.Type)
} else {
fmt.Println("Destination creation did not return a destination object.")
}
}
publish_event.go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
outpostgo "github.com/hookdeck/outpost/sdks/outpost-go"
"github.com/hookdeck/outpost/sdks/outpost-go/models/components"
)
func runPublishEventExample() {
adminAPIKey := os.Getenv("ADMIN_API_KEY")
if adminAPIKey == "" {
log.Fatal("Please set the ADMIN_API_KEY environment variable.")
}
tenantID := os.Getenv("TENANT_ID")
if tenantID == "" {
tenantID = "hookdeck"
}
serverURL := os.Getenv("OUTPOST_URL")
if serverURL == "" {
serverURL = "http://localhost:3333"
}
client := outpostgo.New(
outpostgo.WithSecurity(components.Security{
AdminAPIKey: &adminAPIKey,
}),
outpostgo.WithServerURL(fmt.Sprintf("%s/api/v1", serverURL)),
)
topic := "order.created"
payload := map[string]interface{}{
"order_id": "ord_2Ua9d1o2b3c4d5e6f7g8h9i0j",
"customer_id": "cus_1a2b3c4d5e6f7g8h9i0j",
"total_amount": "99.99",
"currency": "USD",
"items": []map[string]interface{}{
{
"product_id": "prod_1a2b3c4d5e6f7g8h9i0j",
"name": "Example Product 1",
"quantity": 1,
"price": "49.99",
},
{
"product_id": "prod_9z8y7x6w5v4u3t2s1r0q",
"name": "Example Product 2",
"quantity": 1,
"price": "50.00",
},
},
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
log.Fatalf("Failed to marshal payload: %v", err)
}
var data map[string]interface{}
err = json.Unmarshal(payloadBytes, &data)
if err != nil {
log.Fatalf("Failed to unmarshal payload into data: %v", err)
}
request := components.PublishRequest{
Topic: &topic,
Data: data,
TenantID: &tenantID,
}
res, err := client.Publish.Event(context.Background(), request)
if err != nil {
log.Fatalf("Error publishing event: %v", err)
}
if res.HTTPMeta.Response.StatusCode == 202 {
fmt.Println("Event published successfully")
if res.PublishResponse != nil {
fmt.Printf("Event ID: %s\n", res.PublishResponse.GetID())
}
} else {
fmt.Printf("Failed to publish event. Status code: %d\n", res.HTTPMeta.Response.StatusCode)
}
}
Expose Tenant User Portal
Your application can offer the user the option to configure their destination, view and retry their events using a themeable Tenant User Portal. Note that if the portal is used, then the Outpost API needs to be exposed to the public internet (or proxy requests to the Outpost API).
You can use GetPortalURL to return a redirect URL containing a JWT to authenticate the user with the portal, like so: s.Tenants.GetPortalURL(ctx, nil). Then redirect your users to this URL or use it in an iframe.
Build your own UI
While Outpost offers a user portal, you may want to build your own UI for users to manage their destinations and view their events. The portal is built using the Outpost API with JWT authentication. You can leverage the same API to build your own UI.
Help with debugging and testing webhooks
To help you and your users work with webhooks, we provide some additional tools. Like Hookdeck Console, a web-based inspector to test and debug incoming webhooks. As well as the Hookdeck CLI to forward webhook events to a local web server.
More information
That's everything you need to start sending webhooks with Outpost. For more information, please check out the Outpost documentation.
If you have any questions, we’d love to help. Please join the Hookdeck community on Slack.