Lemlist's webhook uniqueness key is the URL, not (URL, type)
An n8n workflow was getting the full Lemlist firehose — every event type, ~63% of them falling through a Switch into an “Unrecognized” Slack alert. Easy fix on paper: stop subscribing to the events nothing handles.
Except Lemlist’s hook API only lets you set type to a single string, or omit it (= all events). No arrays, no comma lists, no except. The docs are explicit about this. So “subscribe to 14 specific events” means “create 14 hooks.”
Fine. I deleted the firehose hook first — the safer order would have been to create the new ones first, but the old hook held the target URL and I figured 14 POSTs would be fast.
First POST: 200, hook created.
Second POST: 409 Conflict.
Third through fourteenth: 409, 409, 409.
The uniqueness constraint isn’t on (targetUrl, type). It’s on targetUrl alone. One hook per URL, full stop. Which makes sense if you imagine Lemlist as “a webhook is a destination” instead of “a webhook is a subscription,” but it’s not what I’d assumed from reading the create endpoint, where type looks like a first-class field.
I sat with this for about ten seconds before the workaround clicked: n8n’s webhook nodes match on the path, not the query string. So from Lemlist’s perspective these are 14 different URLs:
https://.../webhook/<id>/webhook
https://.../webhook/<id>/webhook?evt=linkedinInviteAccepted
https://.../webhook/<id>/webhook?evt=linkedinOpened
...
But from n8n’s perspective they’re all the same workflow trigger. The query param is decorative — it exists only to bypass Lemlist’s uniqueness check.
for type in "${TYPES[@]}"; do
curl -s -u ":$KEY" -X POST https://api.lemlist.com/api/hooks \
-H "Content-Type: application/json" \
-d "{\"targetUrl\":\"$BASE?evt=$type\",\"type\":\"$type\"}"
done
Worked. 14 hooks created, all pointing at the same n8n workflow, each scoped to a single event type. The first one I’d already made at the bare URL stayed as-is.
The thing I’m noting for next time: when an API enforces “one resource per X,” check what X actually is before you assume. The endpoint that creates the resource isn’t always the best place to learn the uniqueness key — sometimes you only find out from the 409 body, sometimes you don’t find out at all and just have to try. The type field being prominent in the request schema made me read it as part of the identity. It isn’t.
Cost of the assumption: about thirteen seconds where the firehose was already deleted and the new hooks weren’t creating. Events emitted in that window were lost. Not catastrophic — Lemlist webhooks aren’t a system of record — but a real reminder that “delete-then-create” should be “create-then-delete” whenever the destination can hold both.
The query-param trick is the kind of workaround I’d want a note about if I came back to this in six months and wondered why every hook had a different ?evt= suffix. So: this post is that note.