Skip to main content

Managing endpoints

A webhook endpoint is its own object. An organization can register several, each with its own URL, signing secret, and event subscription — so a CRM sync and a billing service can receive completely different slices of the event stream.

Manage endpoints in Settings → Developers → Webhooks, or over the API.

Endpoints are org-wide​

An endpoint receives every event in the organization that matches its selectors, regardless of what triggered it — an API call, an action in the console, or a scheduled job.

Endpoints are deliberately not scoped to an API key. Most events have no key to attribute them to: envelope.created from the console, envelope.expired from cron, invoice.paid from an inbound Stripe webhook. A key-scoped endpoint would silently miss them.

To route by tenant, filter on customer_id or your own external_id inside your receiver — that is what actually distinguishes tenants, and it works for every event regardless of origin.

An organization may register up to 25 endpoints.

Choosing events​

Each endpoint stores a list of selectors. Three forms are accepted:

SelectorMatches
envelope.completedExactly that event
envelope.*Every event in the envelope domain
*Every event

Wildcards are prefix-only — *.completed and envelope.*.x are rejected.

Prefer a wildcard when you want a whole domain. New events added to a domain are delivered automatically to endpoints subscribed with <domain>.*, whereas an endpoint listing ids one by one must be edited each time the catalog grows.

curl -X POST https://api.loyva.com/api/v2/webhooks/endpoints \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/loyva",
"description": "Production CRM sync",
"events": ["envelope.completed", "vault.*"]
}'

The response returns the signing secret once:

{
"data": {
"endpoint_id": "whep_a1b2c3d4e5f6a7b8c9d0",
"url": "https://your-app.com/webhooks/loyva",
"events": ["envelope.completed", "vault.*"],
"is_active": true,
"secret_set": true,
"secret": "whsec_4f3a...",
"secret_note": "Store this signing secret securely. It will not be shown again."
}
}

Store secret immediately. It is never returned again by any endpoint — not in a list, not in a read. If you lose it, rotate.

Endpoint routes​

RoutePurpose
GET /api/v2/webhooks/endpointsList endpoints
POST /api/v2/webhooks/endpointsCreate; returns the secret once
PATCH /api/v2/webhooks/endpoints/:endpoint_idUpdate url, events, description, or active state
DELETE /api/v2/webhooks/endpoints/:endpoint_idDisable. Delivery history is retained
POST /api/v2/webhooks/endpoints/:endpoint_id/rotate-secretIssue a new secret
POST /api/v2/webhooks/endpoints/:endpoint_id/testSend a webhook.test delivery
GET /api/v2/webhooks/endpoints/:endpoint_id/deliveriesDelivery log
GET /api/v2/webhooks/deliveries/:delivery_idOne delivery, including the payload
POST /api/v2/webhooks/deliveries/:delivery_id/redeliverReplay a delivery
GET /api/v2/webhooks/eventsThe event catalog

All require an admin JWT.

DELETE is a soft delete: the endpoint stops receiving events but its delivery log survives as audit evidence.

URL requirements​

Endpoint URLs must be public HTTPS. Loyva rejects loopback and private addresses, .local / .internal / .lan hosts, and cloud metadata IPs — at create time, at update time, and again immediately before every delivery. Deliveries do not follow redirects, so a 302 toward an internal address fails rather than being followed.

Rotating a signing secret​

curl -X POST https://api.loyva.com/api/v2/webhooks/endpoints/whep_.../rotate-secret \
-H "Authorization: Bearer $JWT"

For 48 hours after a rotation, every delivery is signed with both secrets:

  • X-Loyva-Signature — the new secret
  • X-Loyva-Signature-Previous — the old secret

So you can deploy the new secret without dropping events. Accept either header during the window; after grace_expires_at only the new signature is sent.

An endpoint whose secret_set is false delivers unsigned. This applies to endpoints migrated from the older per-API-key webhook configuration, where a secret was never set. The console flags these as Not signed — rotate to generate one.

Testing an endpoint​

curl -X POST https://api.loyva.com/api/v2/webhooks/endpoints/whep_.../test \
-H "Authorization: Bearer $JWT"

Fires a signed webhook.test event and reports whether your endpoint answered 2xx. The response deliberately does not include your endpoint's response body.

Delivery log and replay​

Every attempt is recorded: status, response code and body, attempt count, duration, and the next retry time.

curl "https://api.loyva.com/api/v2/webhooks/endpoints/whep_.../deliveries?status=failed" \
-H "Authorization: Bearer $JWT"

To replay one:

curl -X POST https://api.loyva.com/api/v2/webhooks/deliveries/wdel_.../redeliver \
-H "Authorization: Bearer $JWT"

A replay is a new delivery row linked by redelivery_of, sent on the next delivery sweep. The body keeps its original event_id, so a receiver deduplicating on event_id treats it as the same event — which is what makes replay safe to use liberally.

Next steps​