---
id: cloud/integrations/webhooks
title: Integrate Webhooks with Cypress Cloud
description: >-
  Send Cypress Cloud run events to your own HTTPS endpoints, and verify their
  authenticity with a signing secret.
section: cloud
source_path: docs/cloud/integrations/webhooks.mdx
version: b4b6c6d032cfec7c0d8b4e4f4e55bd0096d01203
updated_at: '2026-08-31T21:43:49.892Z'
---
# Webhooks Integration

Webhooks let Cypress Cloud send a real-time HTTP request to an endpoint you own whenever a selected event occurs, for example when a run finishes.

## Why Cypress Cloud Webhooks

A webhook lets you build **custom automations**: update a dashboard, open a ticket, gate a deployment, or forward results into any internal system.

Following the increased pace of AI-driven development, custom workflows have become more important than ever. You may use the initial "run completed" webhook in multiple ways:

*   Triage runs programmatically for certain characteristics that require attention, then dive deeper using tools like Cypress Cloud [CLI](/llm/markdown/cloud/integrations/cloud-cli.md) or [MCP](/llm/markdown/cloud/integrations/cloud-mcp.md) to retrieve specific failures or other details.
*   Automatically feed results into your own custom real-time reporting.
*   Create Jira issues based on failed runs.
*   Deliver customized Slack notifications, or notifications in any platform, based on criteria that are not customizable in Cypress Cloud.

