Webhooks

Create a webhook endpoint

Registers an HTTPS endpoint and subscribes it to a set of events.

POST/webhooks

Registers an HTTPS endpoint and subscribes it to a set of events.

data.secret is returned exactly once, on this response. Reads only ever expose secretLastFour. If you lose it, the only way to get a working secret again is POST /webhooks/{id}/rotate, which invalidates every signature already in flight. Store it before you do anything else.

The secret is formatted ev_whsec_<base64url>. Deliveries are signed per the Standard Webhooks spec and carry webhook-id, webhook-timestamp and webhook-signature headers, plus Envless-Event (the event type) and Envless-Delivery (the delivery id). The HMAC key is the full secret string including the ev_whsec_ prefix - if your verification library expects a base64 secret, pass base64(utf8(secret)). Do not strip the prefix.

URL rules, all enforced: https only, at most 2048 characters, no embedded credentials, and the host must be publicly routable. Loopback, private IPv4 ranges, carrier-grade NAT, link-local and unique-local IPv6, localhost, cloud metadata hosts, and any host ending .internal, .local, .localdomain or .home.arpa are all rejected. Redirects are not followed - a 3xx from your receiver is recorded as a failed attempt, so register the final URL.

eventTypes must be a non-empty array drawn from GET /webhooks/event-types. Unknown names are rejected with 400 and the message names each offender.

Endpoints are created enabled: true. Delivery is at-least-once with up to 5 attempts, backing off 30s, 2m, 10m, 30m then 1h, with a 10 second request timeout; any 2xx counts as success. Make your handler idempotent on webhook-id.

A workspace is capped at 20 endpoints and at 500 simultaneously queued deliveries.

Refused for project-restricted keys.

Requires the webhooks:write scope.

curl --request POST \
  --url 'https://api.envless.cloud/webhooks' \
  --header 'Authorization: Bearer ev_sk_xxxxxxxxxxxx' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Billing",
  "url": "https://example.com/hooks/envless",
  "description": "<string>",
  "eventTypes": [
    "variable.created",
    "variable.updated"
  ]
}'
Try itPOSThttps://api.envless.cloud/webhooks

Authorization

  • Authorizationstringrequiredheader

    Bearer token: an Envless API key (ev_sk_…), created in the dashboard or with POST /keys. The key is bound to one workspace; every request runs inside it.

Request body

application/json · required

  • namestringrequiredmin length 1max length 64

    Label for the endpoint. Required, trimmed, at most 64 characters.

  • urlstringrequiredurimax length 2048

    Required. Must be https, at most 2048 characters, free of embedded credentials, and resolve to a public host. Register the final URL - redirects are recorded as failures.

  • descriptionstring | nullmax length 200

    Optional note, at most 200 characters. Empty string is stored as null.

  • eventTypesstring[]requiredmin items 1

    Non-empty array of event names from GET /webhooks/event-types. Any unrecognised name rejects the whole request.

Responses

201Created

Create a webhook endpoint succeeded.

  • successbooleanrequired
  • codeintegerrequired

    The HTTP status of this response, repeated in the body.

  • messagestringrequired
  • errornullrequired
  • dataobjectrequired
    Show child attributesHide child attributes
    • idstringrequired
    • namestring | null
    • urlstringrequireduri
    • descriptionstring | null
    • eventTypesstring[]required
    • enabledbooleanrequired
    • secretLastFourstring | null

      Last four characters of the signing secret. The full secret is returned only on create and rotate.

    • lastSuccessAtstring | nulldate-time
    • lastFailureAtstring | nulldate-time
    • createdAtstringrequireddate-time
    • updatedAtstring | nulldate-time
    • secretstringrequired

      The plaintext signing secret, e.g. ev_whsec_.... Returned on this response only. Reads expose just secretLastFour; the only way to get a fresh one is a rotation, which breaks in-flight deliveries.

  • _reqobject
    Show child attributesHide child attributes
    • reqIdstring
    • resTimeinteger | null
{
    "success": true,
    "code": 201,
    "message": "OK",
    "error": null,
    "data": {
        "id": "<string>",
        "name": "<string>",
        "url": "https://example.com",
        "description": "<string>",
        "eventTypes": [
            "<string>"
        ],
        "enabled": true,
        "secretLastFour": "<string>",
        "lastSuccessAt": "2026-05-30T10:14:22.000Z",
        "lastFailureAt": "2026-05-30T10:14:22.000Z",
        "createdAt": "2026-05-30T10:14:22.000Z",
        "updatedAt": "2026-05-30T10:14:22.000Z",
        "secret": "<string>"
    },
    "_req": {
        "reqId": "<string>",
        "resTime": 1
    }
}

