You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
| 1 |`GET /webhook/:name` returns 404 when the webhook (or source) is missing | Not available — only the collection endpoint existed | The **collection**`GET /webhook` still returns `200 {}` when the org has no source; only the **single**`GET /webhook/:name` returns `404 { "code": "not found" }` when the source or named webhook is missing | For single reads via `:name`, treat 404 as "no such webhook"; the collection endpoint stays `200 {}` when empty |
666
+
| 2 | New path variant `GET /webhook/:name`| Not available — only collection endpoint | Returns the single matching webhook, or `404 { "code": "not found" }`| Switch single-webhook reads from `GET /webhook` + client-side lookup to `GET /webhook/:name`|
667
+
| 3 |`POST /webhook` no longer overwrites | Reusing a `name` overwrote the existing webhook (including url/event maps) and returned `200`| Duplicate `name` returns `409 { "code": "name conflict" }`| Replace the overwrite flow with `PATCH /webhook/:name` (or delete-then-create if you must keep the old call shape) |
668
+
| 4 |`POST /webhook` success status `200` → `201`|`200 OK` on create or update |`201 Created` on create only | Accept `201` (or `>=200 <300`) instead of strict `200` equality |
669
+
| 5 |`POST /webhook` response shape gains `name`|`{ url, transaction?, card?, user?, secret }`|`{ name, url, transaction?, card?, user?, secret }`| Not breaking if clients ignored unknown fields, but valibot/zod consumers should add `name` to the schema |
670
+
| 6 |`POST /webhook` accepts name via path | Required `name` field in body | Name accepted as path param (`POST /webhook/:name`) **or** body field; path wins when both present; missing both → `400 { "code": "invalid name" }`| Prefer the path form for new code; the body form still works for backwards compatibility |
671
+
| 7 |`name` must match slug regex `^[a-z0-9-]{1,64}$`| Any non-empty string accepted | Invalid name → `400 { "code": "invalid name" }` (also on `GET/PATCH/DELETE /:name`) | Normalize names client-side to lowercase, digits, hyphens, ≤64 chars |
672
+
| 8 | URL validation rejects non-https and private/loopback hosts | Used the `isValid` helper (lenient — allowed http and any reachable host) | Rejects: non-`https:`, unresolvable hosts, and addresses in `127.0.0.0/8`, `10/8`, `172.16/12`, `192.168/16`, `169.254/16`, `0.0.0.0`, `::1`, `fc00::/7`, `fe80::/10`, `2001:db8::/32` → `400 { "code": "invalid url" }` (the handler catches the validation error and returns only `code`; no `message` array is sent despite the OpenAPI schema allowing one) | Use only public https endpoints; update local-dev tunnels (ngrok et al. — must resolve to a public IP) |
673
+
| 9 | New `PATCH /webhook/:name` endpoint | Not available | Partial update; omitted fields preserved; `null` on a per-event URL clears it; the parent group is dropped when empty; `404` if the webhook is missing; secret preserved and not returned | Use `PATCH` for any field change; do not re-`POST` the same name |
674
+
| 10 |`DELETE /webhook` body → `DELETE /webhook/:name` path | Name passed as a `{ "name": "..." }` JSON body | Name is a **required path param**; body ignored | Move the name to the URL; remove the JSON body and `content-type: application/json` from delete requests |
675
+
| 11 |`DELETE` no longer silent on missing name | Unknown name returned `200 { "code": "ok" }`|`404 { "code": "not found" }`| Stop relying on an idempotent "ok" — treat 404 as already-deleted if that's acceptable |
676
+
| 12 |`DELETE` of the last webhook drops the source row | Source row preserved with an empty webhooks map | When the last webhook is deleted, the entire `sources` row is deleted | The collection `GET /webhook` then returns `200 {}` (not 404); a subsequent `GET /webhook/:name` returns `404 not found` — see break #1|
| 14 | New source `type` is `"integrator"` (was `"uphold"`) |`WebhookConfig.type === "uphold"` when the first webhook was created |`WebhookConfig.type === "integrator"` for new sources; existing rows untouched | Only relevant if a client reads/parses the raw `sources.config.type` — accept both values |
679
+
| 15 | New permission `webhook:update`| Roles `admin`/`owner` had `["create","delete","read"]`| Roles now also have `"update"`; `PATCH` requires it | Re-issue better-auth role assignments if you cache them; the member role is unchanged |
680
+
| 16 |`POST` body URL fields now strictly validated as URLs at the API boundary |`BaseWebhook.url` etc. were plain `string()` (validation happened elsewhere) | Every URL field uses `pipe(string(), url())` — malformed strings fail `400` before URL-reachability checks | Ensure `transaction.{created,updated,completed}`, `card.updated`, `user.updated`, and top-level `url` are well-formed absolute URLs |
681
+
682
+
### Examples
683
+
684
+
#### Break #1 — collection stays `{}`, single `:name` is 404
685
+
686
+
```http
687
+
# collection endpoint — empty org still returns 200 {}
688
+
GET /webhook
689
+
HTTP/1.1 200 OK
690
+
{}
691
+
692
+
# single-webhook endpoint — missing source or name returns 404
693
+
GET /webhook/main
694
+
HTTP/1.1 404 Not Found
695
+
{ "code": "not found" }
696
+
```
697
+
698
+
#### Break #3 / #4 — POST no longer overwrites, 201 instead of 200
0 commit comments