# Guide to Pipedream Webhooks: Features and Best Practices

Pipedream Connect lets your app's users connect their own accounts on other apps and run Pipedream triggers on their behalf. Connect sends webhooks back to your app at two moments: when a user connects an account (or fails to), and whenever a deployed trigger emits an event, such as a new row in a user's spreadsheet or a new issue in their tracker. If you build on Connect, these webhooks are how your app finds out about new accounts and receives trigger events without polling.

This guide covers the two kinds of Connect webhook, how trigger deliveries are signed and verified, the gaps in Pipedream's delivery documentation, and the best practices for running a receiver in production. It is about webhooks Pipedream sends to your app. HTTP triggers that receive requests into a Pipedream workflow are a different feature.

## What are Pipedream Connect webhooks?

Connect has two categories of webhook, and they behave differently.

**Connection webhooks** report the result of a user's account connection. You set the URL as `webhook_uri` when you create a Connect token, and Pipedream sends a `POST` with `CONNECTION_SUCCESS` or `CONNECTION_ERROR` in the `event` field. Pipedream's docs say nothing about signing these.

**Trigger webhooks** deliver the events a deployed trigger emits. You set the URL on the trigger or as a default for the project, and Pipedream signs every delivery with HMAC-SHA256 in the `x-pd-signature` header. The body is whatever the trigger emits. Pipedream documents no envelope or event-type field for it.

## Pipedream Connect webhook features

| Feature | Connection webhooks | Trigger webhooks |
| --- | --- | --- |
| URL | `webhook_uri` on the Connect token | Trigger-level `webhook_url`, or a project default per environment |
| Method | `POST` | `POST` |
| Events | `CONNECTION_SUCCESS`, `CONNECTION_ERROR` | Whatever the trigger emits |
| Signature | Not documented | `x-pd-signature: t=<unix>,v1=<hex>` |
| Scheme | None documented | HMAC-SHA256 over `{t}.{raw body}`, hex encoded |
| Secret | None | A signing key per destination, shown or returned once |
| Event ID | None | None documented |

Pipedream does not document retries, a response timeout, the status it treats as success, or ordering, for either category.

## Common events

### Connection webhooks

A successful connection includes the account and the app it belongs to. Credentials are not included: use the Retrieve Account endpoint with `account.id` to fetch them.

```json
{
  "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"
    },
    "created_at": "2026-03-25T17:49:15.000Z",
    "updated_at": "2026-03-25T17:49:15.000Z"
  }
}

```

A failed connection carries an `error` string in place of `account`:

```json
{
  "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."
}

```

Note that `connect_session_id` is printed as a very large floating-point number. If you store it, keep it as a string, because a number that size loses precision.

### Trigger webhooks

A trigger webhook's body is the event the trigger component emits, so its shape depends on the trigger. Pipedream doesn't add a documented `event` field to trigger deliveries. Use the URL you configured, such as a path per trigger, to tell triggers apart.

## Setting up Pipedream Connect webhooks

### Connection webhooks

Pass `webhook_uri` when you create a Connect token. Pipedream sends the connection result for that token's flow to the URL.

### Trigger webhooks

Set a project-level default URL for each environment, or a URL on each trigger. If both are set, the trigger-level URL wins.

* **Project default, in the UI:** in your project settings, add a webhook URL for each environment under **Webhooks**. Pipedream shows the signing key when you save, and "will never show the signing key again", so copy it straight away.
* **Project default, with the API:**

```bash
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://example.com/webhooks/pipedream" }'

```

* **Per trigger:** pass `webhook_url` when you deploy a trigger, or change it with Update Trigger Webhooks. The response includes a signing key.

Each destination has its own signing key, and you can regenerate a key at any time. Store each key server-side, for example as `PIPEDREAM_SIGNING_KEY`.

## Securing Pipedream Connect webhooks

For trigger webhooks, parse `t` and `v1` from `x-pd-signature`, compute an HMAC-SHA256 of `{t}.{raw body}` with your signing key, hex-encode it, and compare it in constant time with `v1`.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyPipedreamSignature(rawBody, signatureHeader, signingKey) {
  if (typeof signatureHeader !== 'string' || !signingKey) return null;

  const parts = Object.fromEntries(
    signatureHeader.split(',').map((part) => {
      const index = part.indexOf('=');
      return [part.slice(0, index).trim(), part.slice(index + 1).trim()];
    })
  );
  const timestamp = parts.t;
  const signature = parts.v1;
  if (!timestamp || !signature) return null;

  const expected = crypto
    .createHmac('sha256', signingKey)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest();
  const received = Buffer.from(signature, 'hex');
  if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
    return null;
  }
  return Number(timestamp);
}

const app = express();

