> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cloudcruise.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Register webhooks to receive updates on the workflow execution

You can register webhooks to receive execution events. Webhooks can be scoped to a single workflow or registered globally for all workflows in your workspace.

**Per-workflow webhooks** fire only for runs of a specific workflow.

**Global webhooks** fire for every workflow in your workspace. Every event payload includes a `workflow_id` field so you can route on your end. Register them in [Settings → Webhooks](https://app.cloudcruise.com/settings/webhooks) by selecting "All Workflows" as the scope.

For the full list of subscribable events you can set in `event_types_subscribed`, see the webhook schema in the [Run API reference](/run-api/start-a-run). Maintenance recovery flows emit the lifecycle events documented below, such as `execution.requeued`, `execution.input_required`, `execution.password_updated`, and the final terminal status.

<AccordionGroup>
  <Accordion title="execution.start">
    This event is sent once when the execution of the workflow starts. It returns a live view URL of the running browser agent embeddable as iframe.

    ```json theme={null}
    {
      "event": "execution.start",
      "payload": {
        "session_id": "64355337-7561-45ec-82bc-b6266e11208c",
        "workflow_id": "745fdceb-53bf-4562-95dc-10495e303f0c",
        "live_view_url": "https://cyclone.cloudcruise.com/..."
      },
      "metadata": {
        "key1": "value1"
      },
      "timestamp": 1750068153,
      "expires_at": 1750068453
    }
    ```
  </Accordion>

  <Accordion title="execution.step">
    This event is sent every time the workflow is executing an action like clicking on a button or filling out an input field.

    ```json theme={null}
    {
        "payload": {
            "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8", 
            "workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731",
            "current_step": "Type user name",
            "current_step_type": "INPUT_TEXT",
            "next_step": "Click Next",
            "next_step_type": "CLICK"
        },
        "metadata": {
            "key1": "value1"
        },
        "timestamp": 1724002463,
        "expires_at": 1724002763,
        "event": "execution.step"
    }
    ```
  </Accordion>

  <Accordion title="execution.queued">
    This event is sent when a run is queued and waiting for an available worker.

    ```json theme={null}
    {
      "event": "execution.queued",
      "payload": {
        "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
        "workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731"
      },
      "metadata": {
        "key1": "value1"
      },
      "timestamp": 1724002463,
      "expires_at": 1724002763
    }
    ```
  </Accordion>

  <Accordion title="execution.paused">
    This event is sent when a run is paused before execution (for example, when workflow-level or credential-level pause controls are active).

    ```json theme={null}
    {
      "event": "execution.paused",
      "payload": {
        "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
        "workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731",
        "pause_reasons": [
          {
            "resource_type": "workflow",
            "resource_id": "00b82d36-8243-4a95-a0ca-e64096226731",
            "reason": "Paused by workspace admin"
          }
        ]
      },
      "timestamp": 1724002463,
      "expires_at": 1724002763
    }
    ```
  </Accordion>

  <Accordion title="execution.success">
    This event is sent once when the execution of the workflow has finished successfully. It includes the result data that is returned from the workflow.

    ```json theme={null}
    {
        "payload": {
            "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8", 
            "workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731",
            "data": {
                "your": "defined",
                "output": "datamodel",
            },
            "errors": [],
            "status": "execution.success",
            "file_urls": [],
            "input_variables": {
                "USER_NAME": "adrian"
            },
            "encrypted_variables": [
                "USER_NAME"
            ],
            "vault_entries": {
                "my_credential": {
                    "USER_ID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                    "USER_ALIAS": "My Login",
                    "USER_NAME": "encrypted_value",
                    "PASSWORD": "encrypted_value",
                    "DOMAIN": "https://example.com"
                }
            },
            "screenshot_urls": [
                {
                    "signed_screenshot_url": "https://cloudcruise.com/screenshots/...",
                    "signed_screenshot_url_expires": "2025-06-01T22:21:00.371579+00:00",
                    "error_screenshot": false,
                    "node_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                    "node_display_name": "Click on login",
                    "timestamp": "2025-05-25T22:21:00.371579+00:00"
                }
            ],
            "video_urls": [
                {
                    "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
                    "signed_screen_recording_url": "https://cloudcruise.com/videos/...",
                    "signed_screen_recording_url_expires": "2025-06-01T22:21:00.371579+00:00",
                    "timestamp": "2025-05-25T22:21:00.371579+00:00"
                }
            ]
        },
        "metadata": {
            "key1": "value1"
        },
        "timestamp": 1724002463,
        "expires_at": 1724002763,
        "event": "execution.success"
    }
    ```
  </Accordion>

  <Accordion title="execution.failed">
    This event is sent once the execution of the workflow fails and contains information about the error.

    ```json theme={null}
    {
        "payload": {
            "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8", 
            "workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731",
            "data": {
                "your": "defined",
                "output": "datamodel",
            },
            "errors": [{
                "message": "The login failed. The website showed an error that the credentials are invalid.",
                "error_id": "c4c7d4fe-b00c-49d8-ab67-40808acea59c",
                "error_code": "E001: LOGIN_FAILED",
                "llm_error_category": "AUTHENTICATION_ERROR",
                "llm_error_sub_type": "INVALID_CREDENTIALS",
                "original_error": "Element not found: //button[@id='login-submit']",
                "full_url": "https://cloudcruise.com/login",
                "action_type": "CLICK",
                "action_display_name": "Click on login"
            }],
            "status": "execution.failed",
            "file_urls": [],
            "input_variables": {
                "$USER_NAME": "adrian"
            },
            "encrypted_variables": [
                "USER_NAME"
            ],
            "vault_entries": {
                "my_credential": {
                    "USER_ID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                    "USER_ALIAS": "My Login",
                    "USER_NAME": "encrypted_value",
                    "PASSWORD": "encrypted_value",
                    "DOMAIN": "https://example.com"
                }
            },
            "screenshot_urls": [
                {
                    "signed_screenshot_url": "https://cloudcruise.com/screenshots/...",
                    "signed_screenshot_url_expires": "2025-06-01T22:21:00.371579+00:00",
                    "error_screenshot": true,
                    "node_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                    "node_display_name": "Click on login",
                    "timestamp": "2025-05-25T22:21:00.371579+00:00"
                }
            ],
            "video_urls": [
                {
                    "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
                    "signed_screen_recording_url": "https://cloudcruise.com/videos/...",
                    "signed_screen_recording_url_expires": "2025-06-01T22:21:00.371579+00:00",
                    "timestamp": "2025-05-25T22:21:00.371579+00:00"
                }
            ]
        },
        "timestamp": 1724002463,
        "expires_at": 1724002763,
        "event": "execution.failed"
    }
    ```

    The `llm_error_category` field contains an AI-generated categorization of the error type. This field may be `null` if AI analysis was not performed.

    The `llm_error_sub_type` field contains a more specific sub‑classification within the category. See the [error classification matrix](/concepts/maintenance-agent#error-classification-matrix) for all possible values. This field may be `null` if no sub‑type applies.

    The `original_error` field contains the raw technical runtime error message before any AI analysis. This is useful for debugging and differs from `message` field that contains the Maintenance Agent's analysis.
  </Accordion>

  <Accordion title="execution.stopped">
    This event is sent when one or several workflow runs were cancelled. It respectively either contains `session_id` of the cancelled run or `session_ids` that contains all  of the cancelled runs.

    Response for single run cancelled:

    ```json theme={null}
    {
      "payload": {
        "message": "User interrupted workflow. Session was stopped.",
        "error_code": "INTERRUPTED-E0001",
        "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8", 
      },
      "timestamp": 1724002463,
      "expires_at": 1724002763,
      "event": "execution.stopped"
    }
    ```

    Response for multiple cancelled at once:

    ```json theme={null}
    {
      "payload": {
        "message": "Execution was interrupted because another workflow was executed with the same credentials, but failed its login attempt. This is to prevent repeated incorrect login attempts.",
        "error_code": "INTERRUPTED-E0002",
        "session_causing_error": "60673dc6-92a1-428a-ba51-8c3f324daea8",
        "session_ids": ["60673dc6-92a1-428a-ba51-8c3f324daea8", "50673dc6-92a1-428a-ba51-8c3f324daea8"]
      },
      "metadata": {
            "key1": "value1"
      },
      "timestamp": 1724002463,
      "expires_at": 1724002763,
      "event": "execution.stopped"
    }
    ```
  </Accordion>

  <Accordion title="execution.requeued">
    This event is sent when a failed run is automatically requeued for retry by the Maintenance Agent (e.g., after a `SERVICE_UNAVAILABLE` classification). See [Service Unavailable Recovery](/concepts/maintenance-agent#service-unavailable-recovery) for retry schedule details.

    ```json theme={null}
    {
      "event": "execution.requeued",
      "payload": {
        "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
        "workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731",
        "retry_attempt": 2,
        "max_retries": 10,
        "next_execution_time": "2026-05-26T23:30:00.000Z",
        "delay_ms": 1200000
      },
      "metadata": {
        "key1": "value1"
      },
      "timestamp": 1750068153,
      "expires_at": 1750068453
    }
    ```
  </Accordion>

  <Accordion title="execution.input_required">
    This event is sent when a run pauses because incorrect input data was detected and the workflow is waiting for corrected values. Submit corrected fields using the [Submit New Input Variables endpoint](/run-api/submit-new-input-variables) to resume execution.

    ```json theme={null}
    {
      "event": "execution.input_required",
      "payload": {
        "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
        "input_variables": {
          "patient_id": "ABC-123"
        },
        "screenshot_url": "https://cloudcruise.com/screenshots/..."
      },
      "timestamp": 1724002463,
      "expires_at": 1724002763
    }
    ```
  </Accordion>

  <Accordion title="execution.password_updated">
    This event is sent after an automated password recovery flow succeeds and CloudCruise updates the vault credential used by the run.

    ```json theme={null}
    {
      "event": "execution.password_updated",
      "payload": {
        "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
        "workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731"
      },
      "timestamp": 1724002463,
      "expires_at": 1724002763
    }
    ```
  </Accordion>

  <Accordion title="screenshot.uploaded">
    This event is sent when the upload of the screenshot has finished.

    | Field                    | Description                                                                                                                                                                               |
    | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `error_screenshot`       | `true` when the screenshot was captured during an error.                                                                                                                                  |
    | `full_length_screenshot` | `true` when the screenshot is a full-page capture (scrolled and stitched). This happens for dedicated Screenshot nodes and error screenshots. Viewport-only captures set this to `false`. |
    | `retry_index`            | The retry attempt index for the run (0 = first attempt).                                                                                                                                  |

    ```json theme={null}
    {
        "payload": {
            "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
            "screenshot_id": "6fdfad4a-3064-48cf-99f0-3d8d271f2c8e",
            "signed_screenshot_url": "https://cloudcruise.com/screenshots/...",
            "signed_screenshot_url_expires": "2024-08-25T17:35:00.432Z",
            "node_display_name": "Click on login",
            "node_id": "a1b2c3d4-5678-9abc-def0-1234567890ab",
            "error_screenshot": true,
            "timestamp": "2024-08-30T17:35:00.432Z",
            "retry_index": 0,
            "full_length_screenshot": false,
            "metadata": {}
        },
        "metadata": {
            "key1": "value1"
        },
        "timestamp": 1724002463,
        "expires_at": 1724002763,
        "event": "screenshot.uploaded"
    }
    ```
  </Accordion>

  <Accordion title="video.uploaded">
    This event is sent when the upload of the workflow video has finished.

    ```json theme={null}
    {
        "payload": {
            "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8", 
            "signed_screen_recording_url": "https://cloudcruise.com/...",
            "signed_screen_recording_url_expires": "2025-06-01T22:21:00.371579+00:00",
            "timestamp": "2025-05-25T22:21:00.371579+00:00"
        },
        "metadata": {
            "key1": "value1"
        },
        "timestamp": 1724002463,
        "expires_at": 1724002763,
        "event": "video.uploaded"
    }
    ```
  </Accordion>

  <Accordion title="file.uploaded">
    This event is sent when a file has been uploaded during workflow execution. The `signed_file_url` expiration is configurable per workspace (default: 7 days, range: 1 hour to 7 days). Configure this in your [workspace settings](https://app.cloudcruise.com/settings/data-retention).

    ```json theme={null}
    {
        "payload": {
            "signed_file_url": "https://example.com/storage/v1/object/sign/files/123/456/example.zip?token=dummy-token",
            "file_name": "example_file.zip",
            "timestamp": "2024-03-20T12:00:00.000Z",
            "signed_file_url_expires": "2024-03-27T12:00:00.000Z",
            "metadata": {
                "key1": "value1"
            },
            "session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
        },
        "timestamp": 1710936000,
        "expires_at": 1710936300,
        "event": "file.uploaded"
    }
    ```
  </Accordion>

  <Accordion title="interaction.waiting">
    This event is sent when user input is required to continue the workflow.

    ```json theme={null}
    {
        "payload": {
            "current_step": "Enter 2FA Code",
            "missing_properties": ["two_factor_code"],
            "message": "Please enter the 6-digit code sent to your phone",
            "session_id": "session-123",
            "workflow_id": "workflow-456",
            "expected_json_schema_datamodel": {
                "type": "object",
                "properties": {
                    "two_factor_code": {
                        "type": "string",
                        "pattern": "^\\d{6}$",
                        "description": "6-digit verification code"
                    }
                },
                "required": ["two_factor_code"]
            }
        },
        "timestamp": 1710936000,
        "expires_at": 1710936300,
        "event": "interaction.waiting"
    }
    ```
  </Accordion>

  <Accordion title="interaction.failed">
    This event is sent when the user input was not provided in time or in a wrong format.

    ```json theme={null}
    {
        "timestamp": 1724002463,
        "expires_at": 1724002763,
        "event": "interaction.failed"
    }
    ```
  </Accordion>

  <Accordion title="interaction.finished">
    This event is sent when the user input was provided and the workflow continues.

    ```json theme={null}
    {
        "payload": {
            "current_step": "Enter 2FA Code",
            "message": "Please enter the 6-digit code sent to your phone",
            "session_id": "session-123",
            "workflow_id": "workflow-456",
            "provided_input": {
                "two_factor_code": 123456
            },
            "expected_json_schema_datamodel": {
                "type": "object",
                "properties": {
                    "two_factor_code": {
                        "type": "string",
                        "pattern": "^\\d{6}$",
                        "description": "6-digit verification code"
                    }
                },
                "required": ["two_factor_code"]
            }       
        },
        "timestamp": 1724002463,
        "expires_at": 1724002763,
        "event": "interaction.finished"
    }
    ```
  </Accordion>
</AccordionGroup>

Simply navigate to our [settings](https://app.cloudcruise.com/settings) page to get started.

## Security

We sign webhook events with the secret you get when you register a webhook. We also add an expiry time. You can change the expiry time in the setting of the registered webhook.

Here's how we recommend to verify the message you receive from us:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import time
    import hashlib
    import hmac
    import json

    class VerificationError(Exception):
        """Custom exception to handle verification errors."""
        def __init__(self, message="Verification failed", status_code=400):
            super().__init__(message)
            self.status_code = status_code

    def verify_hmac(received_data, received_signature, secret_key):
        # Ensure the received data is in bytes, necessary for HMAC
        if isinstance(received_data, str):
            received_data = received_data.encode()

        # Generate the HMAC new object with the secret key and SHA-256
        calculated_signature = hmac.new(secret_key.encode(), received_data, hashlib.sha256).hexdigest()

        # Safely compare the computed HMAC with the received HMAC
        return hmac.compare_digest(calculated_signature, received_signature)

    # Extract signature from X-HMAC-Signature header, removing the 'Sha256=' prefix
    # receivedData = await request.body()
    # receivedSignature = request.headers.get('X-HMAC-Signature').split('=')[1]
    # secretKey = your_secret_key
    def verify_message(received_data, received_signature, secret_key):
        if not received_data:
            raise VerificationError("Received request without body", 400)
        
        try:
            data_json = json.loads(received_data.decode('utf-8'))
        except json.JSONDecodeError as e:
            raise VerificationError("Failed to decode json: " + str(e), 400)
        
        # Check if the expiration is sent
        expires_at = data_json.get("expires_at")
        if not expires_at:
            raise VerificationError("No expiration date sent", 400)

        # Verify HMAC first
        if not verify_hmac(received_data, received_signature, secret_key):
            raise VerificationError("Invalid HMAC signature.", 401)

        # Check if the message is not expired
        if time.time() > expires_at:
            raise VerificationError("Webhook message expired.", 400)

        return data_json

    ```
  </Tab>

  <Tab title="Javascript">
    Install the package by following the [TS/JS SDK overview](/sdk/js/overview).

    ```javascript theme={null}
    import express from "express";
    import { CloudCruise, VerificationError, WebhookEventType } from "cloudcruise";

    const client = new CloudCruise({
        apiKey: process.env.CLOUDCRUISE_API_KEY,
        encryptionKey: process.env.CLOUDCRUISE_ENCRYPTION_KEY,
    });

    const app = express();

    // Use a raw body parser so the SDK can verify the signature accurately
    app.post(
        "/webhook",
        express.raw({ type: "application/json" }),
        (req, res) => {
            try {
                const signature = req.get("x-hmac-signature");
                const event = client.webhook.verifySignature(
                    req.body,
                    signature,
                    process.env.CLOUDCRUISE_WEBHOOK_SECRET
                );

                switch (event.event) {
                    case WebhookEventType.ExecutionSuccess:
                        console.log("Workflow completed successfully");
                        break;
                    case WebhookEventType.ExecutionFailed:
                        console.log("Workflow failed");
                        break;
                }

                res.status(200).json({ status: "received" });
            } catch (error) {
                console.error("Invalid webhook", error);
                res.status(400).json({ error: "Invalid webhook" });
            }
        }
    );
    ```
  </Tab>
</Tabs>

## IP Allowlisting

All webhook requests from CloudCruise originate from the following static IP addresses. If your infrastructure requires IP-based filtering, add these to your allowlist:

```
35.164.71.220
54.191.200.70
18.236.26.110
```

## Deliverability

CloudCruise automatically retries failed webhook deliveries up to **3 times** (4 total attempts including the initial request) with exponential backoff delays between attempts.

Each delivery attempt has a 5-second request timeout. Total delivery time depends on failure mode, so retries may complete quickly for immediate network errors or take longer for repeated timeouts.

### Retry Conditions

A webhook delivery is retried when:

* **Network errors** occur (connection timeout, DNS failure, etc.)
* **Server errors** are returned (HTTP status codes 500 and above)
* **Request timeouts** exceed 5 seconds

Webhooks are **not** retried for client errors (4xx status codes) as these typically indicate configuration issues that won't resolve with retries.

### Manual Replay

If webhooks still fail after automatic retries, you can manually replay them:

* **Dashboard**: Navigate to the run detail view and trigger a resend
* **API**: Send a POST request to `https://api.cloudcruise.com/webhooks/{session_id}/replay`

The replay endpoint will re-send all webhook events that were recorded for the given session.