Error envelope

Every failure below returns the same shape with success: false; branch on error.code, never on message.

  • successbooleanrequired
  • codeintegerrequired

    The HTTP status of this response, repeated in the body.

  • messagestringrequired
  • errorobjectrequired

    Machine-readable failure detail. Null on success.

    Show child attributesHide child attributes
    • codeenum<string>required

      Stable error code. Branch on this, never on message.

      Options: "auth.api_key_required", "auth.api_key_not_found", "auth.api_key_user_missing", "auth.api_key_owner_not_member", "auth.scope_missing", "auth.scope_ceiling_exceeded", "auth.key_project_restricted", "auth.product_restricted", "auth.permission_denied", "auth.self_mutation_forbidden", "auth.step_up_required", "auth.step_up_fresh_required", "auth.cli_token_not_accepted", "auth.two_factor_required", "auth.workspace_two_factor_required", "auth.two_factor_invalid", "auth.two_factor_replayed", "auth.two_factor_locked", "auth.two_factor_not_enabled", "auth.two_factor_already_enabled", "request.body_must_be_json", "request.body_invalid", "request.body_too_large", "request.rate_limited", "request.invalid_pagination", "workspace.not_found", "workspace.slug_conflict", "workspace.limit_reached", "workspace.delete_last", "workspace.delete_too_early", "workspace.delete_has_projects", "domain.not_configured", "domain.not_found", "domain.invalid", "domain.taken", "domain.attempt_limit", "product.not_found", "product.slug_conflict", "product.locked", "project.not_found", "project.ambiguous_product", "project.slug_conflict", "environment.not_found", "environment.slug_conflict", "environment.expose_disabled", "environment.expose_private_product", "variable.not_found", "variable.name_conflict", "variable.ciphertext_invalid", "variable.baseline_drifted", "version.not_found", "member.not_found", "member.already_exists", "member.self_invite", "member.self_mutation", "member.owner_immutable", "member.owner_cannot_leave", "member.seat_limit", "invite.already_pending", "role.owner_not_assignable", "role.limit_reached", "role.immutable", "role.in_use", "invite.not_found", "invite.expired", "role.not_found", "role.name_conflict", "key.not_found", "key.limit_reached", "webhook.not_found", "webhook.delivery_not_found", "plan.limit_reached", "plan.upgrade_required", "request.not_found", "request.method_not_allowed", "request.conflict", "auth.unauthorized", "auth.forbidden", "request.invalid", "server.error"

    • resourcestring

      The resource or scope the failure relates to.

    • fieldstring

      The request field that caused the failure.

  • dataobject | nullrequired

    Null on most failures. A plan limit carries { limit, current }.

  • _reqobject
    Show child attributesHide child attributes
    • reqIdstring
    • resTimeinteger | null
{
    "success": false,
    "code": 400,
    "message": "Something went wrong.",
    "error": {
        "code": "auth.api_key_required",
        "resource": "<string>",
        "field": "<string>"
    },
    "data": {},
    "_req": {
        "reqId": "<string>",
        "resTime": 1
    }
}

400Bad Request

  • request.body_must_be_json - the body is absent or is not parseable JSON.
  • request.body_invalid - name or url is missing or not a string; eventTypes is absent, empty, or contains a name outside the catalogue (error.field is eventTypes); the URL is not https, is over 2048 characters, carries credentials, or points at a private, loopback, link-local, metadata or .internal/.local/.localdomain/.home.arpa host; or description exceeds 200 characters.

401Unauthorized

  • auth.api_key_user_missing - the owner of the calling key could not be resolved, so the endpoint has no creator to attribute.
  • auth.api_key_required - the Authorization header is missing, or its value does not start with ev_sk_.
  • auth.api_key_not_found - the key is unknown, inactive or revoked.
  • auth.api_key_owner_not_member - the key's owner is no longer a member of the key's workspace.

403Forbidden

  • auth.key_project_restricted - the calling key is fenced to specific projects; webhooks are workspace-wide and have no project dimension.
  • auth.workspace_two_factor_required - the workspace requires two-factor authentication and the key's owner has not turned it on.
  • auth.scope_missing - the key lacks a scope this operation requires; error.resource names it.

409Conflict

request.conflict - the workspace already has 20 webhook endpoints.

413Payload Too Large

request.body_too_large - the request body is larger than 1 MiB (1,048,576 bytes).

500Internal Server Error

server.error - an unexpected error; retry with backoff.

503Service Unavailable

server.error - the API is in maintenance mode or is missing its configuration; retry with backoff.