Go client for the Webhook Relay v1 API.
Webhook Relay receives webhooks and HTTP requests on a public endpoint and delivers them wherever you need — public URLs, services behind a firewall or on localhost, WebSocket clients, or on a schedule — with in-flight transformations, durable retries, throttling and fine-grained routing. This library configures all of it from Go.
go get github.com/webhookrelay/webhookrelay-goRequires a working Go environment (Go 1.23+).
Generate credentials here. There are two ways to authenticate:
-
API key (recommended) — a single key (looks like
sk-...) sent as a Bearer token:api, err := webhookrelay.NewWithAPIKey(os.Getenv("RELAY_API_KEY"))
-
Key & secret pair — sent as HTTP Basic auth:
api, err := webhookrelay.New(os.Getenv("RELAY_KEY"), os.Getenv("RELAY_SECRET"))
Create a bucket (which comes with a ready-to-use public input endpoint) and forward everything it receives to a destination:
package main
import (
"fmt"
"log"
"os"
"github.com/webhookrelay/webhookrelay-go"
)
func main() {
api, err := webhookrelay.NewWithAPIKey(os.Getenv("RELAY_API_KEY"))
if err != nil {
log.Fatal(err)
}
// A bucket groups an input (public URL) with one or more outputs (destinations).
bucket, err := api.CreateBucket(&webhookrelay.BucketCreateOptions{
Name: "sendgrid-to-segment",
})
if err != nil {
log.Fatal(err)
}
// Every bucket gets a default input. Point your webhook provider at this URL:
fmt.Println("Send webhooks to:", bucket.Inputs[0].EndpointURL())
// Forward everything the bucket receives to a public destination.
if _, err := api.CreateOutput(&webhookrelay.Output{
BucketID: bucket.ID,
Name: "segment",
Destination: "https://webhooks.segment.com?b=yyyy",
}); err != nil {
log.Fatal(err)
}
}Mark an output as Internal (or just point it at a private/localhost address) and
it will only be delivered by the relay
agent running inside your network. This lets an external provider reach a service
that has no public IP.
// Public providers deliver to bucket.Inputs[0].EndpointURL(); the agent picks the
// webhooks up and forwards them to your internal service.
_, err = api.CreateOutput(&webhookrelay.Output{
BucketID: bucket.ID,
Name: "local-dev",
Destination: "http://localhost:8080/webhooks",
Internal: true, // relayed by the agent, not the public forwarder
})Then run the agent to open the connection:
relay forward --bucket sendgrid-to-segment http://localhost:8080/webhooksEnable streaming on a bucket to receive its webhooks in real time over a WebSocket
(wss://my.webhookrelay.com/v1/socket) — handy for browsers, dashboards, or any
client that can't expose an endpoint.
bucket, err := api.GetBucket("sendgrid-to-segment")
if err != nil {
log.Fatal(err)
}
bucket.Stream = true
if _, err := api.UpdateBucket(bucket); err != nil {
log.Fatal(err)
}UpdateBucket replaces the whole bucket configuration (name, description, flags
and auth), so always start from GetBucket as above; a bucket without a Name is
rejected.
When acting as a WebSocket "transponder" (your client, not Webhook Relay, produces
the HTTP response), send the response back within 10 seconds using UpdateWebhookLog:
err = api.UpdateWebhookLog(&webhookrelay.WebhookLogsUpdateRequest{
ID: logID, // the webhook's log ID, received over the socket
StatusCode: 200,
Status: webhookrelay.RequestStatusSent,
ResponseBody: []byte(`{"ok":true}`),
})Prefer to pull instead of being pushed? Poll a bucket's logs and process new
webhooks yourself. BucketID is required; filter by status, time range and paginate.
res, err := api.ListWebhookLogs(&webhookrelay.WebhookLogsListOptions{
BucketID: bucket.ID,
Status: webhookrelay.RequestStatusReceived,
Limit: 50,
})
if err != nil {
log.Fatal(err)
}
for _, entry := range res.Data {
fmt.Printf("%s %s -> %d\n", entry.Method, entry.ID, entry.StatusCode)
// entry.Body, entry.Headers, entry.RawQuery ...
}If a destination is down, durable delivery persists failing webhooks and keeps retrying on a backoff schedule until they succeed or hit a deadline — so an outage doesn't lose events.
import "time"
_, err = api.CreateOutput(&webhookrelay.Output{
BucketID: bucket.ID,
Name: "billing",
Destination: "https://api.internal.example.com/webhooks",
Durability: &webhookrelay.DurabilityConfig{
Enabled: true,
Schedule: "long", // "seconds", "medium", "long" or "custom"
Deadline: 30 * 24 * time.Hour, // give up after 30 days
HandoffAfter: 15 * time.Minute, // move to durable retries after 15m of failures
},
})Cap how fast webhooks are delivered to a fragile or rate-limited destination. Queue and release at a fixed rate, or bound concurrency.
// Rate limit: at most 10 deliveries per minute.
_, err = api.CreateOutput(&webhookrelay.Output{
BucketID: bucket.ID,
Name: "rate-limited-api",
Destination: "https://api.example.com/webhooks",
Throttle: &webhookrelay.ThrottleConfig{
Enabled: true,
Mode: webhookrelay.ThrottleModeRate,
Rate: 10,
Interval: webhookrelay.ThrottleIntervalMinute,
},
})
// Or bound in-flight concurrency instead:
throttle := &webhookrelay.ThrottleConfig{
Enabled: true,
Mode: webhookrelay.ThrottleModeConcurrency,
MaxConcurrent: 5,
MaxQueueDepth: 1000, // reject with HTTP 429 once the queue is full
}
_ = throttleRecover never-attempted deliveries that arrived while an internal agent was
disconnected. The server scans at most Limit recent rows inside Lookback.
_, err = api.CreateOutput(&webhookrelay.Output{
BucketID: bucket.ID,
Name: "cluster-agent",
Destination: "http://webhook-handler.default.svc:8080/webhooks",
Internal: true,
ReplayMissing: &webhookrelay.ReplayMissingConfig{
Enabled: true,
Lookback: 30 * time.Minute,
Limit: 250,
},
})Register each customer as a consumer with the HTTPS endpoint they give you, then
publish events. Webhook Relay signs every delivery
(Standard Webhooks), retries failures durably
for up to 48 hours and keeps the delivery history. GetOutboundAccess reports
whether outbound webhooks are enabled for your account, and its limits.
// Once per customer.
_, err = api.UpsertOutboundEventType(&webhookrelay.OutboundEventTypeOptions{Name: "invoice.paid"})
_, err = api.UpsertOutboundConsumer(&webhookrelay.OutboundConsumerOptions{ID: "customer_42", Name: "Acme"})
endpoint, err := api.CreateOutboundEndpoint(&webhookrelay.OutboundEndpointOptions{
Consumer: "customer_42",
URL: "https://customer.example/webhooks",
EventTypes: []string{"invoice.paid"},
})
// endpoint.Secret is the signing secret your customer verifies deliveries with.
// Migrating from Svix or another Standard Webhooks sender? Set Secret to the
// endpoint's existing whsec_ secret so your customer keeps verifying with it.
// Every time the event happens.
message, err := api.PublishOutboundMessage(&webhookrelay.OutboundMessagePublishOptions{
Consumer: "customer_42",
EventType: "invoice.paid",
Payload: map[string]interface{}{"invoice_id": "inv_123", "amount": 4900},
IdempotencyKey: "invoice-paid:inv_123", // retrying with the same key never publishes twice
})Publishing is asynchronous: the returned message is the durable acceptance
receipt. GetOutboundMessage shows each endpoint's delivery and attempts;
RetryOutboundDelivery, RecoverOutboundDeliveries and
ReplayMissingOutboundDeliveries re-send in the background and return a
recovery task. Without an IdempotencyKey the client generates one per call,
so its own automatic retries are safe.
Watch endpoint health across all your customers. Endpoints carry Stats
(attempts and failures over the last 24 hours) when listed or read:
health, err := api.GetOutboundHealth()
// health.Endpoints["failing"], health.Stats.Attempts, health.Stats.Failures
// Failing endpoints come longest failing first. Filter by Consumer too;
// Limit is 1-100 (0 means 50), page with Offset.
failing, err := api.ListAllOutboundEndpoints(&webhookrelay.OutboundEndpointListOptions{
State: webhookrelay.OutboundEndpointStateFailing,
Limit: 20,
})A runnable version lives in examples/outbound.
The client covers the rest of the Webhook Relay API too:
- Transformation functions (JavaScript / Lua) to reshape webhooks in flight —
CreateFunction,SetFunctionConfigurationVariable,InvokeFunction. Attach one to an input or output viaFunctionID. - Recurring webhooks on a cron schedule —
CreateCron. - Tunnels to expose a local service over a public hostname —
CreateTunnel. - Service connections (GCP / AWS / Azure) plus cloud event inputs (Pub/Sub, S3, SQS, SNS) and outputs (Pub/Sub, GCS, S3, SQS, SNS, Discord, Slack).
- Email-to-webhook — an inbound email address that relays mail through a bucket
(
CreateServiceConnectionInputwith typeemail). - Notification integrations —
CreateIntegration,AddIntegrationBucket. - Routing rules, retries, TLS controls, custom domains, and webhook logs.
New / NewWithAPIKey accept functional options:
api, err := webhookrelay.NewWithAPIKey(os.Getenv("RELAY_API_KEY"),
webhookrelay.WithUserAgent("my-app/1.2.3"),
webhookrelay.WithRetryPolicy(3, 1, 30), // retries, min/max backoff (seconds)
)Available options: WithHTTPClient, WithAPIEndpointURL, WithHeaders,
WithRetryPolicy, WithUserAgent, WithLogger.
Non-2xx responses are returned as *webhookrelay.APIError with the HTTP
status, the error message from a {"error":"..."} body, the raw body and
any Retry-After delay. Transport failures are plain errors, so a network
blip is never mistaken for a 404:
_, err := api.PublishOutboundMessage(&webhookrelay.OutboundMessagePublishOptions{...})
switch {
case err == nil:
case webhookrelay.IsNotFound(err):
// the consumer has no endpoints yet
case webhookrelay.IsBadRequest(err):
var apiErr *webhookrelay.APIError
errors.As(err, &apiErr)
showToUser(apiErr.Message)
default:
return err
}IsConflict and IsRateLimited cover 409 and 429.
See LICENSE.