Skip to content

Commit 03849df

Browse files
docs(webhooks): align compression section with the shipped API
The previous draft referenced helpers that were renamed during the refactor to the verify_and_parse_* contract (CHA-3071): - client.verify_and_decode_webhook(...) -> verify_and_parse_webhook - decompress_webhook_body(...) -> removed (no public form) - content_encoding / payload_encoding -> removed (magic-byte detect) Following the old snippets would hit AttributeError immediately. The section is rewritten to document the real surface area: - client.verify_and_parse_webhook(body, signature) - client.verify_and_parse_sqs(message_body, signature) - client.verify_and_parse_sns(message, signature) - module-level webhook.verify_and_parse_* helpers for stateless use - WebhookSignatureError as the single error class It also clarifies the return type (parsed dict, not raw bytes) and notes that the legacy verify_webhook bool helper stays unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4a3c771 commit 03849df

1 file changed

Lines changed: 30 additions & 24 deletions

File tree

docs/webhooks/webhooks_overview/webhooks_overview.md

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -92,76 +92,82 @@ valid = client.verify_webhook(request.data, request.headers['X-SIGNATURE'])
9292

9393
### Compressed webhook bodies
9494

95-
GZIP compression can be enabled for hooks payloads from the Dashboard. Enabling compression reduces the payload size significantly (often 70–90% smaller) reducing your bandwidth usage on Stream. The computation overhead introduced by the decompression step is usually negligible and offset by the much smaller payload.
95+
GZIP compression can be enabled for hook payloads from the Dashboard. Enabling compression reduces the payload size significantly (often 70–90% smaller) reducing your bandwidth usage on Stream. The decompression cost on your side is usually negligible and offset by the much smaller payload.
9696

97-
When payload compression is enabled, webhook HTTP requests will include the `Content-Encoding: gzip` header and the request body will be compressed with GZIP. Some HTTP servers and middleware (Rails, Django, Laravel, Spring Boot, ASP.NET) handle this transparently and strip the header before your handler runs — in that case the body you see is already raw JSON.
97+
When payload compression is enabled, webhook HTTP requests include the `Content-Encoding: gzip` header and the body is gzipped. SQS and SNS messages are gzipped and then base64-wrapped (both transports are UTF-8 only). Some HTTP servers and middleware (Rails, Django, Laravel, Spring Boot, ASP.NET) auto-decompress the body before your handler runs — in that case the body you see is already raw JSON.
9898

9999
Before enabling compression, make sure that:
100100

101101
* Your backend integration is using a recent version of our official SDKs with compression support
102102
* If you don't use an official SDK, make sure that your code supports receiving compressed payloads
103103
* The payload signature check is done on the **uncompressed** payload
104104