app.post('/webhooks/pipedream/triggers', express.raw({ type: '*/*' }), (req, res) => {
  const signingKey = process.env.PIPEDREAM_SIGNING_KEY;
  if (!signingKey) return res.sendStatus(500); // fail closed

  const timestamp = verifyPipedreamSignature(req.body, req.headers['x-pd-signature'], signingKey);
  if (timestamp === null) return res.sendStatus(401);

  // Optional: Pipedream's docs suggest rejecting signatures older than 300 seconds.
  const event = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac

def verify_pipedream_signature(raw_body: bytes, signature_header: str, signing_key: str) -> bool:
    if not signature_header or not signing_key:
        return False
    parts = dict(part.strip().split("=", 1) for part in signature_header.split(",") if "=" in part)
    timestamp, signature = parts.get("t"), parts.get("v1")
    if not timestamp or not signature:
        return False
    signed_payload = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(signing_key.encode("utf-8"), signed_payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected.encode(), signature.encode())

```

Pipedream's docs show an optional replay check: reject the request if `t` is more than 300 seconds old. Whether to apply it is up to you. If you use it, allow for retries from anything between Pipedream and your service.

To check your verifier, use the signing key `pd_signing_key_guide_test` and this exact body:

```
{"issue":{"id":42,"title":"Example"}}

```

With `t=1761040800`, the `x-pd-signature` header should be:

```
t=1761040800,v1=563618387b60ff4870bcfe0b55f3b95711ad9593fb8bddeeea7b320ea0b85bd3

```

Connection webhooks are not documented as signed, so there is nothing to verify. Send them to a separate endpoint and treat them as a prompt to call Retrieve Account, which you authenticate with your own credentials, rather than trusting the body.

> **Make Pipedream webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies `x-pd-signature` on every trigger delivery, stores each event durably, and lets you retry and replay what your service missed.

## Pipedream Connect webhook limitations and pain points

### Connection webhooks have no documented signature

**The Problem:** Anyone who learns your `webhook_uri` can post a fake `CONNECTION_SUCCESS`.

**Why It Happens:** Pipedream documents a signature only for trigger webhooks. The connection webhook section names no header or secret.

**Workarounds:**

* Use a hard-to-guess URL for `webhook_uri`, served over HTTPS.
* Never act on the body alone. Fetch the account with Retrieve Account using `account.id`, and only continue if it exists and belongs to the expected `external_id`.

**How Hookdeck Can Help:** A Hookdeck Pipedream source with verification on checks `x-pd-signature`, so it is for trigger webhooks. Point connection webhooks at a second source with verification off, and you still get storage, [retries](/docs/retries) and replay for them.

### Delivery behaviour is not documented

**The Problem:** You can't tell how long your endpoint has to respond, whether a failed delivery is retried, or whether events arrive in order.

**Why It Happens:** Pipedream's Connect webhook docs don't cover retries, timeouts, the success status or ordering for either category.

**Workarounds:**

* Return a `2xx` as soon as the signature checks out, and process the event afterwards.
* Assume a failed delivery may not come back, and reconcile important state from the Connect API.

**How Hookdeck Can Help:** Hookdeck accepts each delivery as soon as it arrives and stores it, so Pipedream's attempt succeeds even while your service is down. You then deliver to your service with your own [retry](/docs/retries) policy, and [issues](/docs/issues) alert you when it fails.

### Several signing keys

**The Problem:** Signatures fail for some triggers but not others.

**Why It Happens:** The project default and each trigger-level URL have their own signing key. A trigger-level URL takes precedence over the project default, so one receiver can see deliveries signed with different keys.

**Workarounds:**

* Give each signing key its own endpoint path, so you always know which key to use.
* Keep a record of which triggers have their own `webhook_url`.

**How Hookdeck Can Help:** A Hookdeck source holds one signing key. Create one source per key, and each one verifies its own deliveries before routing them to the same service.

### Trigger payloads have no envelope

**The Problem:** Your handler can't tell which trigger sent an event, or whether it has seen it before.

**Why It Happens:** Trigger webhook bodies are exactly what each trigger emits. Pipedream documents no event type, trigger ID or event ID added to them.

**Workarounds:**

* Use a URL per trigger, for example with the trigger ID in the path or query string.
* Build a deduplication key from fields in the emitted event that are unique for that app, such as the record's ID.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeated deliveries based on the fields you choose, and [filters](/docs/filters) can route events by path or by fields in the body.

## Best practices

### Save the signing key when you see it

The UI shows a project's signing key once. If you lose it, regenerate it and update your receiver.

### Verify against the raw body

The signature covers the exact bytes Pipedream sent. Use `express.raw()` or your framework's raw body before parsing JSON.

### Don't trust connection webhooks on their own

Use them as a prompt to fetch the account from the API. Credentials are never in the payload anyway.

### Store large IDs as strings

`connect_session_id` appears as a large floating-point number in Pipedream's examples. Treat it as a string so you don't lose precision.

### Acknowledge fast and process asynchronously

Return `200` after verification and queue the work. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Make handlers idempotent

Processing the same event twice should have the same result as processing it once. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Pipedream Connect sends two kinds of webhook. Connection webhooks report `CONNECTION_SUCCESS` or `CONNECTION_ERROR` and are not documented as signed, so confirm them through the API. Trigger webhooks carry each trigger's emitted events and an `x-pd-signature` HMAC over the timestamp and raw body, which you verify with the signing key for that destination.

[Hookdeck Event Gateway](https://hookdeck.com) verifies trigger signatures, stores every delivery before your service sees it, and adds the retries and replay that Pipedream's docs don't describe.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Pipedream webhooks reliably in minutes.