Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion hindsight-api-slim/hindsight_api/engine/memory_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -2898,7 +2898,12 @@ async def _handle_webhook_delivery(self, task_dict: dict[str, Any]) -> None:
"timeout": http_config.timeout_seconds,
}
if http_config.method.upper() == "GET":
response = await self._http_client.get(url, **request_kwargs)
# GET webhooks use the same signed-payload contract as POST webhooks,
# so the exact bytes covered by X-Hindsight-Signature must be sent. GET
# caches do not generally key on body content, so prevent a cached
# response from suppressing delivery of a later event to the same URL.
headers["Cache-Control"] = "no-cache, no-store"
response = await self._http_client.request("GET", url, content=payload_bytes, **request_kwargs)
else:
response = await self._http_client.post(url, content=payload_bytes, **request_kwargs)
response.raise_for_status()
Expand Down
39 changes: 39 additions & 0 deletions hindsight-api-slim/tests/test_webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
- HTTP API integration tests for CRUD and delivery listing endpoints
"""

import hashlib
import hmac
import json
import uuid
from datetime import datetime, timezone
Expand Down Expand Up @@ -407,6 +409,43 @@ async def test_deliver_success(self, memory: MemoryEngine):
# Should not raise
await memory._handle_webhook_delivery(task_dict)

@pytest.mark.asyncio
async def test_get_delivers_the_signed_payload(self, memory: MemoryEngine):
"""A GET delivery sends the exact payload bytes covered by its signature."""
task_dict = _make_delivery_task(retry_count=0)
task_dict["secret"] = "test-secret"
task_dict["http_config"] = {"method": "GET", "params": {"source": "hindsight"}}
received_request: httpx.Request | None = None

async def receive(request: httpx.Request) -> httpx.Response:
nonlocal received_request
received_request = request
return httpx.Response(204)

transport = httpx.MockTransport(receive)
async with httpx.AsyncClient(transport=transport) as client:
with patch.object(memory, "_http_client", client):
await memory._handle_webhook_delivery(task_dict)

assert received_request is not None
expected_signature = "sha256=" + hmac.new(b"test-secret", received_request.content, hashlib.sha256).hexdigest()

assert received_request.method == "GET"
assert received_request.url == "https://example.com/hook?source=hindsight"
assert received_request.content == task_dict["payload"].encode()
assert received_request.headers["X-Hindsight-Signature"] == expected_signature
assert received_request.headers["Cache-Control"] == "no-cache, no-store"

@pytest.mark.asyncio
async def test_get_delivery_failure_raises_retry_task_at(self, memory: MemoryEngine):
"""A failed HTTP GET schedules a retry just like a failed POST."""
task_dict = _make_delivery_task(retry_count=0)
task_dict["http_config"] = {"method": "GET"}

with patch.object(memory._http_client, "request", new=AsyncMock(side_effect=Exception("connection refused"))):
with pytest.raises(RetryTaskAt):
await memory._handle_webhook_delivery(task_dict)

@pytest.mark.asyncio
async def test_deliver_failure_raises_retry_task_at(self, memory: MemoryEngine):
"""A failed HTTP POST raises RetryTaskAt when retries remain."""
Expand Down
2 changes: 1 addition & 1 deletion hindsight-docs/docs/developer/api/operations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Bank-deduped at submit time, so concurrent triggers against the same bank coales

### `webhook_delivery`

After certain operations complete (e.g., consolidation finishing on a bank with a registered webhook), Hindsight enqueues a `webhook_delivery` task. The handler POSTs the payload to the configured URL and retries on transient failures.
After certain operations complete (e.g., consolidation finishing on a bank with a registered webhook), Hindsight enqueues a `webhook_delivery` task. The handler sends the payload to the configured URL (POST by default, or GET when configured) and retries on transient failures.

## Endpoints

Expand Down
15 changes: 13 additions & 2 deletions hindsight-docs/docs/developer/api/webhooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@ sidebar_position: 10

# Webhooks

Hindsight can notify your application in real-time when memory events occur by sending HTTP POST requests to a URL you configure.
Hindsight can notify your application in real-time when memory events occur by sending HTTP requests to a URL you configure. Deliveries use POST by default; per-bank webhooks can select GET through `http_config.method`.

## HTTP Delivery

The event payload is sent as the JSON request body for both POST and GET deliveries. Because GET request bodies are not supported consistently by every HTTP server, proxy, or framework, use POST unless the receiving stack is known to preserve GET bodies. GET deliveries include `Cache-Control: no-cache, no-store` so caches do not suppress later events sent to the same URL.

Every delivery includes `X-Hindsight-Event` with the event type. When the webhook has a signing secret, it also includes `X-Hindsight-Signature` in the following form:

```text
sha256=<HMAC-SHA256(secret, raw_request_body)>
```

Verify the signature against the exact request body bytes before parsing JSON. Re-serializing parsed JSON can change whitespace or escaping and produce a different signature.

## Delivery and Retries

Expand Down Expand Up @@ -136,4 +148,3 @@ Fired when a bank's [Memory Defense](../memory-defense/index.md) policy acts on

**Notes:**
- A `redact` event means the secret was scrubbed and the redacted memory was still stored. A `block` event means the item was dropped; if every item in the retain request is blocked, the retain call returns `422`.

Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Bank-deduped at submit time, so concurrent triggers against the same bank coales

### `webhook_delivery`

After certain operations complete (e.g., consolidation finishing on a bank with a registered webhook), Hindsight enqueues a `webhook_delivery` task. The handler POSTs the payload to the configured URL and retries on transient failures.
After certain operations complete (e.g., consolidation finishing on a bank with a registered webhook), Hindsight enqueues a `webhook_delivery` task. The handler sends the payload to the configured URL (POST by default, or GET when configured) and retries on transient failures.

## Endpoints

Expand Down
14 changes: 13 additions & 1 deletion skills/hindsight-docs/references/developer/api/webhooks.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@

# Webhooks

Hindsight can notify your application in real-time when memory events occur by sending HTTP POST requests to a URL you configure.
Hindsight can notify your application in real-time when memory events occur by sending HTTP requests to a URL you configure. Deliveries use POST by default; per-bank webhooks can select GET through `http_config.method`.

## HTTP Delivery

The event payload is sent as the JSON request body for both POST and GET deliveries. Because GET request bodies are not supported consistently by every HTTP server, proxy, or framework, use POST unless the receiving stack is known to preserve GET bodies. GET deliveries include `Cache-Control: no-cache, no-store` so caches do not suppress later events sent to the same URL.

Every delivery includes `X-Hindsight-Event` with the event type. When the webhook has a signing secret, it also includes `X-Hindsight-Signature` in the following form:

```text
sha256=<HMAC-SHA256(secret, raw_request_body)>
```

Verify the signature against the exact request body bytes before parsing JSON. Re-serializing parsed JSON can change whitespace or escaping and produce a different signature.

## Delivery and Retries

Expand Down