105-
The Python SDK ships with `client.verify_and_decode_webhook(...)` which transparently handles plain, gzip-compressed, and base64-wrapped (SQS / SNS firehose) payloads. It returns the raw JSON body as `bytes`, ready to pass to `json.loads`.
105+
The Python SDK exposes a one-liner per transport. Each helper detects the encoding from the body bytes (the gzip magic `1f 8b`, per [RFC 1952](https://datatracker.ietf.org/doc/html/rfc1952)), verifies the HMAC `X-Signature` over the uncompressed JSON, and returns the parsed event as a `dict`. Typed event classes are planned for a future release; until then handlers can key off the `type` field.
106106

107107
```python
108-
import json
109108
from stream_chat import StreamChat
110109

111110
client = StreamChat(api_key="STREAM_KEY", api_secret="STREAM_SECRET")
112111

113112
# Django view
114113
def stream_webhook(request):
115-
body = client.verify_and_decode_webhook(
114+
event = client.verify_and_parse_webhook(
116115
request.body,
117116
request.headers["X-Signature"],
118-
request.headers.get("Content-Encoding"),
119117
)
120-
event = json.loads(body)
121-
# ... handle event ...
118+
# ... handle event["type"], event["message"], ...
122119
```
123120

124121
```python
125-
import json
126122
from flask import request
127123
from stream_chat import StreamChat
128124

129125
client = StreamChat(api_key="STREAM_KEY", api_secret="STREAM_SECRET")
130126

131127
@app.route("/webhooks/stream", methods=["POST"])
132128
def stream_webhook():
133-
body = client.verify_and_decode_webhook(
129+
event = client.verify_and_parse_webhook(
134130
request.get_data(),
135131
request.headers["X-Signature"],
136-
request.headers.get("Content-Encoding"),
137132
)
138-
event = json.loads(body)
139-
# ... handle event ...
133+
# ... handle event["type"], event["message"], ...
140134
```
141135

142-
If your HTTP framework or a middleware already decompressed the body before it reached your handler, the `Content-Encoding` header will be missing (or set to `identity`) and `verify_and_decode_webhook` will be a no-op for the decompression step — the same call works in both cases.
136+
The same call works whether or not Stream is compressing for this app, and whether or not your framework auto-decompressed the request — the helper inspects the body bytes rather than the `Content-Encoding` header.
143137

144-
`verify_and_decode_webhook` raises `stream_chat.base.exceptions.WebhookSignatureError` when the signature does not match or the body cannot be decoded.
138+
All helpers raise `stream_chat.base.exceptions.WebhookSignatureError` when the signature does not match, when the gzip stream is corrupt, or when the SQS/SNS base64 envelope cannot be decoded.
145139

146-
The original `client.verify_webhook(request.body, request.headers["X-Signature"])` is unchanged and still available for handlers that prefer to verify and parse the body separately.
140+
The original `client.verify_webhook(request.body, request.headers["X-Signature"])` — which returns a `bool` and does not decompress — stays unchanged for backward compatibility. Switch to `verify_and_parse_webhook` to support compressed payloads.
147141

148142
#### SQS / SNS firehose
149143

150-
When delivering events through SQS or SNS, Stream base64-wraps the (possibly gzip-compressed) body so the payload stays valid UTF-8 over the queue. Pass `payload_encoding="base64"` so `verify_and_decode_webhook` unwraps the envelope before verifying the HMAC signature, which is always computed over the uncompressed JSON.
144+
For events delivered through SQS or SNS, call the matching helper on the message body. It base64-decodes the envelope, gzip-decompresses when the magic bytes are present, verifies the HMAC, and returns the parsed event.
151145

152146
```python
153-
body = client.verify_and_decode_webhook(
147+
event = client.verify_and_parse_sqs(
154148
sqs_message["Body"],
155149
sqs_message["MessageAttributes"]["X-Signature"]["StringValue"],
156-
content_encoding=sqs_message["MessageAttributes"]
157-
.get("Content-Encoding", {})
158-
.get("StringValue"),
159-
payload_encoding="base64",
160150
)
161-
event = json.loads(body)
151+
152+
event = client.verify_and_parse_sns(
153+
sns_notification["Message"],
154+
sns_notification["MessageAttributes"]["X-Signature"]["Value"],
155+
)
156+
```
157+
158+
#### Stateless / module-level form
159+
160+
If you do not want to construct a `StreamChat` client (for example in a lightweight Lambda that only handles webhooks), call the module-level helpers directly. They take the API secret as a third argument and are otherwise identical:
161+
162+
```python
163+
from stream_chat import webhook
164+
165+
event = webhook.verify_and_parse_webhook(body, signature, secret)
166+
event = webhook.verify_and_parse_sqs(message_body, signature, secret)
167+
event = webhook.verify_and_parse_sns(message, signature, secret)
162168
```
163169

164-
If you only need to decode the body without checking the signature (for example because you have already verified it elsewhere), use `client.decompress_webhook_body(body, content_encoding, payload_encoding)`.
170+
The module also exposes the primitives the composites are built from — `ungzip_payload`, `decode_sqs_payload`, `decode_sns_payload`, `verify_signature` (constant-time HMAC-SHA256), and `parse_event` — for callers that need to run the steps individually.
165171

166172
All webhook requests contain these headers:
167173

0 commit comments

Comments
 (0)