Managing endpoints

The nine webhook routes, the permission each needs, and how to rotate a signing secret safely.

Routes and permissions

Each route requires a specific permission on the calling role.

OperationRoutePermission
List this organisation's endpointsGET /v3/webhooks4801 · WebhooksView
Read the subscribable eventsGET /v3/webhooks/events4801 · WebhooksView
Read one endpointGET /v3/webhooks/{uuid}4801 · WebhooksView
Read an endpoint's delivery logGET /v3/webhooks/{uuid}/executions4801 · WebhooksView
Register an endpointPOST /v3/webhooks4802 · WebhooksCreate
Change, enable or disable an endpointPATCH /v3/webhooks/{uuid}4803 · WebhooksEdit
Fire a test eventPOST /v3/webhooks/{uuid}/test4803 · WebhooksEdit
Rotate the signing secretPOST /v3/webhooks/{uuid}/rotate-secret4805 · WebhooksRotateSecret
Delete an endpointDELETE /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

namestringrequired

Operator-facing name, 1–200 characters.

urlstringrequired

The HTTPS endpoint, at most 500 characters. See URL requirements.

eventTypesstring[]required

Between 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.

descriptionstring

What this endpoint is for, at most 500 characters.

branchIdinteger

Narrow 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:

signingSecretstring

A 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.

subscriptionsnumber

How many event subscriptions were created — one per unique event name.

StatusMeaning
400The URL is not an acceptable endpoint, or an event name is not available for webhook subscription.
403Your session does not name a single organisation. Webhooks belong to one organisation.
503The 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

uuidstring
The endpoint's id. Use this on every other route.
namestring
descriptionstring | null
urlstring
eventTypesstring[]
What this endpoint is subscribed to.
enabledboolean
When false, deliveries are skipped rather than failed.
branchIdnumber | null
lastTriggeredAtstring | null
The last time a delivery was attempted.
lastSuccessAtstring | null
The last 2xx. Still null means nothing has ever got through.
lastFailureAtstring | null
totalAttemptsnumber
totalSuccessesnumber
totalFailuresnumber
createdAtstring | null

The 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: 50

How 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

namestring
1–200 characters.
descriptionstring
At most 500 characters.
urlstring
Re-validated against the URL requirements.
enabledboolean
false pauses the endpoint without deleting it.

Fire a test event

POST /v3/webhooks/{uuid}/test · permission 4803

eventTypestringdefault: cash.payment.received

Which event to send a synthetic instance of.

deliveredboolean
Whether your endpoint answered 2xx.
statusCodenumber | null
The status your endpoint returned, or null if nothing answered.
durationMsnumber
How long the attempt took.
errorstring | null
Why it failed, when it failed.

It 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:

  1. 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.

  2. Rotate and capture the new secret

    Call rotate-secret. The response is the only place the new value appears — store it immediately.

  3. 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.

  4. 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.

StatusMeaning
404No such endpoint in your organisation.
503The 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.