-
Notifications
You must be signed in to change notification settings - Fork 0
Webhooks
Goldsky Subgraph webhooks send an HTTP payload when a chosen entity changes. The SDK provides two complementary capabilities: it manages webhook configuration through client.Webhooks, and it verifies incoming deliveries with a constant-time comparison of the goldsky-webhook-secret header.
A webhook is an event-delivery mechanism, not a transactional replication protocol. Make the receiver idempotent, return quickly, and account for reorg-related state changes and non-deterministic delivery ordering.
When a subgraph handler persists an entity, Goldsky’s webhook system can send INSERT, UPDATE, or DELETE events for the entity. Subgraphs retain entity versions with a block_range, which allows a consumer to distinguish a changed entity from a historical version closing during a blockchain reorganization.1
| Payload concept | Practical receiver behavior |
|---|---|
op is INSERT, UPDATE, or DELETE
|
Model the operation explicitly; do not assume only inserts occur. |
Entity id
|
Use as a stable business key for idempotency and latest-state upserts. |
vid |
Treat a higher version as a newer entity row when tracking latest state. |
block_range |
Preserve it if reorg-aware historical state matters. |
delivery_info.current_retry |
Record for observability; duplicate deliveries must be safe. |
| Same atomic mapping operation | Do not assume the corresponding insert and update webhooks arrive in a guaranteed order. |
For a backend that needs guaranteed database synchronization rather than event-triggered work, review Goldsky Mirror as a product alternative.1
Ask the subgraph version or tag for its webhook-able entities before creating a webhook.
entities, err := client.Subgraphs.WebhookEntities(ctx, "dex-analytics", "prod")
if err != nil {
return err
}
for _, entity := range entities.Data.Entities {
fmt.Printf("%s: %d columns\n", entity.Name, len(entity.Columns))
}The response exposes entity names, rows information, and columns. Use this discovery step to avoid binding a webhook to a misspelled or unexpected entity.
A webhook configuration names the target subgraph, version or tag, entity, destination URL, and optional delivery tuning. If Secret is omitted, Goldsky generates it and returns it exactly once in the create response.
retries := 5
intervalSeconds := 15
timeoutSeconds := 10
created, err := client.Webhooks.Create(ctx, goldsky.CreateWebhookRequest{
Name: "trade-events-to-ledger",
SubgraphName: "dex-analytics",
SubgraphVersion: "prod",
Entity: "trade",
WebhookURL: "https://example.com/webhooks/goldsky/trades",
NumRetries: &retries,
RetryIntervalSeconds: &intervalSeconds,
RetryTimeoutSeconds: &timeoutSeconds,
})
if err != nil {
return err
}
// Store this in a secret manager immediately. It will not appear in list responses.
if err := secretStore.Put("goldsky/trade-events", created.Data.WebhookSecret); err != nil {
return err
}The SDK rejects invalid local configuration before sending the request: NumRetries must be from 0 to 10, while retry interval and timeout must be at least one second. The service remains authoritative for the complete validation contract.
One-time secret: do not log
created.Data.WebhookSecret, place it in a ticket, or return it from an administrative API. Store it in a secret manager before the calling process loses it.
If you want a pre-managed secret, set Secret: &secret. The same value will be sent literally in each delivery header.
Goldsky does not document an HMAC signature format for these deliveries. Instead, it sends the shared secret verbatim in the goldsky-webhook-secret header. Use the SDK helper before reading or processing the body.
func handleGoldskyWebhook(storedSecret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !goldsky.VerifyWebhookRequest(r, storedSecret) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
// Set a small maximum before decoding an untrusted HTTP body.
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
defer r.Body.Close()
var event struct {
ID string `json:"id"`
Op string `json:"op"`
Entity string `json:"entity"`
WebhookID string `json:"webhook_id"`
}
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
if err := enqueueIdempotently(r.Context(), event); err != nil {
http.Error(w, "temporary failure", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
}
}VerifyWebhookRequest calls VerifyWebhookSecret, which uses crypto/subtle.ConstantTimeCompare. An empty expected secret is always rejected. Do not replace this with a normal equality comparison.
A robust receiver acknowledges only after it has durably accepted the work. It does not need to complete expensive processing synchronously.
Goldsky delivery → secret verification → bounded JSON decode → idempotency check
→ durable queue or transaction → 2xx response → asynchronous processor
Use the event’s delivery or business identifier as an idempotency key. If the receiver cannot accept work, return a non-2xx status so Goldsky can apply the configured retry policy. Avoid sending an HTTP 2xx before the event has reached durable storage.
webhooks, err := client.Webhooks.List(ctx)
if err != nil {
return err
}
for _, hook := range webhooks.Data {
fmt.Printf("%s → %s (%s)\n", hook.Name, hook.WebhookURL, hook.Entity)
}
if err := client.Webhooks.Delete(ctx, "trade-events-to-ledger"); err != nil {
return err
}List responses do not contain the delivery secret. Deleting a webhook stops future deliveries; remove it only after the consumer migration is complete.
| Control | Reason |
|---|---|
| Use HTTPS and an allowlisted, dedicated receiver route. | Reduces accidental exposure and routing ambiguity. |
| Store the create-time secret in a secret manager. | Goldsky returns it once; list calls cannot recover it. |
| Verify before processing the body. | Rejects unauthenticated traffic early. |
| Enforce a body-size limit and JSON schema checks. | Protects the receiver from unbounded or malformed input. |
| Persist an idempotency key before returning 2xx. | Makes retries and duplicates safe. |
| Model reorgs and unordered related events. | Reflects the entity-version delivery semantics. |
| Monitor failed delivery attempts. | Prevents silent loss of event-driven work. |