Gareth Wilson Gareth Wilson

Guide to Paragon Webhooks: Features and Best Practices

Published


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 POST when 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_verification request 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

FeatureDetails
ConfigurationSettings > Monitoring > Add Destination, type Webhook (HTTP Request)
AvailabilityPaid plans, Admin users only
Eventsworkflow_failure and credentials_failure
MethodPOST
PayloadThe full event object ($.event) by default, or a template you write
TemplatesReady-made payloads for Slack, Datadog, New Relic and Sentry
HeadersOptional, under Show more request options
AuthenticationA secret request header you add
SignatureNone
TestingTest Webhook button
Auto-disableAfter too many non-2xx responses for consecutive events
Retries and timeoutNot documented

Common events

Paragon supports two events, and says more are planned:

typeFires when
workflow_failureAn error occurs at any point in a workflow and the workflow reaches the Failed status
credentials_failureAn 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.

  1. In Paragon, open the global Settings tab and select Monitoring under your Project Settings.
  2. Click Add Destination.
  3. In Type, select Webhook (HTTP Request).
  4. In URL, enter your HTTPS endpoint.
  5. Leave Request Payload at its default ($.event) unless you need a different shape.
  6. Expand Show more request options and add your secret header, for example X-Webhook-Secret with your secret value.
  7. 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 200 quickly 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.id and integration.

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 type for monitoring events and event for 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.