While Cypress Cloud already has many built-in methods of notifications related to run statuses (for example, [Desktop notifications](/llm/markdown/app/core-concepts/open-mode.md#Cloud-run-notifications) and [Slack](/llm/markdown/cloud/integrations/slack.md) messages), webhooks deliver a **raw JSON payload** to your own service.

**Early access**: Webhooks will be available on all paid plans and during trials. The feature is rolling to admins and owners of Cypress Cloud organizations gradually, and may not yet be available for every organization. If you do not see webhooks in your project settings but want to try it early, contact your Cypress representative.

## Getting started

**You need two things to get started:**

*   **The ability to manage webhooks for the project.** You must be an **Owner**, **Admin**, or **Team Admin** of your Cypress Cloud organization organization. See [User roles](/llm/markdown/cloud/account-management/users.md#User-roles) for details. Anyone who can view the project can _view_ webhooks, but only those roles can create, edit, enable, disable, delete, test, or redeliver them.
*   **A publicly-reachable endpoint** that accepts `POST` requests. **HTTPS is recommended.** Cypress Cloud will not deliver to private, loopback, or internal network addresses (see [Security](#Security)). HTTP URLs are accepted, but be aware HTTP payloads travel unencrypted.

Webhooks are configured **per project**. You can create up to **5 webhooks per project**.

## Create a webhook

1.  Open your project and click **Settings** at the bottom of the project sidebar.
    
2.  Open the **General** tab and locate the **Webhooks** panel.
    
3.  Click **Add webhook**.
    

In the **Add webhook** dialog, configure the following:

*   **Payload URL** _(required)_: The URL Cypress Cloud will send a `POST` request to, for example `https://example.com/receive-webhook`.
*   **Secret** _(optional, recommended)_: A signing secret used to verify that a request genuinely came from Cypress Cloud. Provide your own value (at least 16 characters) or click **Generate secret** to create a random one. **Copy and store the secret somewhere safe. It is shown only once and cannot be viewed again**. If you lose it, you can rotate it later by editing the webhook. See [Verify webhook signatures](#Verify-webhook-signatures).
*   **Events that will trigger webhook**: Select which events should be delivered to this endpoint. At least one event is required. **Currently, only the `run.completed` event is available**, so it is preselected by default and cannot be removed.
*   **Enable this webhook**: Whether the webhook is active. Enabled by default; disabled webhooks not called when

Click **Add webhook** to save. Cypress Cloud will begin delivering the selected events to your endpoint.

## Send a test event

To confirm your endpoint is reachable and your verification logic works, use **Send test** on an enabled webhook. This delivers a synthetic `webhook.ping` event through the same signing and delivery pipeline as real events, so it exercises your signature verification exactly as a `run.completed` event would.

The `webhook.ping` request body is:

```
{
  "message": "Webhook ping"
}
```

Test events are delivered once and are **not** retried, regardless of the outcome. The resulting delivery appears in the webhook's delivery history like any other.

## Manage webhooks

Each webhook appears as a row in the **Webhooks** panel showing its URL, an enable/disable toggle, and the status of its most recent delivery (for example, _Last delivery succeeded_, _Retrying_, _Last delivery failed_, _Delivering…_, or _Never triggered_). Use the inline controls and the _**Options (⋯)**_ menu to manage it:

*   **Enable or disable**: Toggle the switch on the row. A disabled webhook keeps its configuration but receives no deliveries.
*   **Edit**: Open the Options menu and select **Edit** to change the URL, events, or secret.
    *   **Generate new secret**: Leaving the **Secret** field blank keeps the existing secret. To rotate it, enter a new value (at least 16 characters) or click **Generate new secret**. Copy the new secret and store it somewhere safe. It is shown only once. After you save, the previous secret no longer verifies requests, so update your endpoint to use the new value.
*   **Recent deliveries**: Open the Options menu and select **Recent deliveries** to review delivery history. See [Delivery history and retries](#Delivery-history-and-retries).
*   **Delete**: Open the Options menu and select **Delete**. This permanently removes the webhook **and its delivery history**. This cannot be undone.

A project can have at most **5 webhooks**. When you reach the limit, the **Add webhook** button is disabled until you delete an existing webhook.

## Events

Currently only one event in the Cypress Cloud lifecycle triggers a webhook:

| Event | Description |
| --- | --- |
| `run.completed` | Sent when a Cypress Cloud run finished and resolves to one of the following states: `passed`, `failed`, `errored`, `timedOut`, or `cancelled`. |

There is also a special `webhook.ping` event that is **only** sent by the [Send a test event](#Send-a-test-event) action. You cannot subscribe to it directly.

## Payload

Cypress Cloud sends a `POST` request whose **body is the event payload as JSON**. Metadata about the delivery travels in `X-Cypress-*` request headers rather than in the body.

### Request headers

Every delivery includes the following headers:

| Header | Description |
| --- | --- |
| `Content-Type` | Always `application/json`. |
| `X-Cypress-Event` | The event type, for example `run.completed`. |
| `X-Cypress-Event-Id` | Unique ID for the event. **Stable across retries**. Use it to deduplicate. |
| `X-Cypress-Event-Version` | The payload schema version, for example `1`. |
| `X-Cypress-Request-Id` | Unique ID for this individual delivery attempt. A **new** value on every retry. |
| `X-Cypress-Timestamp` | The time the request was sent, as a Unix timestamp in seconds. |
| `X-Cypress-Idempotency-Key` | Stable key for this event and webhook. Identical across retries of the same event. |
| `X-Cypress-Signature` | HMAC-SHA256 signature of the payload. Only present when a signing secret is set. See [Verify webhook signatures](#Verify-webhook-signatures). |

Because `X-Cypress-Event-Id` (and `X-Cypress-Idempotency-Key`) stay constant across retries, your endpoint may receive the same event more than once. Treat delivery as **at-least-once** and make your handler idempotent by keying on `X-Cypress-Event-Id`.

### `run.completed` payload

The `run.completed` payload (version `1`) is a flat JSON object with the following fields:

| Field | Type | Description |
| --- | --- | --- |
| `runNumber` | `number` | The run's number in Cypress Cloud. |
| `runUrl` | `string` | Link to the run in Cypress Cloud. |
| `projectId` | `string` | The [Project ID](/llm/markdown/cloud/account-management/projects.md#Project-ID) used for the run. |
| `projectName` | `string` | The project name, which can be chosen in your Cypress Cloud settings. |
| `status` | `string` | Run status: `passed`, `failed`, `errored`, `timedOut`, or `cancelled`. |
| `statusReason` | `string` | Human-readable explanation of the status. |
| `createdAt` | `string` | ISO 8601 timestamp when the run started. |
| `completedAt` | `string` | `null` | ISO 8601 timestamp when the run finished. |
| `totalDuration` | `number` | `null` | Total run duration in milliseconds. |
| `totalTests` | `number` | `null` | Total number of tests. |
| `totalPassed` | `number` | `null` | Number of passed tests. |
| `totalFailed` | `number` | `null` | Number of failed tests. |
| `totalPending` | `number` | `null` | Number of pending tests. |
| `totalSkipped` | `number` | `null` | Number of skipped tests. |
| `flakyTestCount` | `number` | `null` | Number of passing tests that failed at least one attempt and passed using [retries](/llm/markdown/app/guides/test-retries.md). |
| `totalSpecs` | `number` | `null` | Number of spec files. |
| `commitSha` | `string` | `null` | The commit SHA the run was recorded against. |
| `commitBranch` | `string` | `null` | The branch name. |
| `commitMessage` | `string` | `null` | The commit message. |
| `commitAuthorName` | `string` | `null` | The commit author's name. |
| `commitAuthorEmail` | `string` | `null` | The commit author's email. |
| `commitUrl` | `string` | `null` | Link to the commit. |
| `ciProvider` | `string` | `null` | The detected CI provider, for example `circleci`. |
| `ciBuildId` | `string` | `null` | The CI build ID. |
| `ciUrl` | `string` | `null` | Link to the CI build. |
| `ciPullRequestId` | `string` | `null` | The pull request ID, when available. |
| `ciPullRequestUrl` | `string` | `null` | Link to the pull request, when available. |
| `tags` | `string[]` | The tags the run was recorded with (sorted). |
| `groups` | `string[]` | The run groups (sorted). |

**Missing fields**: If you are missing expected commit information or CI details, ensure the related environment variables are present in **the context where Cypress executes**. For example, when executing Cypress in a container, your [CI identifiers](/llm/markdown/cloud/features/smart-orchestration/parallelization.md#CI-Build-ID-environment-variables-by-provider) and related build information may need to be set explicitly.

Example `run.completed` request body

```
{
  "runNumber": 42,
  "runUrl": "https://cloud.cypress.io/projects/my-project/runs/42",
  "projectId": "my-project",
  "projectName": "My Project",
  "status": "passed",
  "statusReason": "Run passed",
  "createdAt": "2026-01-01T00:00:00.000Z",
  "completedAt": "2026-01-01T00:05:00.000Z",
  "totalDuration": 300000,
  "totalTests": 100,
  "totalPassed": 95,
  "totalFailed": 0,
  "totalPending": 2,
  "totalSkipped": 3,
  "flakyTestCount": 0,
  "totalSpecs": 4,
  "commitSha": "abc123",
  "commitBranch": "main",
  "commitMessage": "fix: something",
  "commitAuthorName": "Ada Lovelace",
  "commitAuthorEmail": "ada@example.com",
  "commitUrl": "https://github.com/cypress-io/cypress-services/commit/abc123",
  "ciProvider": "circleci",
  "ciBuildId": "circleci-workflow-777",
  "ciUrl": "https://circleci.com/gh/cypress-io/cypress-services/555",
  "ciPullRequestId": null,
  "ciPullRequestUrl": null,
  "tags": ["canary", "nightly"],
  "groups": ["chrome", "firefox"]
}
```

## Verify webhook signatures

When you configure a **signing secret**, Cypress Cloud signs every request so your endpoint can confirm that a payload genuinely came from Cypress Cloud and was not altered in transit. **We strongly recommend setting a signing secret for every webhook.** Because your Payload URL is a publicly reachable endpoint, anyone who discovers it could send it forged requests; verifying the signature lets your handler reject anything that isn't authentically from Cypress Cloud.

The signing secret provides three things:

*   **Authenticity**: proof the request came from Cypress Cloud, not an impersonator.
*   **Integrity**: proof the payload was not tampered with, since any change invalidates the signature.
*   **Replay protection**: the signature covers a timestamp, so you can reject stale requests that are replayed later.

A signing secret does **not** encrypt the payload. Use HTTPS when the payload must stay confidential. Over HTTP, anyone on the network path can still read the request body and headers. The HMAC still proves the request came from Cypress Cloud and was not modified, because the secret itself is never sent.

### How the signature is computed

When a secret is set, Cypress Cloud includes an `X-Cypress-Signature` header:

```
X-Cypress-Signature: sha256=<hex-encoded HMAC>
```

The signature is an **HMAC-SHA256**, keyed with your signing secret, of the string `` `${timestamp}.${rawBody}` `` (the value of the `X-Cypress-Timestamp` header, a literal `.`, then the **exact raw bytes of the request body**). The result is hex-encoded and prefixed with `sha256=`.

To verify a request:

1.  Read the `X-Cypress-Signature` and `X-Cypress-Timestamp` headers and the **raw** request body (before any JSON parsing; parsing and re-serializing can change the bytes and break verification).
2.  Optionally reject the request if the timestamp is too old (for example, more than five minutes) to guard against replay attacks.
3.  Recompute `HMAC-SHA256(secret, "${timestamp}.${rawBody}")`, hex-encode it, and prefix `sha256=`.
4.  Compare it to `X-Cypress-Signature` using a **constant-time** comparison.

If a webhook has no signing secret, Cypress Cloud still delivers events, but the `X-Cypress-Signature` header is omitted. Configure a secret to enable verification.

### Verify a signature in your handler

The following example handlers add a route to an existing Express or Flask app. They read the raw body, reject stale timestamps, recompute the HMAC, and compare it in constant time.

Node.js and Python examples

*   Node.js
*   Python

```
const crypto = require('crypto')

const SIGNING_SECRET = process.env.CYPRESS_WEBHOOK_SECRET

// Register this route before express.json() or any other body parser.
// Those parsers consume the body and would break signature verification.
app.post(
  '/receive-webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.get('X-Cypress-Signature')
    const timestamp = req.get('X-Cypress-Timestamp')
    const rawBody = req.body // Buffer

    if (!signature || !timestamp) {
      return res.status(400).send('Missing signature headers')
    }

    // Reject stale requests to protect against replay attacks (5 minute window).
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
      return res.status(400).send('Timestamp outside of tolerance')
    }

    const expected =
      'sha256=' +
      crypto
        .createHmac('sha256', SIGNING_SECRET)
        .update(`${timestamp}.${rawBody.toString('utf8')}`)
        .digest('hex')

    const valid =
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))

    if (!valid) {
      return res.status(401).send('Invalid signature')
    }

    const event = JSON.parse(rawBody.toString('utf8'))
    // ...handle the event...

    res.status(200).send('ok')
  }
)
```

```
import hashlib
import hmac
import os
import time

from flask import abort, request

SIGNING_SECRET = os.environ["CYPRESS_WEBHOOK_SECRET"].encode()


@app.post("/receive-webhook")
def receive_webhook():
    signature = request.headers.get("X-Cypress-Signature", "")
    timestamp = request.headers.get("X-Cypress-Timestamp", "")
    # Bytes as received. Don't use request.json; parsing and re-serializing
    # can change the body and break verification.
    raw_body = request.get_data()

    if not signature or not timestamp:
        abort(400, "Missing signature headers")

    # Reject stale requests to protect against replay attacks (5 minute window).
    if abs(time.time() - int(timestamp)) > 300:
        abort(400, "Timestamp outside of tolerance")

    signed = f"{timestamp}.".encode() + raw_body
    digest = hmac.new(SIGNING_SECRET, signed, hashlib.sha256).hexdigest()
    expected = f"sha256={digest}"

    if not hmac.compare_digest(signature, expected):
        abort(401, "Invalid signature")

    # event = json.loads(raw_body)
    # ...handle the event...

    return "ok", 200
```

## Delivery history and retries

Open the Options menu on a webhook and select **Recent deliveries** to review its delivery history. Cypress Cloud retains up to **100** deliveries per webhook, newest first. Each entry shows the event type and version, timestamps, and a status. Expand a delivery to inspect the full per-attempt request and response: headers, bodies, response status, and any error details.

Delivery statuses:

*   **Delivered**: The endpoint returned a success response.
*   **Pending**: The delivery is queued or in progress.
*   **Retrying**: An attempt failed and another is scheduled.
*   **Failed. No more retries.**: The delivery failed and will not be retried. Automatic retries are exhausted, the failure is not retryable (for example a `3xx` or a `4xx` other than `408` or `429`), or the event was a test (`webhook.ping`).

For your protection, sensitive request headers (such as `Authorization` and `Cookie`) are redacted in the stored delivery history.

### Retry behavior

If a delivery fails, Cypress Cloud retries it until it succeeds or reaches **10 total attempts** (the first attempt plus up to 9 retries), using an exponential backoff schedule with jitter. Cypress Cloud retries on network/transport errors and on HTTP `408`, `429`, and `5xx` responses. Other completed responses (`3xx` and other `4xx`) and blocked destinations are treated as permanent failures and are **not** retried. Test (`webhook.ping`) events are never retried.

Each attempt is listed under that delivery in **Recent deliveries**. Expand the delivery to see **Attempt 1**, **Attempt 2**, and so on. Automatic retries are tagged **Retry**; a redelivery you trigger is tagged **Manual retry**.

To resend a failed or exhausted delivery yourself, expand it in the **Recent deliveries** dialog and choose **Redeliver**. A redelivery reuses the original payload and keeps the same `X-Cypress-Event-Id`, so your idempotency logic still recognizes it as the same event.

## Security

We recommend that you always use an `https://` Payload URL so payloads are encrypted in transit.

In addition, Cypress Cloud applies several protections to webhook delivery:

*   **Endpoint restrictions**: Destination URLs are resolved and validated before each delivery. Requests to private, loopback, and link-local addresses are blocked to prevent server-side request forgery (SSRF).
*   **No redirects**: Cypress Cloud does not follow HTTP redirects; the Payload URL must respond directly.
*   **Timeouts and limits**: Each attempt times out after 10 seconds, and only the first 8 KB of your response body is read and stored.
*   **Secret storage**: Signing secrets are encrypted at rest and are never returned by the Cypress Cloud API. A secret is shown only once, when you set it.

## Troubleshooting

### Run completed webhook fires too late

The `run.completed` webhook is delivered when Cypress Cloud marks the run as complete. If you record in parallel or with multiple groups, Cypress Cloud waits for the [Run Completion Delay](/llm/markdown/cloud/account-management/projects.md#Run-Completion-Delay) (60 seconds by default) after the last known group finishes, in case more groups still join. That wait is what delays the webhook, because if all tests haven't been recorded yet, the final state of the run is not known.

To complete the run as soon as your expected groups have finished, call the [Run Completion API](/llm/markdown/cloud/features/smart-orchestration/parallelization.md#Run-Completion-API). Cypress Cloud then skips the remaining delay, and the webhook is delivered when in-progress groups finish.
