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

# Webhooks

> Receive webhook notifications for account connection events and trigger event deliveries

Connect supports two categories of webhooks:

* **[Connection webhooks](#connection-webhooks)** notify you when an end user connects or fails to connect an account. You define these webhooks when creating a [Connect token](/connect/managed-auth/tokens/).
* **[Trigger webhooks](#trigger-webhooks)** deliver events emitted by [deployed triggers](/connect/components/triggers/). You define these per trigger, or set a default at the project level.

## Connection webhooks

When you [create a Connect token](/connect/api-reference/create-connect-token), you can pass a `webhook_uri`. Pipedream sends a POST request to this URL when the end user successfully connects an account, or if an error occurs during the flow.

### Events

* `CONNECTION_SUCCESS` — the user successfully connected their account
* `CONNECTION_ERROR` — an error occurred during the connection flow

### Payload

#### Successful connection

User credentials are **not** included in the webhook payload. Use the [Retrieve Account](/connect/api-reference/retrieve-account) endpoint with the `account.id` from the payload to fetch credentials.

```json theme={null}
{
  "event": "CONNECTION_SUCCESS",
  "connect_token": "ctok_xxxxxxx",
  "environment": "production",
  "connect_session_id": 1.4058728083872854e+38,
  "account": {
    "id": "apn_xxxxxxx",
    "name": "jane@example.com",
    "external_id": "user-abc-123",
    "healthy": true,
    "dead": null,
    "app": {
      "id": "oa_xxxxxxx",
      "name_slug": "google_sheets",
      "name": "Google Sheets",
      "auth_type": "oauth",
      "description": "Use Google Sheets to create and edit online spreadsheets. Get insights together with secure sharing in real-time and from any device.",
      "img_src": "https://assets.pipedream.net/s.v0/app_168hvn/logo/orig",
      "custom_fields_json": "[]",
      "categories": [
        "Productivity"
      ],
      "featured_weight": 1000000098,
      "connect": {
        "allowed_domains": [
          "www.googleapis.com",
          "sheets.googleapis.com"
        ],
        "base_proxy_target_url": "https://sheets.googleapis.com",
        "proxy_enabled": true
      }
    },
    "created_at": "2026-03-25T17:49:15.000Z",
    "updated_at": "2026-03-25T17:49:15.000Z"
  }
}
```

#### Error

```json theme={null}
{
  "event": "CONNECTION_ERROR",
  "connect_token": "ctok_xxxxxxx",
  "environment": "production",
  "connect_session_id": 1.4058727315359677e+38,
  "error": "Test request failed. Please verify your credentials and try again."
}
```

## Trigger webhooks

When you [deploy a trigger](/connect/components/triggers/) with a `webhook_url`, Pipedream delivers that trigger's emitted events to your URL via a POST request.

### Setting a webhook URL

You can define a webhook destination on an individual trigger, or a project. If both are set, the **trigger-level URL takes precedence**.

#### Project-level

Set a default webhook URL for all triggers in a project. You can configure separate URLs for each environment.

<Tabs>
  <Tab title="UI" icon="window">
    In your [project settings](https://pipedream.com/projects), add webhook URLs under the **Webhooks** section for each environment.

    When you save a new webhook URL, we'll display the signing key for you to copy and use to validate signatures. Note that we will never show the signing key again, so make sure to note it down.

    You can also regenerate the signing key and delete or update the webhook URL at any time.
  </Tab>

  <Tab title="API" icon="code">
    Use the [Set Project Environment Webhook](/connect/api-reference/set-project-environment-webhook) endpoint:

    ```sh theme={null}
    curl -X PUT https://api.pipedream.com/v1/connect/{project_id}/webhook \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer {access_token}" \
      -H "x-pd-environment: production" \
      -d '{
        "webhook_url": "https://events.example.com/pipedream"
      }'
    ```

    Pipedream returns a signing key in the response. Store it securely — you'll need it to [validate signatures](#validating-signatures).
  </Tab>
</Tabs>

#### Trigger-level

Pass `webhook_url` when you [deploy a trigger](/connect/api-reference/deploy-trigger), or update it later with the [Update Trigger Webhooks](/connect/api-reference/update-trigger-webhooks) endpoint. Pipedream returns a signing key in the response.

### Webhook signatures

Pipedream signs every trigger webhook delivery using HMAC-SHA256. Each request includes an `x-pd-signature` header:

```
t=1616000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

* `t` — UNIX timestamp (seconds) when the signature was generated
* `v1` — HMAC-SHA256 hex digest of the signed payload

The signed payload is generated from the timestamp, a `.`, and the raw request body:

```
{timestamp}.{raw_body}
```

### Validating signatures

Compute the expected signature with your signing key and compare it to the `x-pd-signature` header value.

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from "crypto";

  function verifyWebhookSignature(signingKey, signatureHeader, rawBody) {
    const [tPart, v1Part] = signatureHeader.split(",");
    const timestamp = tPart.split("=")[1];
    const receivedSig = v1Part.split("=")[1];

    const signedPayload = `${timestamp}.${rawBody}`;
    const expectedSig = crypto
      .createHmac("sha256", signingKey)
      .update(signedPayload)
      .digest("hex");

    return crypto.timingSafeEqual(
      Buffer.from(receivedSig, "hex"),
      Buffer.from(expectedSig, "hex")
    );
  }

  // Express example
  app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
    const signingKey = process.env.PD_SIGNING_KEY;
    const signatureHeader = req.headers["x-pd-signature"];
    const rawBody = req.body.toString();

    if (!verifyWebhookSignature(signingKey, signatureHeader, rawBody)) {
      return res.status(401).send("Invalid signature");
    }

    // Process the event
    res.status(200).send("OK");
  });
  ```

  ```python Python theme={null}
  import hashlib
  import hmac
  import os

  def verify_webhook_signature(signing_key, signature_header, raw_body):
      t_part, v1_part = signature_header.split(",")
      timestamp = t_part.split("=")[1]
      received_sig = v1_part.split("=")[1]

      signed_payload = f"{timestamp}.{raw_body}"
      expected_sig = hmac.new(
          signing_key.encode("utf-8"),
          signed_payload.encode("utf-8"),
          hashlib.sha256,
      ).hexdigest()

      return hmac.compare_digest(received_sig, expected_sig)

  # Flask example
  @app.route("/webhook", methods=["POST"])
  def webhook():
      signing_key = os.environ["PD_SIGNING_KEY"]
      signature_header = request.headers.get("x-pd-signature")
      raw_body = request.get_data(as_text=True)

      if not verify_webhook_signature(signing_key, signature_header, raw_body):
          return "Invalid signature", 401

      # Process the event
      return "OK", 200
  ```
</CodeGroup>

<Warning>
  Always use a constant-time comparison function (`crypto.timingSafeEqual` in Node.js, `hmac.compare_digest` in Python) to prevent timing attacks.
</Warning>

### Preventing replay attacks

Check the timestamp in the signature header and reject requests that are too old:

```javascript theme={null}
const MAX_AGE_SECONDS = 300; // 5 minutes

const currentTime = Math.floor(Date.now() / 1000);
const signatureAge = currentTime - parseInt(timestamp, 10);

if (signatureAge > MAX_AGE_SECONDS) {
  // Reject the request
}
```
