Connect supports two categories of webhooks:
Connection webhooks
When you create a 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 endpoint with the account.id from the payload to fetch credentials.
{
"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
{
"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 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.
In your project settings, 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. Use the Set Project Environment Webhook endpoint: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.
Trigger-level
Pass webhook_url when you deploy a trigger, or update it later with the 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:
Validating signatures
Compute the expected signature with your signing key and compare it to the x-pd-signature header value.
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");
});
Always use a constant-time comparison function (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python) to prevent timing attacks.
Preventing replay attacks
Check the timestamp in the signature header and reject requests that are too old:
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
}