Routes and permissions
Each route requires a specific permission on the calling role.
| Operation | Route | Permission |
|---|---|---|
| List this organisation's endpoints | GET /v3/webhooks | 4801 · WebhooksView |
| Read the subscribable events | GET /v3/webhooks/events | 4801 · WebhooksView |
| Read one endpoint | GET /v3/webhooks/{uuid} | 4801 · WebhooksView |
| Read an endpoint's delivery log | GET /v3/webhooks/{uuid}/executions | 4801 · WebhooksView |
| Register an endpoint | POST /v3/webhooks | 4802 · WebhooksCreate |
| Change, enable or disable an endpoint | PATCH /v3/webhooks/{uuid} | 4803 · WebhooksEdit |
| Fire a test event | POST /v3/webhooks/{uuid}/test | 4803 · WebhooksEdit |
| Rotate the signing secret | POST /v3/webhooks/{uuid}/rotate-secret | 4805 · WebhooksRotateSecret |
| Delete an endpoint | DELETE /v3/webhooks/{uuid} | 4804 · WebhooksDelete |
Rotation is its own permission, separate from edit, because it breaks a working integration until the subscriber redeploys — and because an operator may reasonably hold one without the other.
Register an endpoint
POST /v3/webhooks · permission 4802
namestringrequiredOperator-facing name, 1–200 characters.
urlstringrequiredThe HTTPS endpoint, at most 500 characters. See URL requirements.
eventTypesstring[]requiredBetween 1 and 50 event names from the subscribable list. Duplicates are collapsed. An unknown or non-public name is refused here, rather than accepted and then never delivered.
descriptionstringWhat this endpoint is for, at most 500 characters.
branchIdintegerNarrow this endpoint to a single branch. Omit for organisation-wide.
curl -s -X POST https://api.nyumbazetu.com/v3/webhooks \
-H "Authorization: Bearer $NZ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Ops notifier",
"url": "https://hooks.example.com/nyumbazetu",
"eventTypes": ["cash.payment.received"],
"branchId": 2187
}'
The 201 response is the endpoint as read back, plus:
signingSecretstringA 64-character hex secret, returned here and from rotate-secret only. No
read route returns it, no delivery log stores it, and it is encrypted at rest.
subscriptionsnumberHow many event subscriptions were created — one per unique event name.
| Status | Meaning |
|---|---|
400 | The URL is not an acceptable endpoint, or an event name is not available for webhook subscription. |
403 | Your session does not name a single organisation. Webhooks belong to one organisation. |
503 | The credential store has no encryption key, so no signing secret can be stored safely. Nothing was created. |
A refused registration leaves nothing behind: the endpoint, its subscriptions and its secret are created as one unit, or not at all.
URL requirements
https only. http is refused, and so is any other scheme.
No credentials in the URL. A user:password@host form is refused.
Must resolve to a public address. Refused if the hostname is in a
private-by-name namespace, or if any address it resolves to is private,
loopback, link-local or otherwise reserved — including 169.254.169.254. A host
that answers with one public and one private address is refused outright.
Must resolve at all. A hostname that does not resolve, or resolves to no addresses, is refused at send time.
These checks run at registration, again when you change the URL, and again on every delivery — because a hostname that is public today can point inward tomorrow. The address that passes the check is the one the request is pinned to, so the name cannot be re-resolved to something else between the check and the connection.
Read an endpoint
GET /v3/webhooks/{uuid} · permission 4801
uuidstringnamestringdescriptionstring | nullurlstringeventTypesstring[]enabledbooleanfalse, deliveries are skipped rather than failed.branchIdnumber | nulllastTriggeredAtstring | nulllastSuccessAtstring | null2xx. Still null means nothing has ever got through.lastFailureAtstring | nulltotalAttemptsnumbertotalSuccessesnumbertotalFailuresnumbercreatedAtstring | nullThe signing secret is not in this response, and there is no field that holds
it. GET /v3/webhooks returns the same shape as an array.
Read the delivery log
GET /v3/webhooks/{uuid}/executions · permission 4801
limitintegerdefault: 50How many of the most recent attempts to return, clamped to between 1 and 200.
Rows are newest first and carry the event type, the URL and method used, the response status, the delivery status, the duration in milliseconds, any error message, and the request and response headers.
Change, enable or disable an endpoint
PATCH /v3/webhooks/{uuid} · permission 4803
namestringdescriptionstringurlstringenabledbooleanfalse pauses the endpoint without deleting it.Fire a test event
POST /v3/webhooks/{uuid}/test · permission 4803
eventTypestringdefault: cash.payment.receivedWhich event to send a synthetic instance of.
deliveredboolean2xx.statusCodenumber | nullnull if nothing answered.durationMsnumbererrorstring | nullIt goes through the real delivery path — same signing, same address checks, same delivery log — so a pass here means a real delivery will pass. It is a single attempt: not queued, not retried.
The synthetic body uses the same envelope as a real delivery but its data is
marked test: true and carries no entity reference. It proves transport and
signature verification, not your payload handling. See
the envelope.
Rotating the signing secret
POST /v3/webhooks/{uuid}/rotate-secret · permission 4805
curl -s -X POST \
https://api.nyumbazetu.com/v3/webhooks/{uuid}/rotate-secret \
-H "Authorization: Bearer $NZ_TOKEN"
{
"success": true,
"data": {
"uuid": "9f1c7e5a-3b2d-4f80-8c11-6a2d9e4b7c03",
"signingSecret": "…a new 64-character hex secret…"
},
"message": "Rotated. Deploy this secret to your endpoint — the previous one no longer verifies."
}
How to rotate without dropping deliveries
The overlap has to live on your side. Accept two secrets for the duration:
Teach your endpoint to accept two secrets
Read a primary and an optional secondary secret from your configuration. Compute the expected signature for each and constant-time compare against both; accept if either matches. Deploy this before you rotate.
Rotate and capture the new secret
Call
rotate-secret. The response is the only place the new value appears — store it immediately.Promote the new secret, keep the old one as secondary
Set primary to the new secret and secondary to the old one, and deploy. From this moment anything already in flight, signed with either, still verifies.
Drop the old secret
Once more than the 300-second replay window has passed — in practice, once you have seen successful deliveries and the delivery log is clean — remove the secondary and deploy again.
Rotate when you believe the secret has been exposed, when someone with access to it leaves, or on whatever schedule your own policy sets. A lost secret can only be replaced by rotating — there is no recovery, because the platform never stores a readable copy.
| Status | Meaning |
|---|---|
404 | No such endpoint in your organisation. |
503 | The credential store has no encryption key. The old secret is unchanged. |
Delete an endpoint
DELETE /v3/webhooks/{uuid} · permission 4804
Deliveries stop. Any delivery still queued for the endpoint fails permanently rather than being retried, because the endpoint it names no longer exists.
Deleting is not reversible by re-registering: a new registration is a new endpoint, with a new uuid and a new secret, and it receives nothing that happened before it was created.