Skip to content

Hookdeck's webhook notification config cannot be read back, and the natural way to set it returns 200 while doing nothing #43

Description

@leggetter

Not an evals finding. No eval run produced this. It came out of building a demo for
the @hookdeck/n8n-nodes-hookdeck verification video, and there was nowhere better to
record it. Filing here because the shape of it — an agent configuring Hookdeck through the
API and getting 200 for a call that did nothing — looks like a scenario worth covering.

What I was doing

Wiring a Hookdeck delivery issue through to an n8n AI Agent: a failed delivery opens an
issue, the issue fires a webhook into a Hookdeck source, that source feeds an n8n workflow
whose agent investigates and pauses the connection. Everything below was hit while setting
that up through the API only, with no dashboard access.

Three traps, in the order I hit them

1. The obvious call returns 200 and silently does nothing

The natural guess is that a webhook is a notification channel on the issue trigger,
alongside email and Slack:

PUT /2025-07-01/issue-triggers/{id}
{ "channels": { "email": {}, "webhook": { "url": "https://hkdk.events/..." } } }
-> HTTP 200
{"channels":{"email":{}}}          # the webhook key is gone

IssueTriggerChannels is additionalProperties: false over exactly
slack, microsoft_teams, discord, betteruptime, incidentio, pagerduty, opsgenie, email.
There is no webhook. But the unknown key is dropped without an error — the response is
200 and the only way to notice is to read the echoed body carefully and spot the absence.

An agent that checks the status code moves on believing it is configured. I did.

The real endpoint is project-level and unrelated to channels:

PUT /2025-07-01/notifications/webhooks
{ "enabled": true, "topics": ["issue.opened"], "source_id": "src_..." }

Verified: this works, and it fires independently of channels. With channels: {}
no email, no Slack, nothing — the webhook still delivered and the downstream agent ran.

2. That endpoint is write-only

PUT is the only method on /notifications/webhooks. There is no GET, and nothing
else exposes the state:

GET /2025-07-01/notifications/webhooks  -> 404 Cannot GET
GET /2025-07-01/projects                -> 404
GET /2025-07-01/projects/current        -> 404
GET /2025-07-01/teams                   -> 401 (org-scoped, not a project key)

So you cannot ask whether webhook notifications are on, which topics are set, or which
source they go to. The dashboard is the only reader. For automation this means the config is
unverifiable — a setup script can only assert it blindly and hope, which is what mine now
does.

It also means an agent cannot check its own work. It has no way to distinguish "I configured
this correctly" from "my call was silently discarded" — which is exactly trap 1.

3. channels: null is rejected, channels: {} is accepted

PUT {"channels": {}}    -> 200   channels now {}
PUT {"channels": null}  -> 422

The schema declares IssueTriggerChannels as nullable: true, so null reading as invalid
is surprising. Minor next to the other two, but it is one more place where the plausible
call fails and the plausible-looking failure teaches nothing.

Related, same session, different subsystem

Dismissing a delivery issue does not reset issue aggregation. Issues aggregate on
(webhook_id, error_code, response_status). Once one exists for a connection, later
failures join it and no further issue.opened fires. Setting the issue to IGNORED
does not change this — verified directly: after dismissing, three more failures produced no
new issue and no notification.

The only reliable way to get a fresh issue.opened is a new connection id. For a repeatable
setup that means deleting the connection and re-provisioning it, which is not something you
would guess from the API surface.

This one matters for agents specifically: an agent testing its own remediation loop will run
it once successfully, then silently get nothing on every subsequent attempt, with no error to
diagnose.

Why this might be worth a scenario

The common thread is success responses that mean nothing happened, in a subsystem with no
read path to check against. That is a failure mode agents are unusually bad at: there is no
error to react to, and no way to verify.

A scenario could ask an agent to "route Hookdeck delivery issues to a webhook so a workflow
can react", then score whether it:

  • reaches for channels.webhook and accepts the 200 as success — the likely failure
  • finds PUT /notifications/webhooks
  • notices it cannot verify the result, and says so rather than asserting success
  • handles the second run at all, given the aggregation behaviour above

The last point is the interesting one for a multi-turn eval: the task appears to succeed on
attempt one and then becomes silently unreproducible.

Suggested outcomes

Nothing here needs fixing in this repo. For the API team, in priority order:

  1. Reject unknown keys in channels instead of dropping them, or document that they are ignored.
  2. Add GET /notifications/webhooks.
  3. Accept channels: null as the schema says, or drop nullable: true.
  4. Document that dismissing an issue does not reset aggregation, and say what does.

Environment: Hookdeck API 2025-07-01, Gateway project, CLI 2.5.0. Every claim above was
executed against a live project rather than read from the spec.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationscenarioScenario coverage, design or correctness

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions