Skip to content

fix(webhooks)!: mark compression and event_hashes required#39

Merged
johnpmitsch merged 2 commits into
mainfrom
dx-5345-webhooks-required-fields
Jun 3, 2026
Merged

fix(webhooks)!: mark compression and event_hashes required#39
johnpmitsch merged 2 commits into
mainfrom
dx-5345-webhooks-required-fields

Conversation

@johnpmitsch

@johnpmitsch johnpmitsch commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two commits resolving SDK drift from the live webhooks REST API.

Commit 1: required-field fix

The webhooks REST API requires compression on every webhook destination and eventHashes on every evmContractEvents template, but both fields were typed Option in the SDK. Calls that omitted them failed at runtime (HTTP 400 for compression, HTTP 500 for event hashes). This tightens the types so the requirement surfaces at the type system instead of at the network boundary.

Note (breaking, Python only): WebhookDestinationAttributes.__init__ positional argument order changed from (url, security_token=None, compression=None) to (url, compression, security_token=None) so the now-required compression field comes before the optional security_token. Positional Python callers (WebhookDestinationAttributes("https://...", "tok")) will silently bind the wrong value and must update; keyword callers are unaffected.

Commit 2: ByList template variants

Every webhook template supports two input shapes on the API: inline values (e.g. wallets: [...]) or a reference to a pre-created list by name (e.g. walletsListName: "my_wallets"). The SDK previously only modeled the inline shape. This commit adds 8 *ByListTemplate structs and an Inline | ByList enum per template, with serde untagged dispatch on disjoint field names.

The two forms share a templateId and URL path; the server disambiguates by field presence.

Live-verified drift map

Verified by running raw curls against https://api.quicknode.com/webhooks/rest/v1/ and cross-checking the published OpenAPI spec.

Required-field drift (commit 1)

Field SDK before OpenAPI claims Live API Action
WebhookDestinationAttributes.compression Option<String> required required (400 when omitted) SDK fix: tighten to required
EvmContractEventsTemplate.event_hashes Option<Vec<String>> required (also missing required array) required (500 when omitted) SDK fix: tighten to required
WebhookDestinationAttributes.security_token Option<String> required optional (auto-generated) OpenAPI fix
EvmAbiFilterTemplate.contracts required optional required (500 when omitted) OpenAPI fix

ByList behavior (commit 2)

Every template accepts a parallel *ByList shape. Two have asymmetric inline-vs-ByList behavior:

Template Inline shape ByList shape
EvmContractEvents both contracts AND eventHashes required contractsListName required, eventHashesListName optional
EvmAbiFilter abi required, contracts required (per live) abiJson required, contractsListName optional (per spec AND live)

The ByList variant of EVMABI also uses a different wire key (abiJson vs abi).

Polyglot ripple

  • Rust core: 16 new structs (8 ByList templates), 8 new <Template>Input enums (Inline | ByList).
  • Python: 16 new #[pyclass] wrappers (*Args for inline, *ByListArgs for by-list) via macro-extended template_wrapper!. Added module registrations and __init__.pyi exports.
  • Node: TS TemplateArgsInput accepts either inline or ByList; TemplateArgs factory methods now accept the union.
  • Ruby: webhook methods accept JSON pass-through, so no Ruby binding change — same template_args_json continues to work with either shape.
  • READMEs: all four updated with inline/ByList table.
  • Tests: 8 new ByList round-trip tests in core (211 total, up from 203).

OpenAPI spec issues to report to the API team

  1. WebhookAttributes.security_token is marked required but the server auto-generates it when absent. The required array should not include it.
  2. EVMABIFilterTemplate.contracts is marked optional but the server rejects calls that omit it. The required array should be ["abi", "contracts"], not ["abi"].
  3. EVMContractEventsTemplate has no required array declared, but the server requires both contracts and eventHashes. The required array should be ["contracts", "eventHashes"].

Polish (not blocking the SDK fix):
4. Missing required template args return HTTP 500 (Expected array for arg "...") instead of a clean 400.
5. The original compression error rendered the value as a literal empty string (invalid compression: ""), suggesting absent-to-empty-string coercion before validation. A clean "compression is required" 400 would be friendlier to non-SDK callers.

https://linear.app/quicknode/issue/BLD-1324/webhooks-rest-api-openapi-spec-drift-and-error-response-polish

Test plan

  • cargo check
  • just lint
  • just test (211 core tests, all pass)
  • just python-build
  • just node-build
  • just ruby-build
  • cargo test -p sdk-node --lib (existing evm_contract_events_preserves_event_hashes_through_outbound_wire test still passes)
  • Run cargo run --example webhooks_e2e -p quicknode-sdk against a live API key
  • Run python/examples/webhooks_e2e.py against a live API key
  • Run npm/examples/webhooks_e2e.ts against a live API key
  • Run ruby/examples/webhooks_e2e.rb against a live API key

Closes DX-5345.

The webhooks REST API requires `compression` on every webhook destination
and `eventHashes` on every `evmContractEvents` template, but both fields
were typed `Option` in the SDK with `skip_serializing_if`. Calls that
omitted them serialized without the keys and the API rejected the request
at runtime (HTTP 400 `"invalid compression: ""` for compression, HTTP 500
`"Expected array for arg \"eventHashes\""` for event hashes). Tightening
the types surfaces the requirement at the type system instead of at the
network boundary.

Verified by running the failing curls against the live API before/after
and by checking the published OpenAPI spec, which lists both fields in
its `required` arrays.

Ripples to PyO3 `#[new]` signatures, the regenerated TS `.d.ts`, the
Python `_core/__init__.pyi`, all four per-language READMEs, and example
scripts. Ruby webhook methods take destination/template as JSON strings
so they pick the change up via serde at deserialization time, no
binding-side change needed.
Every webhook template supports two input shapes on the API: inline
values (e.g. `wallets: [...]`) or a reference to a pre-created list by
name (e.g. `walletsListName: "my_wallets"`). The SDK previously only
modeled the inline shape, so callers had no compile-safe way to use the
ByList form even though the API has supported it on every template.

Each template now has a paired `*ByListTemplate` struct and the existing
`TemplateArgs` variants hold a per-template `<Template>Input` enum that
picks between inline and ByList. The two shapes share a `templateId`
and URL path; the server disambiguates by field presence. Untagged
serde dispatch on disjoint field names handles the wire encoding.

Resolution rules verified against the live API (audit script run
against api.quicknode.com/webhooks/rest/v1):

* `EvmContractEventsByList.event_hashes_list_name` — optional; server
  accepts contracts-only and matches all events from those contracts.
  Mirrors the asymmetric behavior between inline (both required) and
  ByList (one required, one optional).
* `EvmAbiFilterByList.contracts_list_name` — optional per both
  OpenAPI and live behavior; the ABI alone is a valid filter.
* `EvmAbiFilterByList` uses `abiJson` on the wire, distinct from the
  inline variant's `abi`.

Polyglot ripple: Python wrappers gain matching `*ByListArgs` classes
(macro-generated), Node TS factory methods now accept either inline or
ByList template, READMEs document both forms per template. Ruby
webhook methods take JSON pass-through so no Ruby code change.

The core enum names are `<Template>Input` (not `<Template>Args`) to
avoid colliding with the user-facing `<Template>Args` Python wrapper
class names.
@johnpmitsch johnpmitsch merged commit df58311 into main Jun 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants