env-master
Documentation sections

Webhooks

The server tells your system about changes itself.

Why

The SDK reads secrets at service startup. That is enough while values change rarely: restart — pick up the new one.

A webhook is needed when you must react immediately: re-read the config without a restart, raise an alert on a failed rotation, learn that a one-time token was presented a second time.

How to set one up

The project's Webhooks section — or a request:

curl -s $BASE/projects/$PROJECT/webhooks \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"url":"https://ops.example.com/envmaster","eventTypes":["entry.updated","rotation.failed","enrollment.reuse-detected"]}'

You need an address, a list of events and, if you want, a list of environments (environments) — empty means "all".

The signing secret is returned in the response once, at creation. In the database it is stored encrypted and is never shown again: if it is lost, the webhook has to be created anew. The required permission is the project admin; the last 50 deliveries with statuses are visible in the dashboard or via GET /projects/:id/webhooks/:id/deliveries.

What arrives

A POST with a body:

{
  "event": "entry.updated",
  "projectId": "prj_...",
  "environment": "prod",
  "occurredAt": "2026-08-27T10:15:00.000Z",
  "metadata": { "key": "DB_PASSWORD", "kind": "secret", "version": 4 }
}

There are no secret values in the body, and there never will be. You get the key name, its type, and the version number — enough to decide whether to re-read the config, and not enough for a leak into the receiver's log to become a secret leak.

Verifying the signature

The x-signature header contains sha256=<hex> — an HMAC-SHA256 of the request body under your secret.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(header ?? "");
  const b = Buffer.from(expected);
  // Длины сравниваются отдельно: timingSafeEqual бросает на разных длинах.
  return a.length === b.length && timingSafeEqual(a, b);
}

Compute the HMAC over the raw body, before parsing the JSON. JSON reassembled from an object almost certainly differs in whitespace or field order, and the signature will not match.

Compare in constant time. Plain === on strings exits early at the first mismatched byte, and the signature can be recovered from response timing.

Not verifying the signature means accepting such a request from anyone. The webhook address will sooner or later end up in logs, in browser history, or in someone's screenshot.

Events

EventWhen it arrives
entry.createda new key appeared in the environment
entry.updatedthe value changed, including a rollback to a past version
entry.deletedthe key was deleted
entry.rotatedrotation completed successfully
rotation.failedrotation failed; the value stayed the same
secret.near-expirya secret is close to expiring
enrollment.reuse-detectedan enrollment token was presented a second time
validation.failedthe environment failed schema validation

The identity.login type is registered in the list of allowed events but is currently not sent — watch identity logins in the audit log (identity.login, with a newIp flag on a new address). Subscribing to it is pointless; there will be no deliveries.

Two events are worth wiring up even if you need none of the others.

rotation.failed means the automatic replacement failed and the service is running on the old value. Without a notification this comes out on the day the old value stops working.

enrollment.reuse-detected means the one-time token was used twice. One of the two is not you. The server has already refused the second exchange by then, but it does not revoke the identity itself — that is your action: revoke the identity and issue a new one. The full analysis is on the Services and identities page.

Delivery and retries

The first attempt is immediate. On failure (a network error or a non-2xx response) the server retries with growing pauses: 1 s, 10 s, 1 min, 5 min, up to five attempts in total.

Respond with 2xx as early as possible, before any heavy work. A receiver that rebuilds a container first and only then responds will rebuild it five times.

The handler must be idempotent: a retry is the same delivery, not a new event.

Where webhooks can go

HTTPS only. Internal, loopback, and link-local addresses are rejected in production: a server that can be pointed at any address becomes a scanner of your network in someone else's hands.

The address is checked by IP after name resolution, not by text — otherwise the ban would be bypassed with a domain pointing at 127.0.0.1. The check is repeated before every delivery attempt, not only at save time: DNS that changed after setup does not open the door.

Shared secrets

If an environment reads another project's keys via a reference, a change to a shared value is delivered to its webhooks too — with a fromProjectId field in the metadata, so it is visible that the key came from outside.

If the key is overridden by a local value, the event is not delivered: for this environment nothing has changed. Details — in the Shared secrets section.

Disabling without deleting

A webhook has a switch (PATCH /webhooks/:id, field enabled). A disabled webhook keeps its address, secret, and subscriptions but receives no deliveries — handy when the receiver is under maintenance and there is no point flooding it with retries.