Skip to content

Webhooks

Igor Sazonov edited this page Sep 8, 2026 · 1 revision

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.

Understand the delivery model

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

Discover webhook entities

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.

Create a webhook

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.

Verify incoming requests

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.

Receiver architecture

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.

List and delete webhooks

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.

Production checklist

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.

References

Clone this wiki locally