Guide to Paragon Webhooks: Features and Best Practices
Paragon webhooks let your own systems hear when the integrations you've embedded with Paragon go wrong. When a workflow fails for one of your customers, or a customer's OAuth credential expires and can't be refreshed, a Webhook Destination can tell your alerting, logging or support tooling straight away, so you can fix the workflow or ask the customer to reconnect.
This guide covers Paragon's monitoring Webhook Destinations: how to set one up, the two events they send and what the payloads look like, how to authenticate requests that Paragon doesn't sign, and the best practices for production. It also explains how they differ from Managed Sync webhooks, which are a separate feature.
What are Paragon webhooks?
Paragon, the embedded integration platform, has two features that send webhooks:
- Webhook Destinations are monitoring notifications for your Paragon project. They're a kind of Event Destination, configured under Settings > Monitoring, and they send a
POSTwhen a workflow fails or a connected user's credential can't be refreshed. They're available on paid plans and only Admin users can manage them. - Managed Sync webhooks report records created, updated or deleted in a Sync. They're configured on the Syncs page, send a
webhook_verificationrequest when you save the URL, and have their own event types.
This guide is about Webhook Destinations, which is what Hookdeck's Paragon source covers. Paragon documents no authentication for Managed Sync webhooks, so there's nothing for a source to check there.
Webhook Destinations aren't signed. Their documented options are a URL, a request payload template and additional request headers. Add a secret header and compare it in your receiver.
Paragon webhook features
| Feature | Details |
|---|---|
| Configuration | Settings > Monitoring > Add Destination, type Webhook (HTTP Request) |
| Availability | Paid plans, Admin users only |
| Events | workflow_failure and credentials_failure |
| Method | POST |
| Payload | The full event object ($.event) by default, or a template you write |
| Templates | Ready-made payloads for Slack, Datadog, New Relic and Sentry |
| Headers | Optional, under Show more request options |
| Authentication | A secret request header you add |
| Signature | None |
| Testing | Test Webhook button |
| Auto-disable | After too many non-2xx responses for consecutive events |
| Retries and timeout | Not documented |
Common events
Paragon supports two events, and says more are planned:
type | Fires when |
|---|---|
workflow_failure | An error occurs at any point in a workflow and the workflow reaches the Failed status |
credentials_failure | An OAuth refresh fails with an error that can't be retried. The user's credential is then marked as invalid |
Here is Paragon's example of a workflow failure, trimmed, with its placeholders filled in:
{
"type": "workflow_failure",
"timestamp": 1729629642607,
"timestampISO": "2024-10-22T20:40:42.607Z",
"message": "Workflow “Test Workflow Name“ failed for User user_123",
"user": { "id": "user_123", "metadata": {} },
"project": { "id": "613b038f-e9bf-49ec-bc58-ba8d620754c7", "name": "Test Project Name" },
"integration": { "id": "57a96b31-c69e-421a-bb32-7a25f9c6a893", "type": "slack" },
"workflow": { "id": "cae72d65-7fc6-4249-9047-48fc640bfd46", "name": "Test Workflow Name" },
"data": {
"type": "workflow_failure",
"workflowExecution": { "id": "07911c9b-2006-4348-938b-b7ebac94dc1f" },
"stepExecution": {
"stepId": "3507a59f-d552-45a5-8bca-ed5315111ca0",
"startTime": 1729629642606,
"endTime": 1729629642606
},
"error": "Test error message",
"executionUrl": "https://dashboard.useparagon.com/connect/projects/.../executions/07911c9b-2006-4348-938b-b7ebac94dc1f"
}
}
A credentials_failure has the same type, timestamp, timestampISO, message, user, project and integration fields, no workflow, and a data object with userId and error (for example invalid_grant). timestamp is in epoch milliseconds. user.id is your connected user's ID, which is what you need to notify the right customer.
This shape is only guaranteed with the default payload. If you change Request Payload to a template, the body contains whatever the template selects, and type is only there if you include it.
Inspect Paragon webhooks as they arrive. Point a Webhook Destination at a Hookdeck Inspect URL to inspect and replay real deliveries, with no account or setup required.
Setting up Paragon webhooks
Generate a long random secret first (openssl rand -hex 32 works) and store it in your app's environment.
- In Paragon, open the global Settings tab and select Monitoring under your Project Settings.
- Click Add Destination.
- In Type, select Webhook (HTTP Request).
- In URL, enter your HTTPS endpoint.
- Leave Request Payload at its default (
$.event) unless you need a different shape. - Expand Show more request options and add your secret header, for example
X-Webhook-Secretwith your secret value. - Click Test Webhook, then Save to create and enable the destination.
From the Monitoring page you can later update the destination, disable it to pause notifications, or delete it.
For local development, use the Hookdeck CLI: hookdeck listen 3000 paragon --path /webhooks/paragon gives you a public HTTPS URL that forwards to your local server, plus a web UI for inspecting and replaying deliveries, with no account required.
With Hookdeck. Create a Paragon source in Hookdeck, enter the API Key Header and API Key exactly as you set them on the destination, including any Bearer prefix, and use the source URL as the destination URL. The source is for monitoring Webhook Destinations only.
Securing Paragon webhooks
Compare your secret header with the stored value. Look it up case-insensitively, compare the whole value in constant time, and check lengths first, because crypto.timingSafeEqual throws on buffers of different lengths.
const crypto = require('crypto');
const express = require('express');
const app = express();
function safeEqual(a, b) {
const x = Buffer.from(String(a), 'utf8');
const y = Buffer.from(String(b), 'utf8');
return x.length === y.length && crypto.timingSafeEqual(x, y);
}
function verifyParagonRequest(headers) {
const secret = process.env.PARAGON_WEBHOOK_SECRET;
if (!secret) return false; // fail closed when unconfigured
const name = (process.env.PARAGON_WEBHOOK_HEADER || 'X-Webhook-Secret').toLowerCase();
const received = headers[name];
return typeof received === 'string' && safeEqual(received, secret);
}
app.post('/webhooks/paragon', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyParagonRequest(req.headers)) {
return res.sendStatus(401);
}
let event;
try {
event = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
res.sendStatus(200);
switch (event.type) {
case 'workflow_failure':
// alert the team, link to event.data.executionUrl
break;
case 'credentials_failure':
// ask user event.user.id to reconnect event.integration.type
break;
}
});
The same check in Python:
import hmac
import os
def verify_paragon_request(headers) -> bool:
secret = os.environ.get("PARAGON_WEBHOOK_SECRET")
if not secret:
return False
name = os.environ.get("PARAGON_WEBHOOK_HEADER", "X-Webhook-Secret")
received = headers.get(name) # case-insensitive in Starlette and Flask
if not received:
return False
return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))
A static header authenticates the sender but not the body, and gives no replay protection: nothing is signed and there's no timestamp check. Use HTTPS, keep the secret out of logs, and rotate it by updating the destination if it leaks. Be careful before acting automatically on a credentials_failure, for example emailing a customer, since a forged request could trigger it. Check the user's credential status through Paragon's API first.
Make Paragon webhooks production-ready. Hookdeck Event Gateway checks your secret header on every request, stores each failure notification durably, and keeps your destination enabled by answering Paragon even when your own service is failing.
Paragon webhook limitations and pain points
Failing receivers get disabled
The Problem: If your endpoint keeps returning errors, Paragon may switch the destination off, and you stop hearing about failures at exactly the moment you need to.
Why It Happens: Paragon says a destination "may be automatically disabled" after too many non-2xx responses for consecutive events. It doesn't publish the threshold.
Workarounds:
- Return
200quickly once the header checks out, and process the event afterwards. - Check the Monitoring page after any outage and re-enable the destination if needed.
How Hookdeck Can Help: Hookdeck answers Paragon as soon as the request is stored, so errors in your own service never count against the destination. It then delivers to your service with automatic retries, and Issues alert you when deliveries fail.
No signature
The Problem: A request with the right header value is indistinguishable from a real failure notification.
Why It Happens: Webhook Destinations offer request headers and nothing else for authentication.
Workarounds:
- Use a long random header value and compare it in constant time.
- Confirm a credential failure through the API before contacting the customer.
How Hookdeck Can Help: Hookdeck's Paragon source compares the header you name against the exact value you enter. A request without a matching header still gets a 200 response, but Hookdeck marks it as failed verification and creates no event, so it never reaches your destination. Every request is stored with its headers and body.
Custom payloads change the contract
The Problem: A handler written for the default event object breaks if someone changes the Request Payload template, and type can disappear.
Why It Happens: The template replaces the body with whatever it selects from the event.
Workarounds:
- Keep the default payload for destinations that feed your own code, and use templates only for third-party tools like Slack.
- If you do template the body, always include
type,user.idandintegration.
How Hookdeck Can Help: Keep the default payload going to Hookdeck and use transformations to reshape it for each destination, so one Paragon destination can feed several tools.
Two different webhook products
The Problem: It's easy to confuse monitoring Webhook Destinations with Managed Sync webhooks, which use a different payload, a verification request and a different configuration page.
Why It Happens: Both are called webhooks in Paragon's docs, but they belong to different features.
Workarounds:
- Use separate endpoints for monitoring and Managed Sync traffic.
- Route on
typefor monitoring events andeventfor Managed Sync events.
How Hookdeck Can Help: Create separate Hookdeck sources for each feature, so each one has its own URL, its own verification settings and its own delivery history.
Best practices
Route on type and act on user.id
workflow_failure usually means something to fix on your side, and data.executionUrl links to the failed run. credentials_failure usually means the customer must reconnect: use user.id and integration.type to prompt them through the Connect Portal.
Acknowledge fast and process asynchronously
Return 200 as soon as the header checks out, then send alerts or emails from a background job. See why to process webhooks asynchronously.
Deduplicate notifications
Paragon doesn't document an event ID for monitoring events. Use data.workflowExecution.id for workflow failures, and user.id, integration.id and timestamp for credential failures, so a repeated request doesn't send a customer two emails. See our guide to webhook idempotency.
Conclusion
Paragon's monitoring Webhook Destinations send an unsigned POST for workflow failures and unrecoverable credential failures, with type, user, integration and an event-specific data object in the default payload. Authenticate them with a secret request header, keep the default payload for code you own, and keep your receiver healthy, since repeated errors can disable the destination.
Hookdeck Event Gateway checks the header, stores every notification and retries delivery to your service, so failure alerts keep flowing even when the service receiving them is having a bad day.
Get started with Hookdeck for free and handle Paragon webhooks reliably in minutes.